burp-ui/burpui/server.py
2018-02-20 11:28:57 +01:00

521 lines
17 KiB
Python

# -*- coding: utf8 -*-
"""
.. module:: burpui.server
:platform: Unix
:synopsis: Burp-UI server module.
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
"""
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<separator>.)(?P<regex>.*?)(?P=separator)(?P<replace>.*?)(?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'])