mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-15 06:05:58 -06:00
521 lines
17 KiB
Python
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'])
|