Refactors throttle to use Django caching.
This commit is contained in:
parent
7032d907a7
commit
2bd2a649ca
2 changed files with 80 additions and 21 deletions
|
|
@ -52,10 +52,10 @@ _MUDINFO_CHANNEL = None
|
||||||
|
|
||||||
# Create throttles for too many account-creations and login attempts
|
# Create throttles for too many account-creations and login attempts
|
||||||
CREATION_THROTTLE = Throttle(
|
CREATION_THROTTLE = Throttle(
|
||||||
limit=settings.CREATION_THROTTLE_LIMIT, timeout=settings.CREATION_THROTTLE_TIMEOUT
|
name='creation', limit=settings.CREATION_THROTTLE_LIMIT, timeout=settings.CREATION_THROTTLE_TIMEOUT
|
||||||
)
|
)
|
||||||
LOGIN_THROTTLE = Throttle(
|
LOGIN_THROTTLE = Throttle(
|
||||||
limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
|
name='authentication', limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from collections import defaultdict, deque
|
from django.core.cache import caches
|
||||||
|
from collections import deque
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -12,8 +13,8 @@ class Throttle(object):
|
||||||
|
|
||||||
This version of the throttle is usable by both the terminal server as well
|
This version of the throttle is usable by both the terminal server as well
|
||||||
as the web server, imposes limits on memory consumption by using deques
|
as the web server, imposes limits on memory consumption by using deques
|
||||||
with length limits instead of open-ended lists, and removes sparse keys when
|
with length limits instead of open-ended lists, and uses native Django
|
||||||
no recent failures have been recorded.
|
caches for automatic key eviction and persistence configurability.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error_msg = "Too many failed attempts; you must wait a few minutes before trying again."
|
error_msg = "Too many failed attempts; you must wait a few minutes before trying again."
|
||||||
|
|
@ -23,6 +24,7 @@ class Throttle(object):
|
||||||
Allows setting of throttle parameters.
|
Allows setting of throttle parameters.
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
|
name (str): Name of this throttle.
|
||||||
limit (int): Max number of failures before imposing limiter
|
limit (int): Max number of failures before imposing limiter
|
||||||
timeout (int): number of timeout seconds after
|
timeout (int): number of timeout seconds after
|
||||||
max number of tries has been reached.
|
max number of tries has been reached.
|
||||||
|
|
@ -30,10 +32,38 @@ class Throttle(object):
|
||||||
rolling window; this is NOT the same as the limit after which
|
rolling window; this is NOT the same as the limit after which
|
||||||
the throttle is imposed!
|
the throttle is imposed!
|
||||||
"""
|
"""
|
||||||
self.storage = defaultdict(deque)
|
try:
|
||||||
self.cache_size = self.limit = kwargs.get("limit", 5)
|
self.storage = caches['throttle']
|
||||||
|
except Exception as e:
|
||||||
|
logger.log_err(f'Throttle: {e}')
|
||||||
|
self.storage = caches['default']
|
||||||
|
|
||||||
|
self.name = kwargs.get('name', 'ip-throttle')
|
||||||
|
self.limit = kwargs.get("limit", 5)
|
||||||
|
self.cache_size = kwargs.get('cache_size', self.limit)
|
||||||
self.timeout = kwargs.get("timeout", 5 * 60)
|
self.timeout = kwargs.get("timeout", 5 * 60)
|
||||||
|
|
||||||
|
def get_cache_key(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a 'prefixed' key containing arbitrary terms to prevent key
|
||||||
|
collisions in the same namespace.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return '-'.join((self.name, *args))
|
||||||
|
|
||||||
|
def touch(self, key, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Refreshes the timeout on a given key and ensures it is recorded in the
|
||||||
|
key register.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(str): Key of entry to renew.
|
||||||
|
|
||||||
|
"""
|
||||||
|
cache_key = self.get_cache_key(key)
|
||||||
|
if self.storage.touch(cache_key, self.timeout):
|
||||||
|
self.record_key(key)
|
||||||
|
|
||||||
def get(self, ip=None):
|
def get(self, ip=None):
|
||||||
"""
|
"""
|
||||||
Convenience function that returns the storage table, or part of.
|
Convenience function that returns the storage table, or part of.
|
||||||
|
|
@ -50,9 +80,18 @@ class Throttle(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if ip:
|
if ip:
|
||||||
return self.storage.get(ip, deque(maxlen=self.cache_size))
|
cache_key = self.get_cache_key(str(ip))
|
||||||
|
return self.storage.get(cache_key, deque(maxlen=self.limit))
|
||||||
else:
|
else:
|
||||||
return self.storage
|
keys_key = self.get_cache_key('keys')
|
||||||
|
keys = self.storage.get_or_set(keys_key, set(), self.timeout)
|
||||||
|
data = self.storage.get_many((self.get_cache_key(x) for x in keys))
|
||||||
|
|
||||||
|
found_keys = set(data.keys())
|
||||||
|
if len(keys) != len(found_keys):
|
||||||
|
self.storage.set(keys_key, found_keys, self.timeout)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def update(self, ip, failmsg="Exceeded threshold."):
|
def update(self, ip, failmsg="Exceeded threshold."):
|
||||||
"""
|
"""
|
||||||
|
|
@ -67,24 +106,41 @@ class Throttle(object):
|
||||||
None
|
None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
cache_key = self.get_cache_key(ip)
|
||||||
|
|
||||||
# Get current status
|
# Get current status
|
||||||
previously_throttled = self.check(ip)
|
previously_throttled = self.check(ip)
|
||||||
|
|
||||||
# Enforce length limits
|
# Get previous failures, if any
|
||||||
if not self.storage[ip].maxlen:
|
entries = self.storage.get(cache_key, [])
|
||||||
self.storage[ip] = deque(maxlen=self.cache_size)
|
entries.append(time.time())
|
||||||
|
|
||||||
self.storage[ip].append(time.time())
|
# Store updated record
|
||||||
|
self.storage.set(cache_key, deque(entries, maxlen=self.limit), self.timeout)
|
||||||
|
|
||||||
# See if this update caused a change in status
|
# See if this update caused a change in status
|
||||||
currently_throttled = self.check(ip)
|
currently_throttled = self.check(ip)
|
||||||
|
|
||||||
# If this makes it engage, log a single activation event
|
# If this makes it engage, log a single activation event
|
||||||
if not previously_throttled and currently_throttled:
|
if not previously_throttled and currently_throttled:
|
||||||
logger.log_sec(
|
logger.log_sec(f"Throttle Activated: {failmsg} (IP: {ip}, {self.limit} hits in {self.timeout} seconds.)")
|
||||||
"Throttle Activated: %s (IP: %s, %i hits in %i seconds.)"
|
|
||||||
% (failmsg, ip, self.limit, self.timeout)
|
self.record_key(ip)
|
||||||
)
|
|
||||||
|
def record_key(self, key, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Tracks keys as they are added to the cache (since there is no way to
|
||||||
|
get a list of keys after-the-fact).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(str): Key being added to cache. This should be the original
|
||||||
|
key, not the cache-prefixed version.
|
||||||
|
|
||||||
|
"""
|
||||||
|
keys_key = self.get_cache_key('keys')
|
||||||
|
keys = self.storage.get(keys_key, set())
|
||||||
|
keys.add(key)
|
||||||
|
self.storage.set(keys_key, keys, self.timeout)
|
||||||
|
|
||||||
def check(self, ip):
|
def check(self, ip):
|
||||||
"""
|
"""
|
||||||
|
|
@ -103,16 +159,19 @@ class Throttle(object):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
ip = str(ip)
|
ip = str(ip)
|
||||||
|
|
||||||
|
cache_key = self.get_cache_key(ip)
|
||||||
|
|
||||||
# checking mode
|
# checking mode
|
||||||
latest_fails = self.storage[ip]
|
latest_fails = self.storage.get(cache_key)
|
||||||
if latest_fails and len(latest_fails) >= self.limit:
|
if latest_fails and len(latest_fails) >= self.limit:
|
||||||
# too many fails recently
|
# too many fails recently
|
||||||
if now - latest_fails[-1] < self.timeout:
|
if now - latest_fails[-1] < self.timeout:
|
||||||
# too soon - timeout in play
|
# too soon - timeout in play
|
||||||
|
self.touch(cache_key)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# timeout has passed. clear faillist
|
# timeout has passed. clear faillist
|
||||||
del self.storage[ip]
|
self.storage.delete(cache_key)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
Loading…
Add table
Add a link
Reference in a new issue