Moves account creation logic from Commands module to Account class.
This commit is contained in:
parent
16648d47d1
commit
21d66ab625
4 changed files with 354 additions and 176 deletions
|
|
@ -23,8 +23,9 @@ from evennia.accounts.models import AccountDB
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
from evennia.server.models import ServerConfig
|
||||||
from evennia.server.throttle import Throttle
|
from evennia.server.throttle import Throttle
|
||||||
from evennia.utils import logger
|
from evennia.utils import create, logger
|
||||||
from evennia.utils.utils import (lazy_property, to_str,
|
from evennia.utils.utils import (lazy_property, to_str,
|
||||||
make_iter, to_unicode, is_iter,
|
make_iter, to_unicode, is_iter,
|
||||||
variable_from_module)
|
variable_from_module)
|
||||||
|
|
@ -34,6 +35,7 @@ from evennia.commands.cmdsethandler import CmdSetHandler
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
|
from random import getrandbits
|
||||||
|
|
||||||
__all__ = ("DefaultAccount",)
|
__all__ = ("DefaultAccount",)
|
||||||
|
|
||||||
|
|
@ -364,6 +366,31 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
puppet = property(__get_single_puppet)
|
puppet = property(__get_single_puppet)
|
||||||
|
|
||||||
# utility methods
|
# utility methods
|
||||||
|
@classmethod
|
||||||
|
def is_banned(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Checks if a given username or IP is banned.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
ip (str, optional): IP address.
|
||||||
|
username (str, optional): Username.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
is_banned (bool): Whether either is banned or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ip = kwargs.get('ip', '').strip()
|
||||||
|
username = kwargs.get('username', '').lower().strip()
|
||||||
|
|
||||||
|
# Check IP and/or name bans
|
||||||
|
bans = ServerConfig.objects.conf("server_bans")
|
||||||
|
if bans and (any(tup[0] == username for tup in bans if username) or
|
||||||
|
any(tup[2].match(ip) for tup in bans if ip and tup[2])):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
||||||
"""
|
"""
|
||||||
|
|
@ -386,9 +413,85 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
raise ImproperlyConfigured(msg % validator['NAME'])
|
raise ImproperlyConfigured(msg % validator['NAME'])
|
||||||
objs.append(klass(**validator.get('OPTIONS', {})))
|
objs.append(klass(**validator.get('OPTIONS', {})))
|
||||||
return objs
|
return objs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate_guest(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Gets or creates a Guest account object.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
ip (str, optional): IP address of requestor; used for ban checking,
|
||||||
|
throttling and logging
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
account = None
|
||||||
|
username = None
|
||||||
|
ip = kwargs.get('ip', '').strip()
|
||||||
|
|
||||||
|
# check if guests are enabled.
|
||||||
|
if not settings.GUEST_ENABLED:
|
||||||
|
errors.append('Guest accounts are not enabled on this server.')
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# See if authentication is currently being throttled
|
||||||
|
if ip and LOGIN_THROTTLE.check(ip):
|
||||||
|
errors.append('Too many login failures; please try again in a few minutes.')
|
||||||
|
|
||||||
|
# With throttle active, do not log continued hits-- it is a
|
||||||
|
# waste of storage and can be abused to make your logs harder to
|
||||||
|
# read and/or fill up your disk.
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# check if IP banned
|
||||||
|
if ip and cls.is_banned(ip=ip):
|
||||||
|
errors.append("|rYou have been banned and cannot continue from here." \
|
||||||
|
"\nIf you feel this ban is in error, please email an admin.|x")
|
||||||
|
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % ('guest', ip))
|
||||||
|
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned IP.')
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find an available guest name.
|
||||||
|
for name in settings.GUEST_LIST:
|
||||||
|
if not AccountDB.objects.filter(username__iexact=name).count():
|
||||||
|
username = name
|
||||||
|
break
|
||||||
|
if not username:
|
||||||
|
errors.append("All guest accounts are in use. Please try again later.")
|
||||||
|
if ip: LOGIN_THROTTLE.update(ip, 'Too many requests for Guest access.')
|
||||||
|
return None, errors
|
||||||
|
else:
|
||||||
|
# build a new account with the found guest username
|
||||||
|
password = "%016x" % getrandbits(64)
|
||||||
|
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
|
||||||
|
permissions = settings.PERMISSION_GUEST_DEFAULT
|
||||||
|
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||||
|
account_typeclass = settings.BASE_GUEST_TYPECLASS
|
||||||
|
account, errs = cls.create(
|
||||||
|
guest=True,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
permissions=permissions,
|
||||||
|
account_typeclass=account_typeclass,
|
||||||
|
character_typeclass=character_typeclass,
|
||||||
|
ip=ip,
|
||||||
|
)
|
||||||
|
errors.extend(errs)
|
||||||
|
return account, errors
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# We are in the middle between logged in and -not, so we have
|
||||||
|
# to handle tracebacks ourselves at this point. If we don't,
|
||||||
|
# we won't see any errors at all.
|
||||||
|
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
|
logger.log_trace()
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
return account, errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def authenticate(cls, username, password, ip=None):
|
def authenticate(cls, username, password, ip='', **kwargs):
|
||||||
"""
|
"""
|
||||||
Checks the given username/password against the database to see if the
|
Checks the given username/password against the database to see if the
|
||||||
credentials are valid.
|
credentials are valid.
|
||||||
|
|
@ -408,6 +511,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
password (str): Password of account
|
password (str): Password of account
|
||||||
ip (str, optional): IP address of client
|
ip (str, optional): IP address of client
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
session (Session, optional): Session requesting authentication
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
account (DefaultAccount, None): Account whose credentials were
|
account (DefaultAccount, None): Account whose credentials were
|
||||||
provided if not banned.
|
provided if not banned.
|
||||||
|
|
@ -423,7 +529,17 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
|
|
||||||
# With throttle active, do not log continued hits-- it is a
|
# With throttle active, do not log continued hits-- it is a
|
||||||
# waste of storage and can be abused to make your logs harder to
|
# waste of storage and can be abused to make your logs harder to
|
||||||
# read and fill up your disk.
|
# read and/or fill up your disk.
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Check IP and/or name bans
|
||||||
|
banned = cls.is_banned(username=username, ip=ip)
|
||||||
|
if banned:
|
||||||
|
# this is a banned IP or name!
|
||||||
|
errors.append("|rYou have been banned and cannot continue from here." \
|
||||||
|
"\nIf you feel this ban is in error, please email an admin.|x")
|
||||||
|
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip))
|
||||||
|
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.')
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# Authenticate and get Account object
|
# Authenticate and get Account object
|
||||||
|
|
@ -436,7 +552,14 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
||||||
|
|
||||||
# Update throttle
|
# Update throttle
|
||||||
if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures')
|
if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
|
||||||
|
|
||||||
|
# Try to call post-failure hook
|
||||||
|
session = kwargs.get('session', None)
|
||||||
|
if session:
|
||||||
|
account = AccountDB.objects.get_account_from_name(username)
|
||||||
|
if account:
|
||||||
|
account.at_failed_login(session)
|
||||||
|
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
|
|
@ -493,7 +616,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
valid.append(not validator(username))
|
valid.append(not validator(username))
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
valid.append(False)
|
valid.append(False)
|
||||||
[errors.append(x) for x in e.messages]
|
errors.extend(e.messages)
|
||||||
|
|
||||||
# Disqualify if any check failed
|
# Disqualify if any check failed
|
||||||
if False in valid:
|
if False in valid:
|
||||||
|
|
@ -561,6 +684,142 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
super(DefaultAccount, self).set_password(password)
|
super(DefaultAccount, self).set_password(password)
|
||||||
logger.log_sec("Password successfully changed for %s." % self)
|
logger.log_sec("Password successfully changed for %s." % self)
|
||||||
self.at_password_change()
|
self.at_password_change()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates an Account (or Account/Character pair for MM<2) with default
|
||||||
|
(or overridden) permissions and having joined them to the appropriate
|
||||||
|
default channels.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
username (str): Username of Account owner
|
||||||
|
password (str): Password of Account owner
|
||||||
|
email (str, optional): Email address of Account owner
|
||||||
|
ip (str, optional): IP address of requesting connection
|
||||||
|
guest (bool, optional): Whether or not this is to be a Guest account
|
||||||
|
|
||||||
|
permissions (str, optional): Default permissions for the Account
|
||||||
|
account_typeclass (str, optional): Typeclass to use for new Account
|
||||||
|
character_typeclass (str, optional): Typeclass to use for new char
|
||||||
|
when applicable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
account (Account): Account if successfully created; None if not
|
||||||
|
errors (list): List of error messages in string form
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
account = None
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
username = kwargs.get('username')
|
||||||
|
password = kwargs.get('password')
|
||||||
|
email = kwargs.get('email', '').strip()
|
||||||
|
guest = kwargs.get('guest', False)
|
||||||
|
|
||||||
|
permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
||||||
|
account_typeclass = kwargs.get('account_typeclass', settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS)
|
||||||
|
|
||||||
|
ip = kwargs.get('ip', '')
|
||||||
|
if ip and CREATION_THROTTLE.check(ip):
|
||||||
|
errors.append("You are creating too many accounts. Please log into an existing account.")
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Normalize username
|
||||||
|
username = cls.normalize_username(username)
|
||||||
|
|
||||||
|
# Validate username
|
||||||
|
if not guest:
|
||||||
|
valid, errs = cls.validate_username(username)
|
||||||
|
if not valid:
|
||||||
|
# this echoes the restrictions made by django's auth
|
||||||
|
# module (except not allowing spaces, for convenience of
|
||||||
|
# logging in).
|
||||||
|
errors.extend(errs)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Validate password
|
||||||
|
# Have to create a dummy Account object to check username similarity
|
||||||
|
valid, errs = cls.validate_password(password, account=cls(username=username))
|
||||||
|
if not valid:
|
||||||
|
errors.extend(errs)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Check IP and/or name bans
|
||||||
|
banned = cls.is_banned(username=username, ip=ip)
|
||||||
|
if banned:
|
||||||
|
# this is a banned IP or name!
|
||||||
|
string = "|rYou have been banned and cannot continue from here." \
|
||||||
|
"\nIf you feel this ban is in error, please email an admin.|x"
|
||||||
|
errors.append(string)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# everything's ok. Create the new account account.
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
account = create.create_account(username, email, password, permissions=permissions, typeclass=account_typeclass)
|
||||||
|
logger.log_sec('Account Created: %s (IP: %s).' % (account, ip))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
|
||||||
|
logger.log_trace()
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# This needs to be set so the engine knows this account is
|
||||||
|
# logging in for the first time. (so it knows to call the right
|
||||||
|
# hooks during login later)
|
||||||
|
account.db.FIRST_LOGIN = True
|
||||||
|
|
||||||
|
# Record IP address of creation, if available
|
||||||
|
if ip: account.db.creator_ip = ip
|
||||||
|
|
||||||
|
# join the new account to the public channel
|
||||||
|
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
||||||
|
if not pchannel or not pchannel.connect(account):
|
||||||
|
string = "New account '%s' could not connect to public channel!" % account.key
|
||||||
|
errors.append(string)
|
||||||
|
logger.log_err(string)
|
||||||
|
|
||||||
|
if account:
|
||||||
|
if settings.MULTISESSION_MODE < 2:
|
||||||
|
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
||||||
|
|
||||||
|
try:
|
||||||
|
character = create.create_object(character_typeclass, key=account.key, home=default_home, permissions=permissions)
|
||||||
|
|
||||||
|
# set playable character list
|
||||||
|
account.db._playable_characters.append(character)
|
||||||
|
|
||||||
|
# allow only the character itself and the account to puppet this character (and Developers).
|
||||||
|
character.locks.add("puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer)" %
|
||||||
|
(character.id, account.id))
|
||||||
|
|
||||||
|
# If no description is set, set a default description
|
||||||
|
if not character.db.desc:
|
||||||
|
character.db.desc = "This is a character."
|
||||||
|
# We need to set this to have @ic auto-connect to this character
|
||||||
|
account.db._last_puppet = character
|
||||||
|
|
||||||
|
# Record creator id and creation IP
|
||||||
|
if ip: character.db.creator_ip = ip
|
||||||
|
character.db.creator_id = account.id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("There was an error creating a Character. If this problem persists, contact an admin.")
|
||||||
|
logger.log_trace()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# We are in the middle between logged in and -not, so we have
|
||||||
|
# to handle tracebacks ourselves at this point. If we don't,
|
||||||
|
# we won't see any errors at all.
|
||||||
|
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
|
logger.log_trace()
|
||||||
|
|
||||||
|
# Update the throttle to indicate a new account was created from this IP
|
||||||
|
if ip and not guest: CREATION_THROTTLE.update(ip, 'Too many accounts being created.')
|
||||||
|
return account, errors
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,41 @@ class TestDefaultAccount(TestCase):
|
||||||
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
|
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
|
||||||
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
|
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
"Confirm Account creation is working as expected."
|
||||||
|
# Create a normal account
|
||||||
|
account, errors = DefaultAccount.create(username='ziggy', password='stardust11')
|
||||||
|
self.assertTrue(account, 'New account should have been created.')
|
||||||
|
|
||||||
|
# Try creating a duplicate account
|
||||||
|
account, errors = DefaultAccount.create(username='Ziggy', password='starman11')
|
||||||
|
self.assertFalse(account, 'Duplicate account name should not have been allowed.')
|
||||||
|
|
||||||
|
# Guest account should not be permitted
|
||||||
|
account, errors = DefaultAccount.authenticate_guest()
|
||||||
|
self.assertFalse(account, 'Guest account was created despite being disabled.')
|
||||||
|
|
||||||
|
settings.GUEST_ENABLED = True
|
||||||
|
settings.GUEST_LIST = ['bruce_wayne']
|
||||||
|
|
||||||
|
# Create a guest account
|
||||||
|
account, errors = DefaultAccount.authenticate_guest()
|
||||||
|
self.assertTrue(account, 'Guest account should have been created.')
|
||||||
|
|
||||||
|
# Create a second guest account
|
||||||
|
account, errors = DefaultAccount.authenticate_guest()
|
||||||
|
self.assertFalse(account, 'Two guest accounts were created despite a single entry on the guest list!')
|
||||||
|
|
||||||
|
settings.GUEST_ENABLED = False
|
||||||
|
|
||||||
|
def test_throttle(self):
|
||||||
|
"Confirm throttle activates on too many failures."
|
||||||
|
for x in xrange(20):
|
||||||
|
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy', ip='12.24.36.48')
|
||||||
|
self.assertFalse(obj, 'Authentication was provided a bogus password; this should NOT have returned an account!')
|
||||||
|
|
||||||
|
self.assertTrue('too many login failures' in errors[-1].lower(), 'Failed logins should have been throttled.')
|
||||||
|
|
||||||
def test_username_validation(self):
|
def test_username_validation(self):
|
||||||
"Check username validators deny relevant usernames"
|
"Check username validators deny relevant usernames"
|
||||||
# Should not accept Unicode by default, lest users pick names like this
|
# Should not accept Unicode by default, lest users pick names like this
|
||||||
|
|
@ -92,7 +127,7 @@ class TestDefaultAccount(TestCase):
|
||||||
def test_password_validation(self):
|
def test_password_validation(self):
|
||||||
"Check password validators deny bad passwords"
|
"Check password validators deny bad passwords"
|
||||||
|
|
||||||
self.account = create.create_account("TestAccount%s" % randint(0, 9),
|
self.account = create.create_account("TestAccount%s" % randint(100000, 999999),
|
||||||
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
||||||
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
|
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
|
||||||
self.assertFalse(self.account.validate_password(bad, account=self.account)[0])
|
self.assertFalse(self.account.validate_password(bad, account=self.account)[0])
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,9 @@ Commands that are available from the connect screen.
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
from random import getrandbits
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from evennia.accounts.accounts import CREATION_THROTTLE, LOGIN_THROTTLE
|
|
||||||
from evennia.accounts.models import AccountDB
|
from evennia.accounts.models import AccountDB
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
|
|
@ -15,7 +14,7 @@ from evennia.server.models import ServerConfig
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia.server.throttle import Throttle
|
from evennia.server.throttle import Throttle
|
||||||
|
|
||||||
from evennia.utils import create, logger, utils, gametime
|
from evennia.utils import class_from_module, create, logger, utils, gametime
|
||||||
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
@ -27,9 +26,6 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
||||||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||||
|
|
||||||
# Create throttles for too many connections
|
|
||||||
CONNECTION_THROTTLE = Throttle(limit=5, timeout=1 * 60)
|
|
||||||
|
|
||||||
def create_guest_account(session):
|
def create_guest_account(session):
|
||||||
"""
|
"""
|
||||||
Creates a guest account/character for this session, if one is available.
|
Creates a guest account/character for this session, if one is available.
|
||||||
|
|
@ -42,50 +38,20 @@ def create_guest_account(session):
|
||||||
the boolean is whether guest accounts are enabled at all.
|
the boolean is whether guest accounts are enabled at all.
|
||||||
the Account which was created from an available guest name.
|
the Account which was created from an available guest name.
|
||||||
"""
|
"""
|
||||||
# check if guests are enabled.
|
enabled = settings.GUEST_ENABLED
|
||||||
if not settings.GUEST_ENABLED:
|
address = session.address
|
||||||
return False, None
|
|
||||||
|
# Get account class
|
||||||
# Check IP bans.
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
bans = ServerConfig.objects.conf("server_bans")
|
|
||||||
if bans and any(tup[2].match(session.address) for tup in bans if tup[2]):
|
# Get an available guest account
|
||||||
# this is a banned IP!
|
# authenticate_guest() handles its own throttling
|
||||||
string = "|rYou have been banned and cannot continue from here." \
|
account, errors = Account.authenticate_guest(ip=address)
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
if account:
|
||||||
session.msg(string)
|
return enabled, account
|
||||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
else:
|
||||||
return True, None
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
|
return enabled, None
|
||||||
try:
|
|
||||||
# Find an available guest name.
|
|
||||||
accountname = None
|
|
||||||
for name in settings.GUEST_LIST:
|
|
||||||
if not AccountDB.objects.filter(username__iexact=accountname).count():
|
|
||||||
accountname = name
|
|
||||||
break
|
|
||||||
if not accountname:
|
|
||||||
session.msg("All guest accounts are in use. Please try again later.")
|
|
||||||
return True, None
|
|
||||||
else:
|
|
||||||
# build a new account with the found guest accountname
|
|
||||||
password = "%016x" % getrandbits(64)
|
|
||||||
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
|
|
||||||
permissions = settings.PERMISSION_GUEST_DEFAULT
|
|
||||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
|
||||||
ptypeclass = settings.BASE_GUEST_TYPECLASS
|
|
||||||
new_account = _create_account(session, accountname, password, permissions, ptypeclass)
|
|
||||||
if new_account:
|
|
||||||
_create_character(session, new_account, typeclass, home, permissions)
|
|
||||||
return True, new_account
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# We are in the middle between logged in and -not, so we have
|
|
||||||
# to handle tracebacks ourselves at this point. If we don't,
|
|
||||||
# we won't see any errors at all.
|
|
||||||
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):
|
def create_normal_account(session, name, password):
|
||||||
"""
|
"""
|
||||||
|
|
@ -99,38 +65,17 @@ def create_normal_account(session, name, password):
|
||||||
Returns:
|
Returns:
|
||||||
account (Account): the account which was created from the name and password.
|
account (Account): the account which was created from the name and password.
|
||||||
"""
|
"""
|
||||||
# check for too many login errors too quick.
|
# Get account class
|
||||||
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
address = session.address
|
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
|
|
||||||
|
|
||||||
# Match account name and check password
|
# Match account name and check password
|
||||||
account = authenticate(username=name, password=password)
|
# authenticate() handles all its own throttling
|
||||||
|
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
||||||
if not account:
|
if not account:
|
||||||
# No accountname or password match
|
# No accountname or password match
|
||||||
session.msg("Incorrect login information given.")
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
# this just updates the throttle
|
|
||||||
LOGIN_THROTTLE.update(address)
|
|
||||||
# calls account hook for a failed login if possible.
|
|
||||||
account = AccountDB.objects.get_account_from_name(name)
|
|
||||||
if account:
|
|
||||||
account.at_failed_login(session)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 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." \
|
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
|
||||||
session.msg(string)
|
|
||||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
@ -162,15 +107,10 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
||||||
there is no object yet before the account has logged in)
|
there is no object yet before the account has logged in)
|
||||||
"""
|
"""
|
||||||
session = self.caller
|
session = self.caller
|
||||||
|
|
||||||
# check for too many login errors too quick.
|
|
||||||
address = session.address
|
address = session.address
|
||||||
if isinstance(address, tuple):
|
|
||||||
address = address[0]
|
# Get account class
|
||||||
if CONNECTION_THROTTLE.check(address):
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
# timeout is 5 minutes.
|
|
||||||
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
|
|
||||||
return
|
|
||||||
|
|
||||||
args = self.args
|
args = self.args
|
||||||
# extract double quote parts
|
# extract double quote parts
|
||||||
|
|
@ -178,23 +118,27 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
# this was (hopefully) due to no double quotes being found, or a guest login
|
# this was (hopefully) due to no double quotes being found, or a guest login
|
||||||
parts = parts[0].split(None, 1)
|
parts = parts[0].split(None, 1)
|
||||||
|
|
||||||
# Guest login
|
# Guest login
|
||||||
if len(parts) == 1 and parts[0].lower() == "guest":
|
if len(parts) == 1 and parts[0].lower() == "guest":
|
||||||
enabled, new_account = create_guest_account(session)
|
account, errors = Account.authenticate_guest(ip=address)
|
||||||
if new_account:
|
if account:
|
||||||
session.sessionhandler.login(session, new_account)
|
session.sessionhandler.login(session, account)
|
||||||
if enabled:
|
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
|
return
|
||||||
|
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
session.msg("\n\r Usage (without <>): connect <name> <password>")
|
session.msg("\n\r Usage (without <>): connect <name> <password>")
|
||||||
return
|
return
|
||||||
|
|
||||||
CONNECTION_THROTTLE.update(address)
|
|
||||||
name, password = parts
|
name, password = parts
|
||||||
account = create_normal_account(session, name, password)
|
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
||||||
if account:
|
if account:
|
||||||
session.sessionhandler.login(session, account)
|
session.sessionhandler.login(session, account)
|
||||||
|
else:
|
||||||
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
|
|
||||||
|
|
||||||
class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -220,14 +164,10 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
session = self.caller
|
session = self.caller
|
||||||
args = self.args.strip()
|
args = self.args.strip()
|
||||||
|
|
||||||
# Rate-limit account creation.
|
|
||||||
address = session.address
|
address = session.address
|
||||||
|
|
||||||
if isinstance(address, tuple):
|
# Get account class
|
||||||
address = address[0]
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
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
|
# extract double quoted parts
|
||||||
parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
|
parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
|
||||||
|
|
@ -239,77 +179,21 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
|
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
|
||||||
session.msg(string)
|
session.msg(string)
|
||||||
return
|
return
|
||||||
accountname, password = parts
|
|
||||||
|
username, password = parts
|
||||||
# sanity checks
|
|
||||||
if not re.findall(r"^[\w. @+\-']+$", accountname) or not (0 < len(accountname) <= 30):
|
|
||||||
# this echoes the restrictions made by django's auth
|
|
||||||
# module (except not allowing spaces, for convenience of
|
|
||||||
# logging in).
|
|
||||||
string = "\n\r Accountname can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_/' only."
|
|
||||||
session.msg(string)
|
|
||||||
return
|
|
||||||
# strip excessive spaces in accountname
|
|
||||||
accountname = re.sub(r"\s+", " ", accountname).strip()
|
|
||||||
if AccountDB.objects.filter(username__iexact=accountname):
|
|
||||||
# account already exists (we also ignore capitalization here)
|
|
||||||
session.msg("Sorry, there is already an account with the name '%s'." % accountname)
|
|
||||||
return
|
|
||||||
# Reserve accountnames found in GUEST_LIST
|
|
||||||
if settings.GUEST_LIST and accountname.lower() in (guest.lower() for guest in settings.GUEST_LIST):
|
|
||||||
string = "\n\r That name is reserved. Please choose another Accountname."
|
|
||||||
session.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Validate password
|
|
||||||
Account = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
|
||||||
# Have to create a dummy Account object to check username similarity
|
|
||||||
valid, error = Account.validate_password(password, account=Account(username=accountname))
|
|
||||||
if error:
|
|
||||||
errors = [e for suberror in error.messages for e in error.messages]
|
|
||||||
string = "\n".join(errors)
|
|
||||||
session.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check IP and/or name bans
|
|
||||||
bans = ServerConfig.objects.conf("server_bans")
|
|
||||||
if bans and (any(tup[0] == accountname.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." \
|
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
|
||||||
session.msg(string)
|
|
||||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# everything's ok. Create the new account account.
|
# everything's ok. Create the new account account.
|
||||||
try:
|
account, errors = Account.create(username=username, password=password, ip=address, session=session)
|
||||||
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
|
if account:
|
||||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
# tell the caller everything went well.
|
||||||
new_account = _create_account(session, accountname, password, permissions)
|
string = "A new account '%s' was created. Welcome!"
|
||||||
if new_account:
|
if " " in username:
|
||||||
if MULTISESSION_MODE < 2:
|
string += "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
|
||||||
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
else:
|
||||||
_create_character(session, new_account, typeclass, default_home, permissions)
|
string += "\n\nYou can now log with the command 'connect %s <your password>'."
|
||||||
|
session.msg(string % (username, username))
|
||||||
# Update the throttle to indicate a new account was created from this IP
|
else:
|
||||||
CREATION_THROTTLE.update(address)
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
|
|
||||||
# tell the caller everything went well.
|
|
||||||
string = "A new account '%s' was created. Welcome!"
|
|
||||||
if " " in accountname:
|
|
||||||
string += "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
|
|
||||||
else:
|
|
||||||
string += "\n\nYou can now log with the command 'connect %s <your password>'."
|
|
||||||
session.msg(string % (accountname, accountname))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# We are in the middle between logged in and -not, so we have
|
|
||||||
# to handle tracebacks ourselves at this point. If we don't,
|
|
||||||
# we won't see any errors at all.
|
|
||||||
session.msg("An error occurred. Please e-mail an admin if the problem persists.")
|
|
||||||
logger.log_trace()
|
|
||||||
|
|
||||||
|
|
||||||
class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
|
class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class EvenniaUsernameAvailabilityValidator:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check guest list
|
# Check guest list
|
||||||
if settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST):
|
if (settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST)):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Sorry, that username is reserved.'),
|
_('Sorry, that username is reserved.'),
|
||||||
code='evennia_username_reserved',
|
code='evennia_username_reserved',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue