# -*- coding: utf8 -*- """ .. module:: burpui.server :platform: Unix :synopsis: Burp-UI server module. .. moduleauthor:: Ziirish """ import os import re import sys import logging import traceback from .misc.auth.handler import UserAuthHandler from .misc.acl.handler import ACLloader from .config import config from .plugins import PluginManager from datetime import timedelta from flask import Flask from six import iteritems G_PORT = 5000 G_BIND = u'::' G_REFRESH = 180 G_LIVEREFRESH = 5 G_IGNORE_LABELS = [] G_FORMAT_LABELS = [] G_SSL = False G_SINGLE = True G_SSLCERT = u'' G_SSLKEY = u'' G_VERSION = 2 G_AUTH = [u'basic'] G_ACL = u'none' G_STORAGE = u'' G_CACHE = u'' G_SESSION = u'' G_REDIS = u'' G_LIMITER = False G_RATIO = u'60/minute' G_CELERY = False G_SCOOKIE = True G_DEMO = False G_DSN = u'' G_APPSECRET = u'random' G_COOKIETIME = 14 G_SESSIONTIME = 5 G_DATABASE = u'' G_PREFIX = u'' G_PLUGINS = [] G_NO_SERVER_RESTORE = False G_WS_ENABLED = True G_WS_EMBEDDED = False G_WS_BROKER = u'redis' G_WS_URL = u'' G_WS_DEBUG = False class BUIServer(Flask): """ The :class:`burpui.server.BUIServer` class provides the ``Burp-UI`` server. """ gunicorn = False defaults = { 'Global': { 'port': G_PORT, 'bind': G_BIND, 'ssl': G_SSL, 'standalone': G_SINGLE, 'single': G_SINGLE, 'sslcert': G_SSLCERT, 'sslkey': G_SSLKEY, 'version': G_VERSION, 'auth': G_AUTH, 'acl': G_ACL, 'prefix': G_PREFIX, 'plugins': G_PLUGINS, 'demo': G_DEMO, 'dsn': G_DSN, }, 'UI': { 'refresh': G_REFRESH, 'liverefresh': G_LIVEREFRESH, 'ignore_labels': G_IGNORE_LABELS, 'format_labels': G_FORMAT_LABELS, }, 'Security': { 'scookie': G_SCOOKIE, 'appsecret': G_APPSECRET, 'cookietime': G_COOKIETIME, 'sessiontime': G_SESSIONTIME, }, 'Production': { 'storage': G_STORAGE, 'session': G_SESSION, 'cache': G_CACHE, 'redis': G_REDIS, 'celery': G_CELERY, 'database': G_DATABASE, 'limiter': G_LIMITER, 'ratio': G_RATIO, }, 'WebSocket': { 'enabled': G_WS_ENABLED, 'embedded': G_WS_EMBEDDED, 'broker': G_WS_BROKER, 'url': G_WS_URL, 'debug': G_WS_DEBUG, }, 'Experimental': { 'noserverrestore': G_NO_SERVER_RESTORE, } } def __init__(self): """The :class:`burpui.server.BUIServer` class provides the ``Burp-UI`` server. :param app: The Flask application to launch """ super(BUIServer, self).__init__('burpui') self.init = False # We cannot override the Flask's logger so we use our own self._logger = logging.getLogger('burp-ui') self._logger.disabled = True self.conf = config # switch the flask config with our magic config object self.conf.update(self.config) self.config = self.conf def enable_logger(self, enable=True): """Enable or disable the logger""" self._logger.disabled = not enable @property def logger(self): """ :rtype: :class:`logging.Logger` """ return self._logger def setup(self, conf=None, unittest=False, cli=False): """The :func:`burpui.server.BUIServer.setup` functions is used to setup the whole server by parsing the configuration file and loading the different backends. :param conf: Path to a configuration file :type conf: str :param unittest: Whether we are unittesting or not (used to avoid burp2 strict requirements checks) :type unittest: bool :param cli: Whether we are in cli mode or not :type cli: bool """ self.sslcontext = None if not conf: conf = self.config['CFG'] if not conf: raise IOError('No configuration file found') # Raise exception if errors are encountered during parsing self.conf.parse(conf, True, self.defaults) self.conf.default_section('Global') self.port = self.config['BUI_PORT'] = self.conf.safe_get( 'port', 'integer' ) self.demo = self.config['BUI_DEMO'] = self.conf.safe_get( 'demo', 'boolean' ) self.config['BUI_DSN'] = self.conf.safe_get('dsn') self.bind = self.config['BUI_BIND'] = self.conf.safe_get('bind') version = self.conf.safe_get('version', 'integer') if unittest and version != 1: version = 1 self.vers = self.config['BUI_VERS'] = version self.ssl = self.config['BUI_SSL'] = self.conf.safe_get( 'ssl', 'boolean' ) # option standalone has been renamed for less confusion key = 'standalone' if 'standalone' in \ self.conf.conf.get(self.conf.section, {}) else 'single' if key == 'standalone': # TODO: remove the compatibility in v0.7.0 self.logger.warning( 'The "standalone" option is DEPRECATED and has been replaced ' 'by the "single" option. Please update your conf before we ' 'remove the compatibility in v0.7.0' ) self.standalone = self.config['STANDALONE'] = self.conf.safe_get( key, 'boolean' ) self.sslcert = self.config['BUI_SSLCERT'] = self.conf.safe_get( 'sslcert' ) self.sslkey = self.config['BUI_SSLKEY'] = self.conf.safe_get( 'sslkey' ) self.prefix = self.config['BUI_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.config['BUI_PREFIX'] = '' self.plugins = self.config['BUI_PLUGINS'] = self.conf.safe_get( 'plugins', 'string_lower_list' ) if len(self.plugins) == 1 and self.plugins[0] == 'none': self.plugins = self.config['BUI_PLUGINS'] = [] self.auth = self.config['BUI_AUTH'] = self.conf.safe_get( 'auth', 'string_lower_list' ) # UI options self.config['REFRESH'] = self.conf.safe_get( 'refresh', 'integer', 'UI' ) self.config['LIVEREFRESH'] = self.conf.safe_get( 'liverefresh', 'integer', 'UI' ) self.ignore_labels = self.conf.safe_get( 'ignore_labels', 'force_list', 'UI' ) format_labels = self.conf.safe_get( 'format_labels', 'force_list', 'UI' ) self.format_labels = [] for format_label in format_labels: search = re.search(r'^s(?P.)(?P.*?)(?P=separator)(?P.*?)(?P=separator)$', format_label) if search: self.format_labels.append((search.group('regex'), search.group('replace'))) # Production options self.storage = self.config['BUI_STORAGE'] = self.conf.safe_get( 'storage', section='Production' ) self.cache_db = self.config['BUI_CACHE_DB'] = self.conf.safe_get( 'cache', section='Production' ) self.session_db = self.config['BUI_SESSION_DB'] = self.conf.safe_get( 'session', section='Production' ) self.redis = self.config['BUI_REDIS'] = self.conf.safe_get( 'redis', section='Production' ) self.limiter = self.config['BUI_LIMITER'] = self.conf.safe_get( 'limiter', 'boolean_or_string', section='Production' ) if isinstance(self.limiter, bool) and not self.limiter: self.limiter = self.config['BUI_LIMITER'] = 'none' self.ratio = self.config['BUI_RATIO'] = self.conf.safe_get( 'ratio', section='Production' ) self.use_celery = self.config['BUI_CELERY'] = self.conf.safe_get( 'celery', 'boolean_or_string', section='Production' ) self.database = self.config['SQLALCHEMY_DATABASE_URI'] = \ self.conf.safe_get( 'database', 'boolean_or_string', section='Production' ) self.config['WITH_LIMIT'] = False if isinstance(self.database, bool): self.config['WITH_SQL'] = self.database else: self.config['WITH_SQL'] = self.database and \ self.database.lower() != 'none' if isinstance(self.use_celery, bool): self.config['WITH_CELERY'] = self.use_celery else: self.config['WITH_CELERY'] = self.use_celery and \ self.use_celery.lower() != 'none' # WebSocket options self.ws_enabled = self.config['WS_ENABLED'] = self.conf.safe_get( 'enabled', 'boolean', section='WebSocket' ) self.websocket = self.config['WITH_WS'] = self.conf.safe_get( 'embedded', 'boolean', section='WebSocket' ) self.ws_broker = self.config['BUI_WS_BROKER'] = self.conf.safe_get( 'broker', 'boolean_or_string', section='WebSocket' ) self.config['WS_DEBUG'] = self.conf.safe_get( 'debug', 'boolean', section='WebSocket' ) self.config['WS_URL'] = self.conf.safe_get( 'url', section='WebSocket' ) if self.config.get('WS_URL', '').lower() == 'none' or self.websocket: self.config['WS_URL'] = None # Experimental options self.noserverrestore = self.conf.safe_get( 'noserverrestore', 'boolean', section='Experimental' ) # Security options self.scookie = self.config['BUI_SCOOKIE'] = self.conf.safe_get( 'scookie', 'boolean', section='Security' ) self.config['SECRET_KEY'] = self.conf.safe_get( 'appsecret', section='Security' ) days = self.conf.safe_get('cookietime', 'integer', section='Security') \ or G_COOKIETIME self.config['REMEMBER_COOKIE_DURATION'] = \ self.config['PERMANENT_SESSION_LIFETIME'] = timedelta( days=days ) self.config['REMEMBER_COOKIE_NAME'] = 'remember_token' days = self.conf.safe_get( 'sessiontime', 'integer', section='Security' ) or G_SESSIONTIME self.config['SESSION_INACTIVE'] = timedelta(days=days) self.logger.info('burp version: {}'.format(self.vers)) self.logger.info('listen port: {}'.format(self.port)) self.logger.info('bind addr: {}'.format(self.bind)) self.logger.info('use ssl: {}'.format(self.ssl)) self.logger.info('standalone: {}'.format(self.standalone)) self.logger.info('sslcert: {}'.format(self.sslcert)) self.logger.info('sslkey: {}'.format(self.sslkey)) self.logger.info('prefix: {}'.format(self.prefix)) self.logger.info('secure cookie: {}'.format(self.scookie)) self.logger.info( 'cookietime: {}'.format(self.config['REMEMBER_COOKIE_DURATION']) ) self.logger.info( 'session inactive: {}'.format(self.config['SESSION_INACTIVE']) ) self.logger.info('refresh: {}'.format(self.config['REFRESH'])) self.logger.info('liverefresh: {}'.format(self.config['LIVEREFRESH'])) self.logger.info('auth: {}'.format(self.auth)) self.logger.info('celery: {}'.format(self.use_celery)) self.logger.info('redis: {}'.format(self.redis)) self.logger.info('limiter: {}'.format(self.limiter)) self.logger.info('database: {}'.format(self.database)) self.logger.info('with SQL: {}'.format(self.config['WITH_SQL'])) self.logger.info('with Celery: {}'.format(self.config['WITH_CELERY'])) self.logger.info('with WebSocket: {}'.format(self.config['WITH_WS'])) self.logger.info('demo: {}'.format(self.config['BUI_DEMO'])) self.init = True if not cli: self.load_modules() def load_modules(self, strict=False): """Load the extensions""" self.plugin_manager = PluginManager(self, self.plugins) if self.plugins: self.plugin_manager.load_all() if self.auth and 'none' not in self.auth: try: self.uhandler = UserAuthHandler(self) for back, err in iteritems(self.uhandler.errors): self.logger.critical( 'Unable to load \'{}\' authentication backend:\n{}' .format(back, err) ) except ImportError as e: self.logger.critical( 'Import Exception, module \'{0}\': {1}'.format( self.auth, str(e) ) ) raise e self.acl_engine = self.config['BUI_ACL'] = self.conf.safe_get( 'acl', 'string_lower_list' ) self.config['LOGIN_DISABLED'] = False else: self.config['LOGIN_DISABLED'] = True # No login => no ACL self.acl_engine = self.config['BUI_ACL'] = ['none'] self.auth = self.config['BUI_AUTH'] = 'none' if self.acl_engine and 'none' not in self.acl_engine: try: self.acl_handler = ACLloader(self) 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.logger.info('acl: {}'.format(self.acl_engine)) if self.standalone: module = 'burpui.misc.backend.burp{0}'.format(self.vers) else: module = 'burpui.misc.backend.multi' # This is used for development purpose only from .misc.backend.burp1 import Burp as BurpGeneric self.client = BurpGeneric(dummy=True) self.strict = strict try: # Try to load submodules from our current environment # first sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) mod = __import__(module, fromlist=['Burp']) Client = mod.Burp self.client = Client(self, conf=self.conf) except Exception as e: msg = 'Failed loading backend for Burp version {0}: {1}'.format( self.vers, str(e) ) if strict: self.logger.critical(traceback.format_exc()) self.logger.critical(msg) sys.exit(2) else: raise Exception(msg) @property def acl(self): """ACL module :returns: :class:`burpui.misc.acl.interface.BUIacl` """ if self.acl_engine and 'none' not in self.acl_engine: # refresh acl to detect config changes from .misc.acl.interface import BUIacl # noqa acl = self.acl_handler.acl # type: BUIacl return acl return None def get_send_file_max_age(self, name): """Provides default cache_timeout for the send_file() functions.""" if name: lname = name.lower() extensions = ['js', 'css', 'woff'] for ext in extensions: if lname.endswith('.{}'.format(ext)): return 3600 * 24 * 30 # 30 days return Flask.get_send_file_max_age(self, name) def manual_run(self): """The :func:`burpui.server.BUIServer.manual_run` functions is used to actually launch the ``Burp-UI`` server. .. deprecated:: 0.4.0 You should now run the Flask's run command. """ if not self.init: self.setup() if self.ssl: self.sslcontext = (self.sslcert, self.sslkey) if self.sslcontext: self.config['SSL'] = True self.run( host=self.bind, port=self.port, debug=self.config['DEBUG'], ssl_context=self.sslcontext ) else: self.run(host=self.bind, port=self.port, debug=self.config['DEBUG'])