diff --git a/bin/bui-agent b/bin/bui-agent index 75537d82..4097e41e 100755 --- a/bin/bui-agent +++ b/bin/bui-agent @@ -14,7 +14,8 @@ if __name__ == '__main__': Main function """ parser = OptionParser() - parser.add_option('-v', '--verbose', dest='log', help='verbose output', action='store_true') + parser.add_option('-v', '--verbose', dest='log', help='increase output verbosity (e.g., -vv is more than -v)', action='count') + parser.add_option('-l', '--logfile', dest='logfile', help='where to store logs', metavar='LOGFILE') parser.add_option('-c', '--config', dest='config', help='configuration file', metavar='CONFIG') (options, args) = parser.parse_args() @@ -42,5 +43,5 @@ if __name__ == '__main__': conf = p break - agent = Agent(conf, options.log) + agent = Agent(conf, options.log, options.logfile) agent.run() diff --git a/burpui/agent.py b/burpui/agent.py index 51e9a290..6ab66328 100644 --- a/burpui/agent.py +++ b/burpui/agent.py @@ -6,13 +6,18 @@ try: import ujson as json except ImportError: import json +import re import time import sys +import logging import pickle import traceback import ConfigParser import SocketServer from threading import Thread +from logging import Formatter, StreamHandler +from logging.handlers import RotatingFileHandler +from burpui.misc.utils import BUIlogging g_port = '10000' g_bind = '::' @@ -24,13 +29,35 @@ g_timeout = '5' g_password = 'password' -class BUIAgent: - def __init__(self, conf=None, debug=False): +class BUIAgent(BUIlogging): + def __init__(self, conf=None, debug=False, logfile=None): global g_port, g_bind, g_ssl, g_version, g_sslcert, g_sslkey, g_password self.conf = conf self.dbg = debug - print 'conf: ' + self.conf - print 'debug: ' + str(self.dbg) + self.logger = None + if debug > logging.NOTSET: + levels = [0, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] + if debug >= len(levels): + debug = len(levels) - 1 + lvl = levels[debug] + self.logger = logging.getLogger(__name__) + self.logger.setLevel(lvl) + if logfile: + handler = RotatingFileHandler(logfile, maxBytes=1024 * 1024 * 100, backupCount=20) + LOG_FORMAT = '[%(asctime)s] %(levelname)s in %(module)s: %(message)s' + else: + handler = StreamHandler() + LOG_FORMAT = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 + ) + handler.setLevel(lvl) + handler.setFormatter(Formatter(LOG_FORMAT)) + self.logger.addHandler(handler) + self.logger.info('conf: ' + self.conf) + self.logger.info('level: ' + logging.getLevelName(lvl)) if not conf: raise IOError('No configuration file found') @@ -42,18 +69,18 @@ class BUIAgent: with open(conf) as fp: config.readfp(fp) try: - self.port = config.getint('Global', 'port') - self.bind = config.get('Global', 'bind') - self.vers = config.getint('Global', 'version') - self.timeout = config.getint('Global', 'timeout') + self.port = self._safe_config_get(config.getint, 'port', cast=int) + self.bind = self._safe_config_get(config.get, 'bind') + self.vers = self._safe_config_get(config.getint, 'version', cast=int) + self.timeout = self._safe_config_get(config.getint, 'timeout', cast=int) try: self.ssl = config.getboolean('Global', 'ssl') except ValueError: self.app.logger.error("Wrong value for 'ssl' key! Assuming 'false'") self.ssl = False - self.sslcert = config.get('Global', 'sslcert') - self.sslkey = config.get('Global', 'sslkey') - self.password = config.get('Global', 'password') + self.sslcert = self._safe_config_get(config.get, 'sslcert') + self.sslkey = self._safe_config_get(config.get, 'sslkey') + self.password = self._safe_config_get(config.get, 'password') except ConfigParser.NoOptionError, e: raise e @@ -62,6 +89,7 @@ class BUIAgent: mod = __import__(module, fromlist=['Burp']) Client = mod.Burp self.backend = Client(conf=conf) + self.backend.set_logger(self.logger) except Exception, e: traceback.print_exc() self.debug('Failed loading backend for Burp version %d: %s', self.vers, str(e)) @@ -89,22 +117,55 @@ class BUIAgent: self.server = AgentServer((self.bind, self.port), AgentTCPHandler, self) + def _safe_config_get(self, callback, key, sect='Global', cast=None): + """ + :func:`burpui.agent._safe_config_get` is a wrapper to handle + Exceptions throwed by :mod:`ConfigParser`. + + :param callback: Function to wrap + :type callback: callable + + :param key: Key to retrieve + :type key: str + + :param sect: Section of the config file to read + :type sect: str + + :param cast: Cast the returned value if provided + :type case: callable + + :returns: The value returned by the `callback` + """ + try: + return callback(sect, key) + except ConfigParser.NoOptionError as e: + self._logger('error', str(e)) + except ConfigParser.NoSectionError as e: + self._logger('warning', str(e)) + if key in self.defaults: + if cast: + return cast(self.defaults[key]) + return self.defaults[key] + return None + def run(self): try: self.server.serve_forever() except KeyboardInterrupt: sys.exit(0) - def debug(self, msg, *args): - if self.dbg: - print msg % (args) + def _logger(self, level, message): + # hide password from logs + msg = message + if self.logger.getEffectiveLevel() != logging.DEBUG: + msg = re.sub(r'"password": \S+', '"password": "*****",', message) + super(BUIAgent, self)._logger(level, msg) class AgentTCPHandler(SocketServer.BaseRequestHandler): "One instance per connection. Override handle(self) to customize action." def handle(self): # self.request is the client connection - self.server.agent.debug('===============>') timeout = self.server.agent.timeout try: err = None @@ -114,19 +175,17 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler): lengthbuf = self.request.recv(8) length, = struct.unpack('!Q', lengthbuf) data = self.recvall(length) - self.server.agent.debug('####################') - self.server.agent.debug('recv: %s', data) - self.server.agent.debug('####################') + self.server.agent._logger('info','recv: {}'.format(data)) j = json.loads(data) _, w, _ = select.select([], [self.request], [], timeout) if not w: raise Exception('Socket timed-out 2') if j['password'] != self.server.agent.password: - self.server.agent.debug('-----> Wrong Password <-----') + self.server.agent._logger('warning', '-----> Wrong Password <-----') self.request.sendall('KO') return if j['func'] not in self.server.agent.methods: - self.server.agent.debug('-----> Wrong method <-----') + self.server.agent._logger('warning', '-----> Wrong method <-----') self.request.sendall('KO') return self.request.sendall('OK') @@ -140,9 +199,7 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler): res = json.dumps(self.server.agent.methods[j['func']](**j['args'])) else: res = json.dumps(self.server.agent.methods[j['func']]()) - self.server.agent.debug('####################') - self.server.agent.debug('result: %s', res) - self.server.agent.debug('####################') + self.server.agent._logger('info', 'result: {}'.format(res)) _, w, _ = select.select([], [self.request], [], timeout) if not w: raise Exception('Socket timed-out 3') @@ -159,7 +216,7 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler): with open(res, 'rb') as f: buf = f.read(1024) while buf: - self.server.agent.debug('sending %d Bytes', len(buf)) + self.server.agent._logger('info', 'sending {} Bytes'.format(len(buf))) self.request.sendall(buf) buf = f.read(1024) _, w, _ = select.select([], [self.request], [], timeout) @@ -171,9 +228,7 @@ class AgentTCPHandler(SocketServer.BaseRequestHandler): self.request.sendall(res) self.request.close() except Exception as e: - self.server.agent.debug('ERROR: %s', str(e)) - finally: - self.server.agent.debug('<===============') + self.server.agent._logger('error', '{}'.format(str(e))) def recvall(self, length=1024): buf = b'' diff --git a/burpui/misc/backend/burp1.py b/burpui/misc/backend/burp1.py index ed86b430..bfbb5c67 100644 --- a/burpui/misc/backend/burp1.py +++ b/burpui/misc/backend/burp1.py @@ -102,10 +102,12 @@ class Burp(BUIbackend, BUIlogging): if dummy: return self.app = None + self.logger = None self.acl_handler = False if server: if hasattr(server, 'app'): self.app = server.app + self.set_logger(self.app.logger) self.acl_handler = server.acl_handler self.host = g_burphost self.port = int(g_burpport) diff --git a/burpui/misc/backend/burp2.py b/burpui/misc/backend/burp2.py index 24c21adc..13e6dea8 100644 --- a/burpui/misc/backend/burp2.py +++ b/burpui/misc/backend/burp2.py @@ -46,10 +46,12 @@ class Burp(Burp1): global g_burpbin, g_stripbin, g_burpconfcli, g_burpconfsrv, g_tmpdir, BURP_MINIMAL_VERSION self.proc = None self.app = None + self.logger = None self.acl_handler = False if server: if hasattr(server, 'app'): self.app = server.app + self.set_logger(self.app.logger) self.acl_handler = server.acl_handler self.burpbin = g_burpbin self.stripbin = g_stripbin diff --git a/burpui/misc/backend/interface.py b/burpui/misc/backend/interface.py index de65da42..73624982 100644 --- a/burpui/misc/backend/interface.py +++ b/burpui/misc/backend/interface.py @@ -7,6 +7,9 @@ class BUIbackend: self.host = host self.port = port + def set_logger(self, logger): + self.logger = logger + def status(self, query='\n', agent=None): raise NotImplementedError("Sorry, the current Backend does not implement this method!") diff --git a/burpui/misc/backend/multi.py b/burpui/misc/backend/multi.py index 1e76b869..3073f91d 100644 --- a/burpui/misc/backend/multi.py +++ b/burpui/misc/backend/multi.py @@ -17,16 +17,19 @@ except ImportError: import configparser as ConfigParser from burpui.misc.backend.interface import BUIbackend, BUIserverException +from burpui.misc.utils import BUIlogging -class Burp(BUIbackend): +class Burp(BUIbackend,BUIlogging): def __init__(self, server=None, conf=None): self.app = None + self.logger = None self.acl_handler = False if server: if hasattr(server, 'app'): self.app = server.app + self.set_logger(self.app.logger) self.acl_handler = server.acl_handler self.servers = {} self.app.config['SERVERS'] = [] diff --git a/burpui/misc/parser/interface.py b/burpui/misc/parser/interface.py index 3b2f0770..fd239849 100644 --- a/burpui/misc/parser/interface.py +++ b/burpui/misc/parser/interface.py @@ -5,6 +5,9 @@ class BUIparser(object): def __init__(self, app=None, conf=None): self.app = app self.conf = conf + self.logger = None + if self.app: + self.logger = self.app.logger def read_server_conf(self): raise NotImplementedError("Sorry, the current Parser does not implement this method!") diff --git a/burpui/misc/utils.py b/burpui/misc/utils.py index ca2fc72c..eadd3895 100644 --- a/burpui/misc/utils.py +++ b/burpui/misc/utils.py @@ -11,22 +11,24 @@ import sys import inspect import zipfile import tarfile +import logging if sys.version_info >= (3, 0): long = int class human_readable(long): - """ define a human_readable class to allow custom formatting - format specifiers supported : - em : formats the size as bits in IEC format i.e. 1024 bits (128 bytes) = 1Kib - eM : formats the size as Bytes in IEC format i.e. 1024 bytes = 1KiB - sm : formats the size as bits in SI format i.e. 1000 bits = 1kb - sM : formats the size as bytes in SI format i.e. 1000 bytes = 1KB - cm : format the size as bit in the common format i.e. 1024 bits (128 bytes) = 1Kb - cM : format the size as bytes in the common format i.e. 1024 bytes = 1KB + """ + define a human_readable class to allow custom formatting + format specifiers supported : + em : formats the size as bits in IEC format i.e. 1024 bits (128 bytes) = 1Kib + eM : formats the size as Bytes in IEC format i.e. 1024 bytes = 1KiB + sm : formats the size as bits in SI format i.e. 1000 bits = 1kb + sM : formats the size as bytes in SI format i.e. 1000 bytes = 1KB + cm : format the size as bit in the common format i.e. 1024 bits (128 bytes) = 1Kb + cM : format the size as bytes in the common format i.e. 1024 bytes = 1KB - code from: http://code.activestate.com/recipes/578323-human-readable-filememory-sizes-v2/ + code from: http://code.activestate.com/recipes/578323-human-readable-filememory-sizes-v2/ """ def __format__(self, fmt): # is it an empty format or not a special format for the size class @@ -73,31 +75,24 @@ def currentframe(): class BUIlogging(object): def _logger(self, level, *args): - if self.app: - logs = { - 'info': self.app.logger.info, - 'error': self.app.logger.error, - 'debug': self.app.logger.debug, - 'warning': self.app.logger.warning - } - if level in logs: + if self.logger: + """ + Try to guess where was call the function + """ + cf = currentframe() + (frame, filename, line_number, function_name, lines, index) = inspect.getouterframes(cf)[1] + if cf is not None: + cf = cf.f_back """ - Try to guess where was call the function + Ugly hack to reformat the message """ - cf = currentframe() - (frame, filename, line_number, function_name, lines, index) = inspect.getouterframes(cf)[1] - if cf is not None: - cf = cf.f_back - """ - Ugly hack to reformat the message - """ - ar = list(args) - if isinstance(ar[0], str): - ar[0] = filename + ':' + str(cf.f_lineno) + ' => ' + ar[0] - else: - ar = [filename + ':' + str(cf.f_lineno) + ' => {0}'.format(ar)] - args = tuple(ar) - logs[level](*args) + ar = list(args) + if isinstance(ar[0], str): + ar[0] = filename + ':' + str(cf.f_lineno) + ' => ' + ar[0] + else: + ar = [filename + ':' + str(cf.f_lineno) + ' => {0}'.format(ar)] + args = tuple(ar) + self.logger.log(logging.getLevelName(level.upper()), *args) class BUIcompress(): diff --git a/burpui/server.py b/burpui/server.py index 72387213..992e6ff9 100644 --- a/burpui/server.py +++ b/burpui/server.py @@ -90,10 +90,13 @@ class BUIServer: traceback.print_exc() self.app.logger.error('Import Exception, module \'{0}\': {1}'.format(self.auth, str(e))) sys.exit(1) + self.acl_engine = self._safe_config_get(config.get, 'acl') else: # I know that's ugly, but hey, I need it! self.app.login_manager._login_disabled = True - self.acl_engine = self._safe_config_get(config.get, 'acl') + # No login => no ACL + self.acl_engine = 'none' + if self.acl_engine and self.acl_engine.lower() != 'none': try: mod = __import__(