add: limiter extension to allow rate-limit the API

This commit is contained in:
ziirish 2017-01-29 18:00:22 +01:00
parent 3692887d80
commit 12100ff884
9 changed files with 154 additions and 20 deletions

View file

@ -71,6 +71,9 @@ class Api(ApiPlus):
CELERY_REQUIRED = ['async'] CELERY_REQUIRED = ['async']
def load_all(self): 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""" """hack to automatically import api modules"""
if not self.loaded: if not self.loaded:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

View file

@ -337,7 +337,6 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs):
list=list, list=list,
mypad=mypad, mypad=mypad,
version_id='{}-{}'.format(__version__, __release__), version_id='{}-{}'.format(__version__, __release__),
config=app.config
) )
# manage application secret key # 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) app.wsgi_app = ProxyFix(app.wsgi_app)
if app.storage and app.storage.lower() != 'default': if app.storage and app.storage.lower() == 'redis':
try: try:
# Session setup # Session setup
if not app.session_db or \ if not app.session_db or \
@ -378,10 +377,11 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs):
db = int(db) db = int(db)
except ValueError: except ValueError:
db = 0 db = 0
logger.debug('Using redis://guest:****@{}:{}/{}'.format( logger.debug(
host, 'SESSION: Using redis://guest:****@{}:{}/{}'.format(
port, host,
db) port,
db)
) )
red = Redis(host=host, port=port, db=db, password=pwd) red = Redis(host=host, port=port, db=db, password=pwd)
app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_TYPE'] = 'redis'
@ -390,6 +390,9 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs):
app.config['SESSION_PERMANENT'] = False app.config['SESSION_PERMANENT'] = False
sess.init_app(app) sess.init_app(app)
session_manager.backend = red session_manager.backend = red
except Exception as exp:
logger.warning('Unable to initialize session: {}'.format(str(exp)))
try:
# Cache setup # Cache setup
if not app.cache_db or \ if not app.cache_db or \
app.cache_db.lower() not in ['none']: app.cache_db.lower() not in ['none']:
@ -406,7 +409,7 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs):
db = int(db) db = int(db)
except ValueError: except ValueError:
db = 1 db = 1
logger.debug('Using redis://guest:****@{}:{}/{}'.format( logger.debug('CACHE: Using redis://guest:****@{}:{}/{}'.format(
host, host,
port, port,
db) db)
@ -426,9 +429,48 @@ def create_app(conf=None, verbose=0, logfile=None, **kwargs):
cache.clear() cache.clear()
else: else:
cache.init_app(app) cache.init_app(app)
except Exception as e: except Exception as exp:
logger.warning('Unable to initialize redis: {}'.format(str(e))) logger.warning('Unable to initialize cache: {}'.format(str(exp)))
cache.init_app(app) 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: else:
cache.init_app(app) cache.init_app(app)

13
burpui/ext/limit.py Normal file
View file

@ -0,0 +1,13 @@
# -*- coding: utf8 -*-
"""
.. module:: burpui.ext.limit
:platform: Unix
:synopsis: Burp-UI external Limit module.
.. moduleauthor:: Ziirish <hi+burpui@ziirish.me>
"""
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)

View file

@ -15,7 +15,7 @@ from wtforms import TextField, PasswordField, BooleanField, SelectField, validat
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
username = TextField(__('Username'), [validators.Length(min=2, max=25)]) username = TextField(__('Username'), [validators.Required()])
password = PasswordField(__('Password'), [validators.Required()]) password = PasswordField(__('Password'), [validators.Required()])
language = SelectField(__('Language'), choices=LANGUAGES.items(), default=get_locale) language = SelectField(__('Language'), choices=LANGUAGES.items(), default=get_locale)
remember = BooleanField(__('Remember me'), [validators.Optional()]) remember = BooleanField(__('Remember me'), [validators.Optional()])

View file

@ -34,6 +34,8 @@ G_STORAGE = u''
G_CACHE = u'' G_CACHE = u''
G_SESSION = u'' G_SESSION = u''
G_REDIS = u'' G_REDIS = u''
G_LIMITER = False
G_RATIO = u'20/minute'
G_CELERY = False G_CELERY = False
G_SCOOKIE = True G_SCOOKIE = True
G_DEMO = False G_DEMO = False
@ -82,6 +84,8 @@ class BUIServer(Flask):
'redis': G_REDIS, 'redis': G_REDIS,
'celery': G_CELERY, 'celery': G_CELERY,
'database': G_DATABASE, 'database': G_DATABASE,
'limiter': G_LIMITER,
'ratio': G_RATIO,
}, },
'Experimental': { 'Experimental': {
'noserverrestore': G_NO_SERVER_RESTORE, 'noserverrestore': G_NO_SERVER_RESTORE,
@ -94,19 +98,23 @@ class BUIServer(Flask):
:param app: The Flask application to launch :param app: The Flask application to launch
""" """
super(BUIServer, self).__init__('burpui')
self.init = False self.init = False
# We cannot override the Flask's logger so we use our own # We cannot override the Flask's logger so we use our own
self.builogger = logging.getLogger('burp-ui') self._logger = logging.getLogger('burp-ui')
self.builogger.disabled = True self._logger.disabled = True
super(BUIServer, self).__init__('burpui') 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): def enable_logger(self, enable=True):
"""Enable or disable the logger""" """Enable or disable the logger"""
self.builogger.disabled = not enable self._logger.disabled = not enable
@property @property
def logger(self): def logger(self):
return self.builogger return self._logger
def setup(self, conf=None, unittest=False, cli=False): def setup(self, conf=None, unittest=False, cli=False):
"""The :func:`burpui.server.BUIServer.setup` functions is used to setup """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 IOError('No configuration file found')
# Raise exception if errors are encountered during parsing # Raise exception if errors are encountered during parsing
self.conf = config
self.conf.parse(conf, True, self.defaults) self.conf.parse(conf, True, self.defaults)
self.conf.default_section('Global') 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( self.port = self.config['BUI_PORT'] = self.conf.safe_get(
'port', 'port',
'integer' 'integer'
@ -208,6 +211,17 @@ class BUIServer(Flask):
'redis', 'redis',
section='Production' 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( self.use_celery = self.config['BUI_CELERY'] = self.conf.safe_get(
'celery', 'celery',
'boolean_or_string', 'boolean_or_string',
@ -219,6 +233,7 @@ class BUIServer(Flask):
'boolean_or_string', 'boolean_or_string',
section='Production' section='Production'
) )
self.config['WITH_LIMIT'] = False
if isinstance(self.database, bool): if isinstance(self.database, bool):
self.config['WITH_SQL'] = self.database self.config['WITH_SQL'] = self.database
else: else:

View file

@ -105,12 +105,41 @@ Are there any known issues?
There is a `known issue <introduction.html#known-issues>`__ section in this There is a `known issue <introduction.html#known-issues>`__ section in this
documentation. 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? Burp-UI does not work anymore since I upgraded it, what can I do?
----------------------------------------------------------------- -----------------------------------------------------------------
Make sure you read the `upgrading <upgrading.html>`_ page in case some breaking Make sure you read the `upgrading <upgrading.html>`_ page in case some breaking
changes occurred. 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? How can I contribute?
--------------------- ---------------------

View file

@ -80,6 +80,16 @@ If you need persistent data, you will need additional dependencies as well:
pip install burp-ui-sql 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 Burp1
----- -----

View file

@ -140,6 +140,17 @@ follow:
# http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls # http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
# example: sqlite:////var/lib/burpui/store.db # example: sqlite:////var/lib/burpui/store.db
database = none 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://<redis_host>:<redis_port>/3
# where <redis_host> is the host part, and <redis_port> 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 Experimental

View file

@ -63,6 +63,17 @@ celery = false
# http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls # http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
# example: sqlite:////var/lib/burpui/store.db # example: sqlite:////var/lib/burpui/store.db
database = none 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://<redis_host>:<redis_port>/3
# where <redis_host> is the host part, and <redis_port> 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] [Security]
## This section contains some security options. Make sure you understand the ## This section contains some security options. Make sure you understand the