Skip to content
Snippets Groups Projects
Commit d4529359 authored by nd's avatar nd
Browse files

add dummy redis support

parent 4bc7ffd0
No related branches found
No related tags found
No related merge requests found
......@@ -15,6 +15,7 @@ Please note that we refer to Debian packages here and **not** pip packages.
- python3-qrcode
- python3-fido2 (version 0.5.0 or 0.9.1, optional)
- python3-prometheus-client (optional, needed for metrics)
- python3-redis (optional, needed for redis support)
- python3-oauthlib
- python3-flask-babel
- python3-argon2
......
......@@ -33,4 +33,5 @@ Recommends:
nginx,
python3-mysqldb,
python3-prometheus-client,
python3-redis,
Description: Web-based user management and single sign-on software
......@@ -6,7 +6,10 @@ from uffd.models.ratelimit import get_addrkey, format_delay, Ratelimit, Ratelimi
from utils import UffdTestCase
class TestRatelimit(UffdTestCase):
class TestRatelimitDB(UffdTestCase):
def setUpConfig(self, config):
config['REDIS_HOST'] = False
def test_limiting(self):
cases = [
(1*60, 3),
......@@ -48,3 +51,18 @@ class TestRatelimit(UffdTestCase):
self.assertIsInstance(format_delay(120), str)
self.assertIsInstance(format_delay(3600), str)
self.assertIsInstance(format_delay(4000), str)
class TestRatelimitRedis(TestRatelimitDB):
def setUpConfig(self, config):
import redis
config['REDIS_HOST'] = 'localhost'
config['REDIS_PORT'] = 6379
config['REDIS_DB'] = 0
self.redis = redis.Redis(
host=config['REDIS_HOST'],
port=config['REDIS_PORT'],
db=config['REDIS_DB'])
return config
def setUpDB(self):
self.redis.flushdb();
......@@ -68,7 +68,7 @@ class UffdTestCase(unittest.TestCase):
'MAIL_SKIP_SEND': True,
'SELF_SIGNUP': True,
}
config = self.setUpConfig(config)
self.app = create_app(config)
self.setUpApp()
self.client = self.app.test_client()
......@@ -92,6 +92,9 @@ class UffdTestCase(unittest.TestCase):
self.setUpDB()
db.session.commit()
def setUpConfig(self, config):
return config
def setUpApp(self):
pass
......
......@@ -68,6 +68,7 @@ def create_app(test_config=None): # pylint: disable=too-many-locals,too-many-sta
app.register_blueprint(csrf_bp)
models.init_app(app)
views.init_app(app)
commands.init_app(app)
......
......@@ -41,6 +41,14 @@ MAIL_PASSWORD='*****'
MAIL_USE_STARTTLS=True
MAIL_FROM_ADDRESS='foo@bar.com'
# Optional Redis Server, used for rate limits if configured.
# Rate Limits fail back to in Database if no redis is configured.
# If you enable this, we depend on python3-redis
#REDIS_HOST='localhost'
#REDIS_PORT=6379
#REDIS_DB=0
# Set to a domain name (e.g. "remailer.example.com") to enable remailer.
# Requires special mail server configuration (see uffd-socketmapd). Can be
# enabled/disabled per-service in the service settings. If enabled, services
......
......@@ -26,3 +26,6 @@ __all__ = [
'User', 'UserEmail', 'Group',
'RatelimitEvent', 'Ratelimit', 'HostRatelimit', 'host_ratelimit', 'format_delay',
]
def init_app(app):
Ratelimit.init_app(app)
......@@ -24,23 +24,36 @@ class RatelimitEvent(db.Model):
return self.expires < datetime.datetime.utcnow()
class Ratelimit:
_redis = False
def __init__(self, name, interval, limit):
self.name = name
self.interval = interval
self.limit = limit
self.base = interval**(1/limit)
@classmethod
def init_app(cls, app):
if not app.config.get('REDIS_HOST'):
cls._redis = False
else:
import redis
cls._redis = redis.Redis(host=app.config['REDIS_HOST'], port=app.config['REDIS_PORT'], db=app.config['REDIS_DB'])
def __redis_get_index(self, key=None):
return 'ratelimit:{}{}'.format(self.name, (':' + key) or '')
def log(self, key=None):
if not self._redis:
db.session.add(RatelimitEvent(name=self.name, key=key, expires=datetime.datetime.utcnow() + datetime.timedelta(seconds=self.interval)))
db.session.commit()
else:
self._redis.incr(self.__redis_get_index(key))
self._redis.expire(self.__redis_get_index(key), ttl=self.intervall, nx=True)
def get_delay(self, key=None):
events = RatelimitEvent.query\
.filter(db.not_(RatelimitEvent.expired))\
.filter_by(name=self.name, key=key)\
.order_by(RatelimitEvent.timestamp)\
.all()
if not events:
def get_delay_backoff(self, events):
if events < 1:
return 0
delay = math.ceil(self.base**len(events))
if delay < 5:
......@@ -49,6 +62,18 @@ class Ratelimit:
remaining = events[0].timestamp + datetime.timedelta(seconds=delay) - datetime.datetime.utcnow()
return max(0, math.ceil(remaining.total_seconds()))
def get_delay(self, key=None):
if not self._redis:
events = RatelimitEvent.query\
.filter(db.not_(RatelimitEvent.expired))\
.filter_by(name=self.name, key=key)\
.order_by(RatelimitEvent.timestamp)\
.all()
else:
events = self._redis.get(self.__redis_get_index(key)) or 0
return self.get_delay_backoff(len(events))
def get_addrkey(addr=None):
if addr is None:
addr = request.remote_addr
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment