diff --git a/burpui/agent.py b/burpui/agent.py index 0856e1f6..01730ec9 100644 --- a/burpui/agent.py +++ b/burpui/agent.py @@ -12,16 +12,16 @@ from gevent.server import StreamServer from logging.handlers import RotatingFileHandler from .exceptions import BUIserverException from .misc.backend.interface import BUIbackend -from ._compat import ConfigParser, pickle -from .utils import BUIlogging +from ._compat import pickle +from .utils import BUIlogging, BUIConfig -g_port = u'10000' -g_bind = u'::' -g_ssl = u'' -g_version = u'1' -g_sslcert = u'' -g_sslkey = u'' -g_password = u'password' +G_PORT = 10000 +G_BIND = u'::' +G_SSL = False +G_VERSION = 1 +G_SSLCERT = u'' +G_SSLKEY = u'' +G_PASSWORD = u'password' DISCLOSURE = 5 @@ -63,13 +63,18 @@ class BurpHandler(BUIbackend): class BUIAgent(BUIbackend, BUIlogging): BUIbackend.__abstractmethods__ = frozenset() defaults = { - 'port': g_port, 'bind': g_bind, - 'ssl': g_ssl, 'sslcert': g_sslcert, 'sslkey': g_sslkey, - 'version': g_version, 'password': g_password + 'Global': { + 'port': G_PORT, + 'bind': G_BIND, + 'ssl': G_SSL, + 'sslcert': G_SSLCERT, + 'sslkey': G_SSLKEY, + 'version': G_VERSION, + 'password': G_PASSWORD, + }, } def __init__(self, conf=None, level=0, logfile=None, debug=False): - self.conf = conf self.debug = debug self.padding = 1 if level > logging.NOTSET: @@ -102,28 +107,21 @@ class BUIAgent(BUIbackend, BUIlogging): handler.setLevel(lvl) handler.setFormatter(logging.Formatter(LOG_FORMAT)) self.logger.addHandler(handler) - self._logger('info', 'conf: ' + self.conf) + self._logger('info', 'conf: ' + conf) self._logger('info', 'level: ' + logging.getLevelName(lvl)) - if not self.conf: + if not conf: raise IOError('No configuration file found') - config = ConfigParser.ConfigParser(self.defaults) - with open(self.conf) as fp: - config.readfp(fp) - try: - self.port = self._safe_config_get(config.getint, 'port', 'Global', cast=int) - self.bind = self._safe_config_get(config.get, 'bind', 'Global') - self.vers = self._safe_config_get(config.getint, 'version', 'Global', cast=int) - try: - self.ssl = config.getboolean('Global', 'ssl') - except ValueError: - self._logger('warning', "Wrong value for 'ssl' key! Assuming 'false'") - self.ssl = False - self.sslcert = self._safe_config_get(config.get, 'sslcert', 'Global') - self.sslkey = self._safe_config_get(config.get, 'sslkey', 'Global') - self.password = self._safe_config_get(config.get, 'password', 'Global') - except ConfigParser.NoOptionError as e: - raise e + # Raise exception if errors are encountered during parsing + self.conf = BUIConfig(conf, True, self.defaults) + self.conf.default_section('Global') + self.port = self.conf.safe_get('port', 'integer') + self.bind = self.conf.safe_get('bind') + self.vers = self.conf.safe_get('version', 'integer') + self.ssl = self.conf.safe_get('ssl', 'boolean') + self.sslcert = self.conf.safe_get('sslcert') + self.sslkey = self.conf.safe_get('sslkey') + self.password = self.conf.safe_get('password') self.cli = BurpHandler(self.vers, self.logger, self.conf) if not self.ssl: diff --git a/burpui/misc/acl/basic.py b/burpui/misc/acl/basic.py index 25d92e78..e96817ae 100644 --- a/burpui/misc/acl/basic.py +++ b/burpui/misc/acl/basic.py @@ -1,6 +1,5 @@ # -*- coding: utf8 -*- from .interface import BUIacl, BUIaclLoader -from ..._compat import ConfigParser import json @@ -8,7 +7,11 @@ import json class ACLloader(BUIaclLoader): """See :class:`burpui.misc.acl.interface.BUIaclLoader`""" def __init__(self, app=None): - """See :func:`burpui.misc.acl.interface.BUIaclLoader.__init__`""" + """See :func:`burpui.misc.acl.interface.BUIaclLoader.__init__` + + :param app: Application context + :type app: :class:`burpui.server.BUIServer` + """ self.app = app self.admins = [ 'admin' @@ -16,34 +19,23 @@ class ACLloader(BUIaclLoader): self.clients = {} self.servers = {} self.standalone = self.app.standalone - conf = self.app.config['CFG'] - c = ConfigParser.ConfigParser() + conf = self.app.conf adms = [] - with open(conf) as fp: - c.readfp(fp) - if c.has_section('BASIC:ACL'): + if 'BASIC:ACL' in conf.options: + adms = conf.safe_get('admin', 'force_list', section='BASIC:ACL') + for opt in conf.options.get('BASIC:ACL').keys(): + if opt == 'admin': + continue + lit = conf.safe_get(opt, section='BASIC:ACL') + rec = [] try: - temp = c.get('BASIC:ACL', 'admin') - try: - adms = json.loads(temp) - except Exception as e: - self.logger.error(str(e)) - adms = [temp] + rec = json.loads(lit) + if isinstance(rec, dict): + self.servers[opt] = rec.keys() except Exception as e: - self.logger.warning(str(e)) - for opt in c.options('BASIC:ACL'): - if opt == 'admin': - continue - lit = c.get('BASIC:ACL', opt) - rec = [] - try: - rec = json.loads(lit) - if isinstance(rec, dict): - self.servers[opt] = rec.keys() - except Exception as e: - self.logger.error(str(e)) - rec = [lit] - self.clients[opt] = rec + self.logger.error(str(e)) + rec = [lit] + self.clients[opt] = rec if adms: self.admins = adms diff --git a/burpui/misc/auth/handler.py b/burpui/misc/auth/handler.py index f1ddd863..f154bc4a 100644 --- a/burpui/misc/auth/handler.py +++ b/burpui/misc/auth/handler.py @@ -1,12 +1,10 @@ # -*- coding: utf8 -*- +import os + from .interface import BUIhandler, BUIuser from importlib import import_module from flask import session -import re -import os -import json - class UserAuthHandler(BUIhandler): """See :class:`burpui.misc.auth.interface.BUIhandler`""" @@ -17,10 +15,7 @@ class UserAuthHandler(BUIhandler): self.backends = [] if self.app.auth: me, _ = os.path.splitext(os.path.basename(__file__)) - try: - back = json.loads(self.app.auth) - except: - back = re.split(' *,+ *', self.app.auth) + back = self.app.auth for au in back: if au == me: self.app.logger.error('Recursive import not permited!') diff --git a/burpui/misc/backend/burp1.py b/burpui/misc/backend/burp1.py index 84f3d10b..f5fa6117 100644 --- a/burpui/misc/backend/burp1.py +++ b/burpui/misc/backend/burp1.py @@ -16,9 +16,7 @@ import json import shutil import subprocess import tempfile -import codecs -from ast import literal_eval from pipes import quote from six import iteritems, viewkeys @@ -26,19 +24,19 @@ from .interface import BUIbackend from ..parser.burp1 import Parser from ...utils import human_readable as _hr, BUIcompress from ...exceptions import BUIserverException -from ..._compat import ConfigParser, unquote, PY3 +from ..._compat import unquote, PY3 -G_BURPPORT = u'4972' +G_BURPPORT = 4972 G_BURPHOST = u'::1' G_BURPBIN = u'/usr/sbin/burp' G_STRIPBIN = u'/usr/sbin/vss_strip' G_BURPCONFCLI = u'' G_BURPCONFSRV = u'/etc/burp/burp-server.conf' G_TMPDIR = u'/tmp/bui' -G_ZIP64 = u'False' -G_INCLUDES = u'/etc/burp' -G_ENFORCE = u'False' -G_REVOKE = u'False' +G_ZIP64 = False +G_INCLUDES = [u'/etc/burp'] +G_ENFORCE = False +G_REVOKE = False class Burp(BUIbackend): @@ -110,8 +108,8 @@ class Burp(BUIbackend): and/or some global settings :type server: :class:`burpui.server.BUIServer` - :param conf: Configuration file to use - :type conf: str + :param conf: Configuration to use + :type conf: :class:`burpui.utils.BUIConfig` :param dummy: Does not instanciate the object (used for development purpose) @@ -122,101 +120,105 @@ class Burp(BUIbackend): self.client_version = None self.server_version = None self.app = None - self.zip64 = literal_eval(G_ZIP64) + self.zip64 = G_ZIP64 self.host = G_BURPHOST - self.port = int(G_BURPPORT) + self.port = G_BURPPORT self.burpbin = G_BURPBIN self.stripbin = G_STRIPBIN self.burpconfcli = G_BURPCONFCLI self.burpconfsrv = G_BURPCONFSRV self.tmpdir = G_TMPDIR self.includes = G_INCLUDES - self.revoke = literal_eval(G_REVOKE) - self.enforce = literal_eval(G_ENFORCE) + self.revoke = G_REVOKE + self.enforce = G_ENFORCE self.running = [] self.defaults = { - 'bport': G_BURPPORT, - 'bhost': G_BURPHOST, - 'burpbin': G_BURPBIN, - 'stripbin': G_STRIPBIN, - 'bconfcli': G_BURPCONFCLI, - 'bconfsrv': G_BURPCONFSRV, - 'tmpdir': G_TMPDIR, - 'zip64': G_ZIP64, - 'includes': G_INCLUDES, - 'revoke': G_REVOKE, - 'enforce': G_ENFORCE, + 'Burp1': { + 'bport': G_BURPPORT, + 'bhost': G_BURPHOST, + 'burpbin': G_BURPBIN, + 'stripbin': G_STRIPBIN, + 'bconfcli': G_BURPCONFCLI, + 'bconfsrv': G_BURPCONFSRV, + 'tmpdir': G_TMPDIR, + }, + 'Experimental': { + 'zip64': G_ZIP64, + }, + 'Security': { + 'includes': G_INCLUDES, + 'revoke': G_REVOKE, + 'enforce': G_ENFORCE, + }, } if conf: - config = ConfigParser.ConfigParser(self.defaults) - with codecs.open(conf, 'r', 'utf-8') as fileobj: - config.readfp(fileobj) + conf.update_defaults(self.defaults) + conf.default_section('Burp1') + self.port = conf.safe_get('bport', 'integer') + self.host = conf.safe_get('bhost') + self.burpbin = self._get_binary_path( + conf, + 'burpbin', + G_BURPBIN + ) + self.stripbin = self._get_binary_path( + conf, + 'stripbin', + G_STRIPBIN + ) + confcli = conf.safe_get('bconfcli') + confsrv = conf.safe_get('bconfsrv') + tmpdir = conf.safe_get('tmpdir') - self.port = self._safe_config_get(config.getint, 'bport', cast=int) - self.host = self._safe_config_get(config.get, 'bhost') - self.burpbin = self._get_binary_path( - config, - 'burpbin', - G_BURPBIN - ) - self.stripbin = self._get_binary_path( - config, - 'stripbin', - G_STRIPBIN - ) - confcli = self._safe_config_get(config.get, 'bconfcli') - confsrv = self._safe_config_get(config.get, 'bconfsrv') - tmpdir = self._safe_config_get(config.get, 'tmpdir') + # Experimental options + self.zip64 = conf.safe_get( + 'zip64', + 'boolean', + section='Experimental' + ) - # Experimental options - self.zip64 = self._safe_config_get( - config.getboolean, - 'zip64', - sect='Experimental' - ) + # Security options + self.includes = conf.safe_get( + 'includes', + 'force_list', + section='Security' + ) + self.enforce = conf.safe_get( + 'enforce', + 'boolean', + section='Security' + ) + self.revoke = conf.safe_get( + 'revoke', + 'boolean', + section='Security' + ) - # Security options - self.includes = self._safe_config_get( - config.get, - 'includes', - sect='Security' - ) - self.enforce = self._safe_config_get( - config.getboolean, - 'enforce', - sect='Security' - ) - self.revoke = self._safe_config_get( - config.getboolean, - 'revoke', - sect='Security' - ) + if tmpdir and os.path.exists(tmpdir) and not os.path.isdir(tmpdir): + self.logger.warning("'%s' is not a directory", tmpdir) + if tmpdir == G_TMPDIR: + raise IOError("Cannot use '{}' as tmpdir".format(tmpdir)) + tmpdir = G_TMPDIR + if os.path.exists(tmpdir) and not os.path.isdir(tmpdir): + raise IOError("Cannot use '{}' as tmpdir".format(tmpdir)) + if tmpdir and not os.path.exists(tmpdir): + os.makedirs(tmpdir) - if tmpdir and os.path.exists(tmpdir) and not os.path.isdir(tmpdir): - self.logger.warning("'%s' is not a directory", tmpdir) - if tmpdir == G_TMPDIR: - raise IOError("Cannot use '{}' as tmpdir".format(tmpdir)) - tmpdir = G_TMPDIR - if os.path.exists(tmpdir) and not os.path.isdir(tmpdir): - raise IOError("Cannot use '{}' as tmpdir".format(tmpdir)) - if tmpdir and not os.path.exists(tmpdir): - os.makedirs(tmpdir) + if confcli and not os.path.isfile(confcli): + self.logger.warning("The file '%s' does not exist", confcli) + confcli = None - if confcli and not os.path.isfile(confcli): - self.logger.warning("The file '%s' does not exist", confcli) - confcli = None + if confsrv and not os.path.isfile(confsrv): + self.logger.warning("The file '%s' does not exist", confsrv) + confsrv = None - if confsrv and not os.path.isfile(confsrv): - self.logger.warning("The file '%s' does not exist", confsrv) - confsrv = None + if self.host not in ['127.0.0.1', '::1']: + self.logger.warning("Invalid value for 'bhost'. Must be '127.0.0.1' or '::1'. Falling back to '%s'", G_BURPHOST) + self.host = G_BURPHOST - if self.host not in ['127.0.0.1', '::1']: - self.logger.warning("Invalid value for 'bhost'. Must be '127.0.0.1' or '::1'. Falling back to '%s'", G_BURPHOST) - self.host = G_BURPHOST - - self.burpconfcli = confcli - self.burpconfsrv = confsrv - self.tmpdir = tmpdir + self.burpconfcli = confcli + self.burpconfsrv = confsrv + self.tmpdir = tmpdir self.parser = Parser(self) @@ -268,7 +270,7 @@ class Burp(BUIbackend): :param default: Default value in case the retrieved value is not correct :type default: str """ - temp = self._safe_config_get(config.get, field, sect) + temp = config.safe_get(field, section=sect) if temp and not temp.startswith('/'): self.logger.warning("Please provide an absolute path for the '{}' option. Fallback to '{}'".format(field, default)) @@ -533,6 +535,11 @@ class Burp(BUIbackend): backup[ckey[0]][ckey[1]] = int(val) else: backup[ckey] = int(val) + + # Needed for graphs + if 'received' not in backup: + backup['received'] = 1 + return backup def _parse_backup_log(self, filemap, number, client=None, agent=None): diff --git a/burpui/misc/backend/burp2.py b/burpui/misc/backend/burp2.py index bae07978..6cc7e237 100644 --- a/burpui/misc/backend/burp2.py +++ b/burpui/misc/backend/burp2.py @@ -11,11 +11,9 @@ import re import os import time import subprocess -import codecs import sys import json -from ast import literal_eval from select import select from six import iteritems, viewkeys @@ -23,7 +21,6 @@ from .burp1 import Burp as Burp1 from ..parser.burp2 import Parser from ...utils import human_readable as _hr from ...exceptions import BUIserverException -from ..._compat import ConfigParser if sys.version_info < (3, 3): TimeoutError = OSError @@ -35,11 +32,11 @@ G_STRIPBIN = u'/usr/sbin/vss_strip' G_BURPCONFCLI = u'/etc/burp/burp.conf' G_BURPCONFSRV = u'/etc/burp/burp-server.conf' G_TMPDIR = u'/tmp/bui' -G_TIMEOUT = u'5' -G_ZIP64 = u'False' -G_INCLUDES = u'/etc/burp' -G_ENFORCE = u'False' -G_REVOKE = u'False' +G_TIMEOUT = 5 +G_ZIP64 = False +G_INCLUDES = [u'/etc/burp'] +G_ENFORCE = False +G_REVOKE = False # Some functions are the same as in Burp1 backend @@ -54,149 +51,146 @@ class Burp(Burp1): and/or some global settings :type server: :class:`burpui.server.BUIServer` - :param conf: Configuration file to use - :type conf: str + :param conf: Configuration to use + :type conf: :class:`burpui.utils.BUIConfig` """ def __init__(self, server=None, conf=None): - global G_BURPBIN, G_STRIPBIN, G_BURPCONFCLI, G_BURPCONFSRV, G_TMPDIR, \ - G_TIMEOUT, BURP_MINIMAL_VERSION + """ + :param server: ``Burp-UI`` server instance in order to access logger + and/or some global settings + :type server: :class:`burpui.server.BUIServer` + + :param conf: Configuration to use + :type conf: :class:`burpui.utils.BUIConfig` + """ self.proc = None self.app = server self.client_version = None self.server_version = None - self.zip64 = literal_eval(G_ZIP64) - self.timeout = int(G_TIMEOUT) + self.zip64 = G_ZIP64 + self.timeout = G_TIMEOUT self.burpbin = G_BURPBIN self.stripbin = G_STRIPBIN self.burpconfcli = G_BURPCONFCLI self.burpconfsrv = G_BURPCONFSRV self.includes = G_INCLUDES - self.revoke = literal_eval(G_REVOKE) - self.enforce = literal_eval(G_ENFORCE) + self.revoke = G_REVOKE + self.enforce = G_ENFORCE self.defaults = { - 'burpbin': G_BURPBIN, - 'stripbin': G_STRIPBIN, - 'bconfcli': G_BURPCONFCLI, - 'bconfsrv': G_BURPCONFSRV, - 'timeout': G_TIMEOUT, - 'tmpdir': G_TMPDIR, - 'zip64': G_ZIP64, - 'includes': G_INCLUDES, - 'revoke': G_REVOKE, - 'enforce': G_ENFORCE, + 'Burp2': { + 'burpbin': G_BURPBIN, + 'stripbin': G_STRIPBIN, + 'bconfcli': G_BURPCONFCLI, + 'bconfsrv': G_BURPCONFSRV, + 'timeout': G_TIMEOUT, + 'tmpdir': G_TMPDIR, + }, + 'Experimental': { + 'zip64': G_ZIP64, + }, + 'Security': { + 'includes': G_INCLUDES, + 'revoke': G_REVOKE, + 'enforce': G_ENFORCE, + }, } self.running = [] version = '' if conf: - config = ConfigParser.ConfigParser(self.defaults) - with codecs.open(conf, 'r', 'utf-8') as conffile: - config.readfp(conffile) - try: - self.burpbin = self._get_binary_path( - config, - 'burpbin', - G_BURPBIN, - sect='Burp2' - ) - self.stripbin = self._get_binary_path( - config, - 'stripbin', - G_STRIPBIN, - sect='Burp2' - ) - confcli = self._safe_config_get( - config.get, - 'bconfcli', - sect='Burp2' - ) - confsrv = self._safe_config_get( - config.get, - 'bconfsrv', - sect='Burp2' - ) - self.timeout = self._safe_config_get( - config.getint, - 'timeout', - sect='Burp2', - cast=int - ) - tmpdir = self._safe_config_get( - config.get, - 'tmpdir', - sect='Burp2' - ) + conf.update_defaults(self.defaults) + conf.default_section('Burp2') + self.burpbin = self._get_binary_path( + conf, + 'burpbin', + G_BURPBIN, + sect='Burp2' + ) + self.stripbin = self._get_binary_path( + conf, + 'stripbin', + G_STRIPBIN, + sect='Burp2' + ) + confcli = conf.safe_get( + 'bconfcli' + ) + confsrv = conf.safe_get( + 'bconfsrv' + ) + self.timeout = conf.safe_get( + 'timeout', + 'integer' + ) + tmpdir = conf.safe_get( + 'tmpdir' + ) - # Experimental options - self.zip64 = self._safe_config_get( - config.getboolean, - 'zip64', - sect='Experimental', - cast=bool + # Experimental options + self.zip64 = conf.safe_get( + 'zip64', + 'boolean', + section='Experimental' + ) + + # Security options + self.includes = conf.safe_get( + 'includes', + 'force_list', + section='Security' + ) + self.enforce = conf.safe_get( + 'enforce', + 'boolean', + section='Security' + ) + self.revoke = conf.safe_get( + 'revoke', + 'boolean', + section='Security' + ) + + if (tmpdir and os.path.exists(tmpdir) and + not os.path.isdir(tmpdir)): + self.logger.warning( + "'%s' is not a directory", + tmpdir + ) + if tmpdir == G_TMPDIR: + raise IOError( + "Cannot use '{}' as tmpdir".format(tmpdir) ) - - # Security options - self.includes = self._safe_config_get( - config.get, - 'includes', - sect='Security' - ) - self.enforce = self._safe_config_get( - config.getboolean, - 'enforce', - sect='Security' - ) - self.revoke = self._safe_config_get( - config.getboolean, - 'revoke', - sect='Security' + tmpdir = G_TMPDIR + if os.path.exists(tmpdir) and not os.path.isdir(tmpdir): + raise IOError( + "Cannot use '{}' as tmpdir".format(tmpdir) ) + if tmpdir and not os.path.exists(tmpdir): + os.makedirs(tmpdir) - if (tmpdir and os.path.exists(tmpdir) and - not os.path.isdir(tmpdir)): - self.logger.warning( - "'%s' is not a directory", - tmpdir - ) - if tmpdir == G_TMPDIR: - raise IOError( - "Cannot use '{}' as tmpdir".format(tmpdir) - ) - tmpdir = G_TMPDIR - if os.path.exists(tmpdir) and not os.path.isdir(tmpdir): - raise IOError( - "Cannot use '{}' as tmpdir".format(tmpdir) - ) - if tmpdir and not os.path.exists(tmpdir): - os.makedirs(tmpdir) + if confcli and not os.path.isfile(confcli): + self.logger.warning( + "The file '%s' does not exist", + confcli + ) + confcli = G_BURPCONFCLI - if confcli and not os.path.isfile(confcli): - self.logger.warning( - "The file '%s' does not exist", - confcli - ) - confcli = G_BURPCONFCLI + if confsrv and not os.path.isfile(confsrv): + self.logger.warning( + "The file '%s' does not exist", + confsrv + ) + confsrv = G_BURPCONFSRV - if confsrv and not os.path.isfile(confsrv): - self.logger.warning( - "The file '%s' does not exist", - confsrv - ) - confsrv = G_BURPCONFSRV + if not self.burpbin: + # The burp binary is mandatory for this backend + raise Exception( + 'This backend *CAN NOT* work without a burp binary' + ) - if not self.burpbin: - # The burp binary is mandatory for this backend - raise Exception( - 'This backend *CAN NOT* work without a burp binary' - ) - - self.tmpdir = tmpdir - self.burpconfcli = confcli - self.burpconfsrv = confsrv - except ConfigParser.NoOptionError as exp: - self.logger.error(str(exp)) - except ConfigParser.NoSectionError as exp: - self.logger.warning(str(exp)) + self.tmpdir = tmpdir + self.burpconfcli = confcli + self.burpconfsrv = confsrv # check the burp version because this backend only supports clients # newer than BURP_MINIMAL_VERSION @@ -576,6 +570,10 @@ class Burp(Burp1): if 'start' in backup and 'end' in backup: backup['duration'] = backup['end'] - backup['start'] + # Needed for graphs + if 'received' not in backup: + backup['received'] = 1 + return backup # TODO: support old clients diff --git a/burpui/misc/parser/burp1.py b/burpui/misc/parser/burp1.py index dde4a854..73d98aec 100644 --- a/burpui/misc/parser/burp1.py +++ b/burpui/misc/parser/burp1.py @@ -258,7 +258,7 @@ class Parser(Doc): # don't check return True path = os.path.normpath(path) - cond = [path.startswith(x) for x in self.backend.includes.split(',')] + cond = [path.startswith(x) for x in self.backend.includes] if not any(cond): self.logger.warning( 'Tried to access non-allowed path: {}'.format(path) @@ -459,7 +459,8 @@ class Parser(Doc): res2[u'includes'] = parsed.flatten('include', False).keys() res2[u'includes_ext'] = parsed.include - self.filecache[path]['parsed'] = res2 + if path in self.filecache: + self.filecache[path]['parsed'] = res2 res.update(res2) return res @@ -503,7 +504,8 @@ class Parser(Doc): res2[u'includes'] = parsed.flatten('include', False).keys() res2[u'includes_ext'] = parsed.include - self.filecache[path]['parsed'] = res2 + if path in self.filecache: + self.filecache[path]['parsed'] = res2 res.update(res2) return res diff --git a/burpui/server.py b/burpui/server.py index bcfc79d6..5e6541eb 100644 --- a/burpui/server.py +++ b/burpui/server.py @@ -13,29 +13,29 @@ import logging import traceback from .misc.auth.handler import UserAuthHandler -from ._compat import ConfigParser +from .utils import BUIConfig from datetime import timedelta from flask import Flask -G_PORT = '5000' -G_BIND = '::' -G_REFRESH = '180' -G_LIVEREFRESH = '5' -G_SSL = 'False' -G_STANDALONE = 'True' -G_SSLCERT = '' -G_SSLKEY = '' -G_VERSION = '1' -G_AUTH = 'basic' -G_ACL = '' -G_STORAGE = '' -G_REDIS = '' -G_SCOOKIE = 'False' -G_APPSECRET = 'random' -G_COOKIETIME = '14' -G_PREFIX = '' +G_PORT = 5000 +G_BIND = u'::' +G_REFRESH = 180 +G_LIVEREFRESH = 5 +G_SSL = False +G_STANDALONE = True +G_SSLCERT = u'' +G_SSLKEY = u'' +G_VERSION = 1 +G_AUTH = [u'basic'] +G_ACL = u'none' +G_STORAGE = u'' +G_REDIS = u'' +G_SCOOKIE = False +G_APPSECRET = u'random' +G_COOKIETIME = 14 +G_PREFIX = u'' class BUIServer(Flask): @@ -44,6 +44,34 @@ class BUIServer(Flask): """ gunicorn = False + defaults = { + 'Global': { + 'port': G_PORT, + 'bind': G_BIND, + 'ssl': G_SSL, + 'standalone': G_STANDALONE, + 'sslcert': G_SSLCERT, + 'sslkey': G_SSLKEY, + 'version': G_VERSION, + 'auth': G_AUTH, + 'acl': G_ACL, + 'prefix': G_PREFIX, + }, + 'UI': { + 'refresh': G_REFRESH, + 'liverefresh': G_LIVEREFRESH, + }, + 'Security': { + 'scookie': G_SCOOKIE, + 'appsecret': G_APPSECRET, + 'cookietime': G_COOKIETIME, + }, + 'Production': { + 'storage': G_STORAGE, + 'redis': G_REDIS, + } + } + def __init__(self): """The :class:`burpui.server.BUIServer` class provides the ``Burp-UI`` server. @@ -79,158 +107,137 @@ class BUIServer(Flask): if not conf: raise IOError('No configuration file found') - self.defaults = { - 'port': G_PORT, - 'bind': G_BIND, - 'refresh': G_REFRESH, - 'ssl': G_SSL, - 'sslcert': G_SSLCERT, - 'sslkey': G_SSLKEY, - 'version': G_VERSION, - 'auth': G_AUTH, - 'standalone': G_STANDALONE, - 'acl': G_ACL, - 'liverefresh': G_LIVEREFRESH, - 'storage': G_STORAGE, - 'redis': G_REDIS, - 'scookie': G_SCOOKIE, - 'appsecret': G_APPSECRET, - 'cookietime': G_COOKIETIME, - 'prefix': G_PREFIX, - } - config = ConfigParser.ConfigParser(self.defaults) - with open(conf) as fp: - config.readfp(fp) + # Raise exception if errors are encountered during parsing + self.conf = BUIConfig(conf, True, self.defaults) + self.conf.default_section('Global') + + self.port = self.conf.safe_get( + 'port', + 'integer' + ) + self.bind = self.conf.safe_get('bind') + self.vers = self.conf.safe_get( + 'version', + 'integer' + ) + self.ssl = self.conf.safe_get( + 'ssl', + 'boolean' + ) + self.standalone = self.conf.safe_get( + 'standalone', + 'boolean' + ) + self.sslcert = self.conf.safe_get( + 'sslcert' + ) + self.sslkey = self.conf.safe_get( + 'sslkey' + ) + self.prefix = self.conf.safe_get( + 'prefix' + ) + if self.prefix and not self.prefix.startswith('/'): + if self.prefix.lower() != 'none': + self.logger.warning("'prefix' must start with a '/'!") + self.prefix = '' + + self.auth = self.conf.safe_get( + 'auth', + 'string_lower_list' + ) + if self.auth and 'none' not in self.auth: try: - self.port = self._safe_config_get( - config.getint, - 'port', - cast=int + self.uhandler = UserAuthHandler(self) + except Exception as e: + self.logger.critical( + 'Import Exception, module \'{0}\': {1}'.format( + self.auth, + str(e) + ) ) - self.bind = self._safe_config_get(config.get, 'bind') - self.vers = self._safe_config_get( - config.getint, - 'version', - cast=int - ) - self.ssl = self._safe_config_get( - config.getboolean, - 'ssl', - cast=bool - ) - self.standalone = self._safe_config_get( - config.getboolean, - 'standalone', - cast=bool - ) - self.sslcert = self._safe_config_get(config.get, 'sslcert') - self.sslkey = self._safe_config_get(config.get, 'sslkey') - self.prefix = self._safe_config_get(config.get, 'prefix') - if self.prefix and not self.prefix.startswith('/'): - if self.prefix.lower() != 'none': - self.logger.warning("'prefix' must start with a '/'!") - self.prefix = '' - self.auth = self._safe_config_get(config.get, 'auth') - if self.auth and self.auth.lower() != 'none': - try: - self.uhandler = UserAuthHandler(self) - except Exception as e: - self.logger.critical( - 'Import Exception, module \'{0}\': {1}'.format( - self.auth, - str(e) - ) - ) - raise e - self.acl_engine = self._safe_config_get(config.get, 'acl') - else: - self.config['LOGIN_DISABLED'] = True - # No login => no ACL - self.acl_engine = 'none' - self.auth = 'none' + raise e + self.acl_engine = self.conf.safe_get( + 'acl' + ) + else: + self.config['LOGIN_DISABLED'] = True + # No login => no ACL + self.acl_engine = 'none' + self.auth = 'none' - if self.acl_engine and self.acl_engine.lower() != 'none': - try: - # Try to load submodules from our current environment - # first - sys.path.insert( - 0, - os.path.dirname(os.path.abspath(__file__)) - ) - mod = __import__( - 'burpui.misc.acl.{0}'.format( - self.acl_engine.lower() - ), - fromlist=['ACLloader'] - ) - ACLloader = mod.ACLloader - self.acl_handler = ACLloader(self) - # for development purpose only - from .misc.acl.interface import BUIacl - self.acl = BUIacl - self.acl = self.acl_handler.acl - except Exception as e: - self.logger.critical( - 'Import Exception, module \'{0}\': {1}'.format( - self.acl_engine, - str(e) - ) - ) - raise e - else: - self.acl_handler = False - self.acl = False + if self.acl_engine and self.acl_engine.lower() != 'none': + try: + # Try to load submodules from our current environment + # first + sys.path.insert( + 0, + os.path.dirname(os.path.abspath(__file__)) + ) + mod = __import__( + 'burpui.misc.acl.{0}'.format( + self.acl_engine.lower() + ), + fromlist=['ACLloader'] + ) + ACLloader = mod.ACLloader + self.acl_handler = ACLloader(self) + # for development purpose only + from .misc.acl.interface import BUIacl + self.acl = BUIacl + self.acl = self.acl_handler.acl + except Exception as e: + self.logger.critical( + 'Import Exception, module \'{0}\': {1}'.format( + self.acl_engine, + str(e) + ) + ) + raise e + else: + self.acl_handler = False + self.acl = False - # UI options - self.config['REFRESH'] = self._safe_config_get( - config.getint, - 'refresh', - 'UI', - cast=int - ) - self.config['LIVEREFRESH'] = self._safe_config_get( - config.getint, - 'liverefresh', - 'UI', - cast=int - ) + # UI options + self.config['REFRESH'] = self.conf.safe_get( + 'refresh', + 'integer', + 'UI' + ) + self.config['LIVEREFRESH'] = self.conf.safe_get( + 'liverefresh', + 'integer', + 'UI' + ) - # Production options - self.storage = self._safe_config_get( - config.get, - 'storage', - 'Production' - ) - self.redis = self._safe_config_get( - config.get, - 'redis', - 'Production' - ) + # Production options + self.storage = self.conf.safe_get( + 'storage', + section='Production' + ) + self.redis = self.conf.safe_get( + 'redis', + section='Production' + ) - # Security options - self.scookie = self._safe_config_get( - config.getboolean, - 'scookie', - 'Security', - cast=bool + # Security options + self.scookie = self.conf.safe_get( + 'scookie', + 'boolean', + section='Security' + ) + self.config['SECRET_KEY'] = self.conf.safe_get( + 'appsecret', + section='Security' + ) + self.config['REMEMBER_COOKIE_DURATION'] = \ + self.config['PERMANENT_SESSION_LIFETIME'] = timedelta( + days=self.conf.safe_get( + 'cookietime', + 'integer', + section='Security' ) - self.config['SECRET_KEY'] = self._safe_config_get( - config.get, - 'appsecret', - 'Security' - ) - self.config['REMEMBER_COOKIE_DURATION'] = \ - self.config['PERMANENT_SESSION_LIFETIME'] = timedelta( - days=self._safe_config_get( - config.getint, - 'cookietime', - 'Security', - cast=int - ) - ) - - except ConfigParser.NoOptionError as e: - self.logger.error(str(e)) + ) self.config['STANDALONE'] = self.standalone @@ -265,7 +272,7 @@ class BUIServer(Flask): sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) mod = __import__(module, fromlist=['Burp']) Client = mod.Burp - self.cli = Client(self, conf=conf) + self.cli = Client(self, conf=self.conf) except Exception as e: traceback.print_exc() self.logger.critical( @@ -278,39 +285,6 @@ class BUIServer(Flask): self.init = True - def _safe_config_get(self, callback, key, sect='Global', cast=None): - """:func:`burpui.server.BUIServer._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: - try: - return cast(self.defaults[key]) - except ValueError: - return None - return self.defaults[key] - return None - def manual_run(self): """The :func:`burpui.server.BUIServer.manual_run` functions is used to actually launch the ``Burp-UI`` server. diff --git a/burpui/utils.py b/burpui/utils.py index 48d80c5b..4a057d96 100644 --- a/burpui/utils.py +++ b/burpui/utils.py @@ -8,18 +8,26 @@ """ import os +import re import math import string import sys +import codecs +import json +import shutil import zipfile import tarfile import logging +import configobj +import validate from inspect import currentframe, getouterframes from ._compat import PY3 +from . import __version__, __release__ if PY3: long = int # pragma: no cover + basestring = str # pragma: no cover class human_readable(long): @@ -273,3 +281,205 @@ class ReverseProxied(object): self.app.warning("'prefix' must start with a '/'!") return self.wsgi_app(environ, start_response) + + +class BUIConfig(object): + """Custom config parser""" + logger = logging.getLogger('burp-ui') + + def __init__(self, config, explain=False, defaults=None): + """Wrapper around the ConfigObj class + + :param config: Configuration to parse + :type config: str, list or File + + :param explain: Whether to explain the parsing errors or not + :type explain: bool + + :param defaults: Default options + :type defaults: dict + """ + self.conf = {} + self.section = None + self.defaults = defaults + self.validator = validate.Validator() + try: + self.conf = configobj.ConfigObj(config, encoding='utf-8') + except configobj.ConfigObjError as exp: + # We were unable to parse the config, maybe we need to + # convert/update it + self.logger.warning( + 'Unable to parse the configuration... Trying to convert it' + ) + # if conversion is successful, give it another try + if self._convert(config): + # This time, if it fails, the exception will be forwarded + try: + self.conf = configobj.ConfigObj(config) + except configobj.ConfigObjError as exp2: + if explain: + self._explain(exp2) + else: + raise exp2 + else: + if explain: + self._explain(exp) + else: + raise exp + + @property + def options(self): + """ConfigObj object""" + return self.conf + + @staticmethod + def string_lower_list(value): + if not value: + raise validate.VdtMissingValue('Option not found') + if not isinstance(value, list): + return [str(value).lower()] + return [str(x).lower() for x in value] + + def update_defaults(self, new_defaults): + """Add new defaults""" + self.defaults.update(new_defaults) + + def default_section(self, section): + """Set the default section""" + self.section = section + + def _convert(self, config): + """Convert an old config to a new one""" + sav = '{}.back'.format(config) + current_section = None + + if os.path.exists(sav): + self.logger.error( + 'Looks like the configuration file has already been converted' + ) + return False + + try: + shutil.copy(config, sav) + except IOError as exp: + self.logger.error(str(exp)) + return False + + try: + with codecs.open(sav, 'r', 'utf-8') as ori: + with codecs.open(config, 'w', 'utf-8') as new: + # We add some headers + new.write('# Auto-generated file from a previous version\n') + new.write('# @version@ - {}\n'.format(__version__)) + new.write('# @release@ - {}\n'.format(__release__)) + for line in ori.readlines(): + line = line.rstrip('\n') + search = re.search(r'^\s*\[([^\]]+)\]\s*', line) + if search: + current_section = search.group(1) + # if we find old style config lines, we convert them + elif re.match(r'^\s*\S+\s*:\s*.+$', line) and \ + re.match(r'^\s*[^\[]', line): + key, val = re.split(r'\s*:\s*', line, 1) + # We support *objects* but we need to serialize them + try: + jsn = json.loads(val) + # special case, we re-format the admin value + if current_section == 'BASIC:ACL' and \ + key == 'admin' and \ + isinstance(jsn, list): + val = ','.join(jsn) + elif isinstance(jsn, list) or \ + isinstance(jsn, dict): + val = "'{}'".format(json.dumps(jsn)) + except ValueError: + pass + line = '{} = {}'.format(key, val) + + new.write('{}\n'.format(line)) + + except IOError as exp: + self.logger.error(str(exp)) + return False + return True + + @staticmethod + def _explain(exception): + """Explain parsing errors + + :param exception: Exception object + :type exception: :class:`configobj.ConfigObjError` + """ + message = u'\n' + for error in exception.errors: + message += error.message + '\n' + + raise configobj.ConfigObjError(message.rstrip('\n')) + + def safe_get( + self, + key, + cast='pass', + section=None, + defaults=None): + """Safely return the asked option + + :param key: Key name + :type key: str + + :param cast: How to cast the option + :type cast: str + + :param section: Section name + :type section: str + + :param defaults: Default options + :type defaults: dict + + :returns: The value of the asked option + """ + # The configobj validator is sooo broken. We need to workaround it... + defaults = defaults or self.defaults + section = section or self.section + if section not in self.conf: + self.logger.warning("No '{}' section found".format(section)) + if defaults: + return defaults.get(section, {}).get(key) + return None + + val = self.conf.get(section).get(key) + default = None + if defaults and section in defaults and \ + key in defaults.get(section): + default = defaults.get(section, {}).get(key) + try: + caster = self.validator.functions.get(cast) + if not caster: + try: + caster = getattr(self, cast) + except AttributeError: + self.logger.error( + "'{}': no such validator".format(cast) + ) + return val + ret = caster(val) + self.logger.debug( + '[{}]:{} - found: {}, default: {}'.format( + section, + key, + val, + default + ) + ) + except validate.ValidateError as exp: + self.logger.warning( + '[{}]:{} - found: {}, default: {}\n{}'.format( + section, + key, + val, + default, + str(exp) + ) + ) + ret = default + return ret diff --git a/docs/requirements.txt b/docs/requirements.txt index 4ad83617..ba5a0fa1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -11,3 +11,4 @@ tzlocal==1.2 sphinxcontrib-httpdomain==1.3.0 six==1.10.0 pyOpenSSL==16.0.0 +configobj==5.0.6 diff --git a/requirements.txt b/requirements.txt index 6017b43a..54b2173c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ arrow==0.7.0 tzlocal==1.2 six==1.10.0 pyOpenSSL==16.0.0 +configobj==5.0.6