diff --git a/burpui/api/__init__.py b/burpui/api/__init__.py index cf662b35..77cdee1e 100644 --- a/burpui/api/__init__.py +++ b/burpui/api/__init__.py @@ -71,6 +71,9 @@ class Api(ApiPlus): CELERY_REQUIRED = ['async'] def load_all(self): + if config['WITH_LIMIT']: + from ..ext.limit import limiter + self.decorators.append(limiter.limit(config['BUI_RATIO'])) """hack to automatically import api modules""" if not self.loaded: sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) diff --git a/burpui/app.py b/burpui/app.py index a0e384da..2fffabd9 100644 --- a/burpui/app.py +++ b/burpui/app.py @@ -337,7 +337,6 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs): list=list, mypad=mypad, version_id='{}-{}'.format(__version__, __release__), - config=app.config ) # manage application secret key @@ -358,7 +357,7 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs): app.wsgi_app = ProxyFix(app.wsgi_app) - if app.storage and app.storage.lower() != 'default': + if app.storage and app.storage.lower() == 'redis': try: # Session setup if not app.session_db or \ @@ -378,10 +377,11 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs): db = int(db) except ValueError: db = 0 - logger.debug('Using redis://guest:****@{}:{}/{}'.format( - host, - port, - db) + logger.debug( + 'SESSION: Using redis://guest:****@{}:{}/{}'.format( + host, + port, + db) ) red = Redis(host=host, port=port, db=db, password=pwd) app.config['SESSION_TYPE'] = 'redis' @@ -390,6 +390,9 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs): app.config['SESSION_PERMANENT'] = False sess.init_app(app) session_manager.backend = red + except Exception as exp: + logger.warning('Unable to initialize session: {}'.format(str(exp))) + try: # Cache setup if not app.cache_db or \ app.cache_db.lower() not in ['none']: @@ -406,7 +409,7 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs): db = int(db) except ValueError: db = 1 - logger.debug('Using redis://guest:****@{}:{}/{}'.format( + logger.debug('CACHE: Using redis://guest:****@{}:{}/{}'.format( host, port, db) @@ -426,9 +429,48 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs): cache.clear() else: cache.init_app(app) - except Exception as e: - logger.warning('Unable to initialize redis: {}'.format(str(e))) + except Exception as exp: + logger.warning('Unable to initialize cache: {}'.format(str(exp))) cache.init_app(app) + try: + # Limiter setup + if not app.limiter or app.limiter.lower() not in ['none']: + from .ext.limit import limiter + app.config['RATELIMIT_HEADERS_ENABLED'] = True + if app.limiter and app.limiter not in ['default', 'redis']: + app.config['RATELIMIT_STORAGE_URL'] = app.limiter + else: + db = 3 + host, port, pwd = get_redis_server(app) + if pwd: + conn = 'redis://guest:{}@{}:{}/{}'.format( + pwd, + host, + port, + db + ) + else: + conn = 'redis://{}:{}/{}'.format(host, port, db) + app.config['RATELIMIT_STORAGE_URL'] = conn + + (_, _, pwd, host, port, db) = parse_db_setting( + app.config['RATELIMIT_STORAGE_URL'] + ) + + logger.debug( + 'LIMITER: Using redis://guest:****@{}:{}/{}'.format( + host, + port, + db + ) + ) + limiter.init_app(app) + app.config['WITH_LIMIT'] = True + except ImportError: + logger.warning('Unable to load limiter. Did you run \'pip install ' + 'flask-limiter\'?') + except Exception as exp: + logger.warning('Unable to initialize limiter: {}'.format(str(exp))) else: cache.init_app(app) diff --git a/burpui/ext/limit.py b/burpui/ext/limit.py new file mode 100644 index 00000000..0ea36251 --- /dev/null +++ b/burpui/ext/limit.py @@ -0,0 +1,13 @@ +# -*- coding: utf8 -*- +""" +.. module:: burpui.ext.limit + :platform: Unix + :synopsis: Burp-UI external Limit module. + +.. moduleauthor:: Ziirish + +""" +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address + +limiter = Limiter(key_func=get_remote_address) diff --git a/burpui/forms.py b/burpui/forms.py index d79b4e8c..cbd31fa9 100644 --- a/burpui/forms.py +++ b/burpui/forms.py @@ -15,7 +15,7 @@ from wtforms import TextField, PasswordField, BooleanField, SelectField, validat class LoginForm(FlaskForm): - username = TextField(__('Username'), [validators.Length(min=2, max=25)]) + username = TextField(__('Username'), [validators.Required()]) password = PasswordField(__('Password'), [validators.Required()]) language = SelectField(__('Language'), choices=LANGUAGES.items(), default=get_locale) remember = BooleanField(__('Remember me'), [validators.Optional()]) diff --git a/burpui/server.py b/burpui/server.py index 081c2492..5c68ee34 100644 --- a/burpui/server.py +++ b/burpui/server.py @@ -34,6 +34,8 @@ G_STORAGE = u'' G_CACHE = u'' G_SESSION = u'' G_REDIS = u'' +G_LIMITER = False +G_RATIO = u'20/minute' G_CELERY = False G_SCOOKIE = True G_DEMO = False @@ -82,6 +84,8 @@ class BUIServer(Flask): 'redis': G_REDIS, 'celery': G_CELERY, 'database': G_DATABASE, + 'limiter': G_LIMITER, + 'ratio': G_RATIO, }, 'Experimental': { 'noserverrestore': G_NO_SERVER_RESTORE, @@ -94,19 +98,23 @@ class BUIServer(Flask): :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.builogger = logging.getLogger('burp-ui') - self.builogger.disabled = True - super(BUIServer, self).__init__('burpui') + 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.builogger.disabled = not enable + self._logger.disabled = not enable @property def logger(self): - return self.builogger + return self._logger def setup(self, conf=None, unittest=False, cli=False): """The :func:`burpui.server.BUIServer.setup` functions is used to setup @@ -131,14 +139,9 @@ class BUIServer(Flask): raise IOError('No configuration file found') # Raise exception if errors are encountered during parsing - self.conf = config self.conf.parse(conf, True, self.defaults) self.conf.default_section('Global') - # switch the flask config with our magic config object - self.conf.update(self.config) - self.config = self.conf - self.port = self.config['BUI_PORT'] = self.conf.safe_get( 'port', 'integer' @@ -208,6 +211,17 @@ class BUIServer(Flask): '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', @@ -219,6 +233,7 @@ class BUIServer(Flask): 'boolean_or_string', section='Production' ) + self.config['WITH_LIMIT'] = False if isinstance(self.database, bool): self.config['WITH_SQL'] = self.database else: diff --git a/docs/faq.rst b/docs/faq.rst index 7e976ce6..ac3f4fbd 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -105,12 +105,41 @@ Are there any known issues? There is a `known issue `__ section in this documentation. +Why using redis? +---------------- + +Redis may be used for several things: + +- store the sessions server side (by default sessions are stored client side in + a secure cookie) +- cache some data +- monitor API usage for the rate limiter + +All of these features are totally optional. +Redis is also used by celery to interact between Burp-UI and the asynchronous +worker. + +Why using SQL? +-------------- + +The SQL database is currently used to keep a track of several meta-data. +Again, it is totally optional to use it. + Burp-UI does not work anymore since I upgraded it, what can I do? ----------------------------------------------------------------- Make sure you read the `upgrading `_ page in case some breaking changes occurred. +I am getting errors while restoring large files (>3GB), what should I do? +------------------------------------------------------------------------- + +The default *zip* module does not support large files by default. You can either +enable large file support by setting ``zip64 = true`` in the ``[Experimental]`` +section. +Alternatively, you can choose an other compression module by selecting an other +extension while proceeding the restoration. + How can I contribute? --------------------- diff --git a/docs/requirements.rst b/docs/requirements.rst index 769fe5d2..143bdbbd 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -80,6 +80,16 @@ If you need persistent data, you will need additional dependencies as well: pip install burp-ui-sql +Limiter +------- + +If you want to rate-limit the API, you will need additional dependencies too: + +:: + + pip install flask-limiter + + Burp1 ----- diff --git a/docs/usage.rst b/docs/usage.rst index fd443c3f..afbae8cf 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -140,6 +140,17 @@ follow: # http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls # example: sqlite:////var/lib/burpui/store.db database = none + # whether to rate limit the API or not + # may also be a redis url like: redis://localhost:6379/0 + # if set to "true" or "redis" or "default", the url defaults to: + # redis://:/3 + # where is the host part, and is the port part of + # the above "redis" setting + # Note: the limiter only applies to the API routes + limiter = false + # limiter ratio + # see https://flask-limiter.readthedocs.io/en/stable/#ratelimit-string + ratio = 20/minute Experimental diff --git a/share/burpui/etc/burpui.sample.cfg b/share/burpui/etc/burpui.sample.cfg index fe181527..617bdd16 100644 --- a/share/burpui/etc/burpui.sample.cfg +++ b/share/burpui/etc/burpui.sample.cfg @@ -63,6 +63,17 @@ celery = false # http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls # example: sqlite:////var/lib/burpui/store.db database = none +# whether to rate limit the API or not +# may also be a redis url like: redis://localhost:6379/0 +# if set to "true" or "redis" or "default", the url defaults to: +# redis://:/3 +# where is the host part, and is the port part of +# the above "redis" setting +# Note: the limiter only applies to the API routes +limiter = false +# limiter ratio +# see https://flask-limiter.readthedocs.io/en/stable/#ratelimit-string +ratio = 20/minute [Security] ## This section contains some security options. Make sure you understand the