Integrates new Throttle with unconnected Commands; rate limits new account creation (partial fix for #1523).

This commit is contained in:
Johnny 2018-09-21 17:39:51 +00:00
parent 791ace73bc
commit a2cccd7326

View file

@ -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 <name> <password>")
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: