mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-21 06:45:24 -06:00
add: limiter extension to allow rate-limit the API
This commit is contained in:
parent
3692887d80
commit
12100ff884
9 changed files with 154 additions and 20 deletions
|
|
@ -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__)))
|
||||||
|
|
|
||||||
|
|
@ -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
13
burpui/ext/limit.py
Normal 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)
|
||||||
|
|
@ -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()])
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
29
docs/faq.rst
29
docs/faq.rst
|
|
@ -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?
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue