From a2cccd73266fcb178a86fa369606f0a44b488106 Mon Sep 17 00:00:00 2001 From: Johnny Date: Fri, 21 Sep 2018 17:39:51 +0000 Subject: [PATCH] Integrates new Throttle with unconnected Commands; rate limits new account creation (partial fix for #1523). --- evennia/commands/default/unloggedin.py | 90 +++++++++----------------- 1 file changed, 31 insertions(+), 59 deletions(-) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index bc7e69934f..f1b26137b7 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -4,13 +4,14 @@ Commands that are available from the connect screen. import re import time import datetime -from collections import defaultdict +from collections import defaultdict, deque from random import getrandbits from django.conf import settings from django.contrib.auth import authenticate from evennia.accounts.models import AccountDB from evennia.objects.models import ObjectDB from evennia.server.models import ServerConfig +from evennia.server.throttle import Throttle from evennia.comms.models import ChannelDB from evennia.server.sessionhandler import SESSIONS @@ -26,58 +27,11 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate", MULTISESSION_MODE = settings.MULTISESSION_MODE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE -# Helper function to throttle failed connection attempts. -# This can easily be used to limit account creation too, -# (just supply a different storage dictionary), but this -# would also block dummyrunner, so it's not added as default. - -_LATEST_FAILED_LOGINS = defaultdict(list) - - -def _throttle(session, maxlim=None, timeout=None, storage=_LATEST_FAILED_LOGINS): - """ - This will check the session's address against the - _LATEST_LOGINS dictionary to check they haven't - spammed too many fails recently. - - Args: - session (Session): Session failing - maxlim (int): max number of attempts to allow - timeout (int): number of timeout seconds after - max number of tries has been reached. - - Returns: - throttles (bool): True if throttling is active, - False otherwise. - - Notes: - If maxlim and/or timeout are set, the function will - just do the comparison, not append a new datapoint. - - """ - address = session.address - if isinstance(address, tuple): - address = address[0] - now = time.time() - if maxlim and timeout: - # checking mode - latest_fails = storage[address] - if latest_fails and len(latest_fails) >= maxlim: - # too many fails recently - if now - latest_fails[-1] < timeout: - # too soon - timeout in play - return True - else: - # timeout has passed. Reset faillist - storage[address] = [] - return False - else: - return False - else: - # store the time of the latest fail - storage[address].append(time.time()) - return False +# Create an object to store account creation attempts per IP +CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60) +# Create an object to store failed login attempts per IP +LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60) def create_guest_account(session): """ @@ -134,7 +88,7 @@ def create_guest_account(session): session.msg("An error occurred. Please e-mail an admin if the problem persists.") logger.log_trace() raise - + def create_normal_account(session, name, password): """ @@ -149,8 +103,11 @@ def create_normal_account(session, name, password): account (Account): the account which was created from the name and password. """ # check for too many login errors too quick. - if _throttle(session, maxlim=5, timeout=5 * 60): - # timeout is 5 minutes. + address = session.address + if isinstance(address, tuple): + address = address[0] + + if LOGIN_THROTTLE.check(address): session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n") return None @@ -161,7 +118,7 @@ def create_normal_account(session, name, password): # No accountname or password match session.msg("Incorrect login information given.") # this just updates the throttle - _throttle(session) + LOGIN_THROTTLE.update(address) # calls account hook for a failed login if possible. account = AccountDB.objects.get_account_from_name(name) if account: @@ -171,7 +128,6 @@ def create_normal_account(session, name, password): # Check IP and/or name bans bans = ServerConfig.objects.conf("server_bans") if bans and (any(tup[0] == account.name.lower() for tup in bans) or - any(tup[2].match(session.address) for tup in bans if tup[2])): # this is a banned IP or name! string = "|rYou have been banned and cannot continue from here." \ @@ -182,7 +138,6 @@ def create_normal_account(session, name, password): return account - class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS): """ connect to the game @@ -211,7 +166,10 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS): session = self.caller # check for too many login errors too quick. - if _throttle(session, maxlim=5, timeout=5 * 60, storage=_LATEST_FAILED_LOGINS): + address = session.address + if isinstance(address, tuple): + address = address[0] + if CONNECTION_THROTTLE.check(address): # timeout is 5 minutes. session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n") return @@ -234,6 +192,7 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS): session.msg("\n\r Usage (without <>): connect ") return + CONNECTION_THROTTLE.update(address) name, password = parts account = create_normal_account(session, name, password) if account: @@ -262,6 +221,15 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): session = self.caller args = self.args.strip() + + # Rate-limit account creation. + address = session.address + + if isinstance(address, tuple): + address = address[0] + if CREATION_THROTTLE.check(address): + session.msg("|RYou are creating too many accounts. Try again in a few minutes.|n") + return # extract double quoted parts parts = [part.strip() for part in re.split(r"\"", args) if part.strip()] @@ -322,6 +290,10 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS): if MULTISESSION_MODE < 2: default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME) _create_character(session, new_account, typeclass, default_home, permissions) + + # Update the throttle to indicate a new account was created from this IP + CREATION_THROTTLE.update(address) + # tell the caller everything went well. string = "A new account '%s' was created. Welcome!" if " " in accountname: