Update CHANGELOG, pep8 fixes

This commit is contained in:
Griatch 2018-10-23 01:04:25 +02:00
parent 0b6d869902
commit b6b07ccdb5
10 changed files with 290 additions and 264 deletions

View file

@ -10,6 +10,30 @@
- Add the Portal uptime to the `@time` command. - Add the Portal uptime to the `@time` command.
- Make the `@link` command first make a local search before a global search. - Make the `@link` command first make a local search before a global search.
### Typeclasses
- Add new methods on all typeclasses, useful specifically for viewing the object in the web/admin:
+ `web_get_admin_url()`: Returns a path that, if followed, will display the object in the Admin backend.
+ `web_get_create_url()`: Returns a path for a view allowing the creation of new instances of this object.
+ `web_get_absolute_url()`: Django construct; returns a path that should display the object in a DetailView.
+ `web_get_update_url()`: Returns a path that should display the object in an UpdateView.
+ `web_get_delete_url()`: Returns a path that should display the object in a DeleteView.
- All typeclasses has new helper class method `create`, which encompasses useful functionality
that used to be embedded for example in the respective `@create` or `@connect` commands.
- DefaultAccount now has new class methods implementing many things that used to be in unloggedin
commands (these can now be customized on the class instead):
+ `is_banned()`: Checks if a given username or IP is banned.
+ `get_username_validators`: Return list of validators for username validation (see
`settings.AUTH_USERNAME_VALIDATORS`)
+ `authenticate`: Method to check given username/password.
+ `normalize_username`: Normalizes names so you can't fake names with similar-looking Unicode
chars.
+ `validate_username`: Mechanism for validating a username.
+ `validate_password`: Mechanism for validating a password.
+ `set_password`: Apply password to account, using validation checks.
### Utils ### Utils
- Added more unit tests. - Added more unit tests.
@ -34,7 +58,7 @@
to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will
return Server to normal daemon operation. return Server to normal daemon operation.
- For validating passwords, use safe Django password-validation backend instead of custom Evennia one. - For validating passwords, use safe Django password-validation backend instead of custom Evennia one.
- Alias `evennia restart` to mean the same as `evennia reload`. - Alias `evennia restart` to mean the same as `evennia reload`.
### Prototype changes ### Prototype changes

View file

@ -370,40 +370,40 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
def is_banned(cls, **kwargs): def is_banned(cls, **kwargs):
""" """
Checks if a given username or IP is banned. Checks if a given username or IP is banned.
Kwargs: Kwargs:
ip (str, optional): IP address. ip (str, optional): IP address.
username (str, optional): Username. username (str, optional): Username.
Returns: Returns:
is_banned (bool): Whether either is banned or not. is_banned (bool): Whether either is banned or not.
""" """
ip = kwargs.get('ip', '').strip() ip = kwargs.get('ip', '').strip()
username = kwargs.get('username', '').lower().strip() username = kwargs.get('username', '').lower().strip()
# Check IP and/or name bans # Check IP and/or name bans
bans = ServerConfig.objects.conf("server_bans") bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0] == username for tup in bans if username) or 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])): any(tup[2].match(ip) for tup in bans if ip and tup[2])):
return True return True
return False 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', [])):
""" """
Retrieves and instantiates validators for usernames. Retrieves and instantiates validators for usernames.
Args: Args:
validator_config (list): List of dicts comprising the battery of validator_config (list): List of dicts comprising the battery of
validators to apply to a username. validators to apply to a username.
Returns: Returns:
validators (list): List of instantiated Validator objects. validators (list): List of instantiated Validator objects.
""" """
objs = [] objs = []
for validator in validator_config: for validator in validator_config:
try: try:
@ -413,49 +413,49 @@ 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 @classmethod
def authenticate(cls, username, password, ip='', **kwargs): 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.
Note that this simply checks credentials and returns a valid reference Note that this simply checks credentials and returns a valid reference
to the user-- it does not log them in! to the user-- it does not log them in!
To finish the job: To finish the job:
After calling this from a Command, associate the account with a Session: After calling this from a Command, associate the account with a Session:
- session.sessionhandler.login(session, account) - session.sessionhandler.login(session, account)
...or after calling this from a View, associate it with an HttpRequest: ...or after calling this from a View, associate it with an HttpRequest:
- django.contrib.auth.login(account, request) - django.contrib.auth.login(account, request)
Args: Args:
username (str): Username of account username (str): Username of account
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: Kwargs:
session (Session, optional): Session requesting authentication 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.
errors (list): Error messages of any failures. errors (list): Error messages of any failures.
""" """
errors = [] errors = []
if ip: ip = str(ip) if ip: ip = str(ip)
# See if authentication is currently being throttled # See if authentication is currently being throttled
if ip and LOGIN_THROTTLE.check(ip): if ip and LOGIN_THROTTLE.check(ip):
errors.append('Too many login failures; please try again in a few minutes.') errors.append('Too many login failures; please try again in a few minutes.')
# 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/or fill up your disk. # read and/or fill up your disk.
return None, errors return None, errors
# Check IP and/or name bans # Check IP and/or name bans
banned = cls.is_banned(username=username, ip=ip) banned = cls.is_banned(username=username, ip=ip)
if banned: if banned:
@ -465,19 +465,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip)) logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip))
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.') 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
account = authenticate(username=username, password=password) account = authenticate(username=username, password=password)
if not account: if not account:
# User-facing message # User-facing message
errors.append('Username and/or password is incorrect.') errors.append('Username and/or password is incorrect.')
# Log auth failures while throttle is inactive # Log auth failures while throttle is inactive
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 # Try to call post-failure hook
session = kwargs.get('session', None) session = kwargs.get('session', None)
if session: if session:
@ -486,49 +486,49 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
account.at_failed_login(session) account.at_failed_login(session)
return None, errors return None, errors
# Account successfully authenticated # Account successfully authenticated
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip)) logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
return account, errors return account, errors
@classmethod @classmethod
def normalize_username(cls, username): def normalize_username(cls, username):
""" """
Django: Applies NFKC Unicode normalization to usernames so that visually Django: Applies NFKC Unicode normalization to usernames so that visually
identical characters with different Unicode code points are considered identical characters with different Unicode code points are considered
identical. identical.
(This deals with the Turkish "i" problem and similar (This deals with the Turkish "i" problem and similar
annoyances. Only relevant if you go out of your way to allow Unicode annoyances. Only relevant if you go out of your way to allow Unicode
usernames though-- Evennia accepts ASCII by default.) usernames though-- Evennia accepts ASCII by default.)
In this case we're simply piggybacking on this feature to apply In this case we're simply piggybacking on this feature to apply
additional normalization per Evennia's standards. additional normalization per Evennia's standards.
""" """
username = super(DefaultAccount, cls).normalize_username(username) username = super(DefaultAccount, cls).normalize_username(username)
# strip excessive spaces in accountname # strip excessive spaces in accountname
username = re.sub(r"\s+", " ", username).strip() username = re.sub(r"\s+", " ", username).strip()
return username return username
@classmethod @classmethod
def validate_username(cls, username): def validate_username(cls, username):
""" """
Checks the given username against the username validator associated with Checks the given username against the username validator associated with
Account objects, and also checks the database to make sure it is unique. Account objects, and also checks the database to make sure it is unique.
Args: Args:
username (str): Username to validate username (str): Username to validate
Returns: Returns:
valid (bool): Whether or not the password passed validation valid (bool): Whether or not the password passed validation
errors (list): Error messages of any failures errors (list): Error messages of any failures
""" """
valid = [] valid = []
errors = [] errors = []
# Make sure we're at least using the default validator # Make sure we're at least using the default validator
validators = cls.get_username_validators() validators = cls.get_username_validators()
if not validators: if not validators:
@ -541,14 +541,14 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
except ValidationError as e: except ValidationError as e:
valid.append(False) valid.append(False)
errors.extend(e.messages) errors.extend(e.messages)
# Disqualify if any check failed # Disqualify if any check failed
if False in valid: if False in valid:
valid = False valid = False
else: valid = True else: valid = True
return valid, errors return valid, errors
@classmethod @classmethod
def validate_password(cls, password, account=None): def validate_password(cls, password, account=None):
""" """
@ -608,48 +608,48 @@ 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 @classmethod
def create(cls, *args, **kwargs): def create(cls, *args, **kwargs):
""" """
Creates an Account (or Account/Character pair for MULTISESSION_MODE<2) Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
with default (or overridden) permissions and having joined them to the with default (or overridden) permissions and having joined them to the
appropriate default channels. appropriate default channels.
Kwargs: Kwargs:
username (str): Username of Account owner username (str): Username of Account owner
password (str): Password of Account owner password (str): Password of Account owner
email (str, optional): Email address of Account owner email (str, optional): Email address of Account owner
ip (str, optional): IP address of requesting connection ip (str, optional): IP address of requesting connection
guest (bool, optional): Whether or not this is to be a Guest account guest (bool, optional): Whether or not this is to be a Guest account
permissions (str, optional): Default permissions for the Account permissions (str, optional): Default permissions for the Account
typeclass (str, optional): Typeclass to use for new Account typeclass (str, optional): Typeclass to use for new Account
character_typeclass (str, optional): Typeclass to use for new char character_typeclass (str, optional): Typeclass to use for new char
when applicable. when applicable.
Returns: Returns:
account (Account): Account if successfully created; None if not account (Account): Account if successfully created; None if not
errors (list): List of error messages in string form errors (list): List of error messages in string form
""" """
account = None account = None
errors = [] errors = []
username = kwargs.get('username') username = kwargs.get('username')
password = kwargs.get('password') password = kwargs.get('password')
email = kwargs.get('email', '').strip() email = kwargs.get('email', '').strip()
guest = kwargs.get('guest', False) guest = kwargs.get('guest', False)
permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT) permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
typeclass = kwargs.get('typeclass', settings.BASE_ACCOUNT_TYPECLASS) typeclass = kwargs.get('typeclass', settings.BASE_ACCOUNT_TYPECLASS)
ip = kwargs.get('ip', '') ip = kwargs.get('ip', '')
if ip and CREATION_THROTTLE.check(ip): if ip and CREATION_THROTTLE.check(ip):
errors.append("You are creating too many accounts. Please log into an existing account.") errors.append("You are creating too many accounts. Please log into an existing account.")
return None, errors return None, errors
# Normalize username # Normalize username
username = cls.normalize_username(username) username = cls.normalize_username(username)
@ -678,50 +678,50 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"\nIf you feel this ban is in error, please email an admin.|x" "\nIf you feel this ban is in error, please email an admin.|x"
errors.append(string) errors.append(string)
return None, errors return None, errors
# everything's ok. Create the new account account. # everything's ok. Create the new account account.
try: try:
try: try:
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass) account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
logger.log_sec('Account Created: %s (IP: %s).' % (account, ip)) logger.log_sec('Account Created: %s (IP: %s).' % (account, ip))
except Exception as e: except Exception as e:
errors.append("There was an error creating the Account. If this problem persists, contact an admin.") errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
logger.log_trace() logger.log_trace()
return None, errors return None, errors
# This needs to be set so the engine knows this account is # 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 # logging in for the first time. (so it knows to call the right
# hooks during login later) # hooks during login later)
account.db.FIRST_LOGIN = True account.db.FIRST_LOGIN = True
# Record IP address of creation, if available # Record IP address of creation, if available
if ip: account.db.creator_ip = ip if ip: account.db.creator_ip = ip
# join the new account to the public channel # join the new account to the public channel
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"]) pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
if not pchannel or not pchannel.connect(account): if not pchannel or not pchannel.connect(account):
string = "New account '%s' could not connect to public channel!" % account.key string = "New account '%s' could not connect to public channel!" % account.key
errors.append(string) errors.append(string)
logger.log_err(string) logger.log_err(string)
if account and settings.MULTISESSION_MODE < 2: if account and settings.MULTISESSION_MODE < 2:
# Load the appropriate Character class # Load the appropriate Character class
character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS) character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS)
character_home = kwargs.get('home') character_home = kwargs.get('home')
Character = class_from_module(character_typeclass) Character = class_from_module(character_typeclass)
# Create the character # Create the character
character, errs = Character.create( character, errs = Character.create(
account.key, account, ip=ip, typeclass=character_typeclass, account.key, account, ip=ip, typeclass=character_typeclass,
permissions=permissions, home=character_home permissions=permissions, home=character_home
) )
errors.extend(errs) errors.extend(errs)
if character: if character:
# Update playable character list # Update playable character list
account.db._playable_characters.append(character) account.db._playable_characters.append(character)
# We need to set this to have @ic auto-connect to this character # We need to set this to have @ic auto-connect to this character
account.db._last_puppet = character account.db._last_puppet = character
@ -731,7 +731,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# we won't see any errors at all. # we won't see any errors at all.
errors.append("An error occurred. Please e-mail an admin if the problem persists.") errors.append("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace() logger.log_trace()
# Update the throttle to indicate a new account was created from this IP # 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.') if ip and not guest: CREATION_THROTTLE.update(ip, 'Too many accounts being created.')
return account, errors return account, errors
@ -1384,7 +1384,7 @@ class DefaultGuest(DefaultAccount):
This class is used for guest logins. Unlike Accounts, Guests and This class is used for guest logins. Unlike Accounts, Guests and
their characters are deleted after disconnection. their characters are deleted after disconnection.
""" """
@classmethod @classmethod
def create(cls, **kwargs): def create(cls, **kwargs):
""" """
@ -1392,31 +1392,31 @@ class DefaultGuest(DefaultAccount):
if one is available for use. if one is available for use.
""" """
return cls.authenticate(**kwargs) return cls.authenticate(**kwargs)
@classmethod @classmethod
def authenticate(cls, **kwargs): def authenticate(cls, **kwargs):
""" """
Gets or creates a Guest account object. Gets or creates a Guest account object.
Kwargs: Kwargs:
ip (str, optional): IP address of requestor; used for ban checking, ip (str, optional): IP address of requestor; used for ban checking,
throttling and logging throttling and logging
Returns: Returns:
account (Object): Guest account object, if available account (Object): Guest account object, if available
errors (list): List of error messages accrued during this request. errors (list): List of error messages accrued during this request.
""" """
errors = [] errors = []
account = None account = None
username = None username = None
ip = kwargs.get('ip', '').strip() ip = kwargs.get('ip', '').strip()
# check if guests are enabled. # check if guests are enabled.
if not settings.GUEST_ENABLED: if not settings.GUEST_ENABLED:
errors.append('Guest accounts are not enabled on this server.') errors.append('Guest accounts are not enabled on this server.')
return None, errors return None, errors
try: try:
# Find an available guest name. # Find an available guest name.
for name in settings.GUEST_LIST: for name in settings.GUEST_LIST:
@ -1433,20 +1433,20 @@ class DefaultGuest(DefaultAccount):
home = settings.GUEST_HOME home = settings.GUEST_HOME
permissions = settings.PERMISSION_GUEST_DEFAULT permissions = settings.PERMISSION_GUEST_DEFAULT
typeclass = settings.BASE_GUEST_TYPECLASS typeclass = settings.BASE_GUEST_TYPECLASS
# Call parent class creator # Call parent class creator
account, errs = super(DefaultGuest, cls).create( account, errs = super(DefaultGuest, cls).create(
guest=True, guest=True,
username=username, username=username,
password=password, password=password,
permissions=permissions, permissions=permissions,
typeclass=typeclass, typeclass=typeclass,
home=home, home=home,
ip=ip, ip=ip,
) )
errors.extend(errs) errors.extend(errs)
return account, errors return account, errors
except Exception as e: except Exception as e:
# We are in the middle between logged in and -not, so we have # We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't, # to handle tracebacks ourselves at this point. If we don't,
@ -1454,7 +1454,7 @@ class DefaultGuest(DefaultAccount):
errors.append("An error occurred. Please e-mail an admin if the problem persists.") errors.append("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace() logger.log_trace()
return None, errors return None, errors
return account, errors return account, errors
def at_post_login(self, session=None, **kwargs): def at_post_login(self, session=None, **kwargs):

View file

@ -7,10 +7,8 @@ from unittest import TestCase
from django.test import override_settings from django.test import override_settings
from evennia.accounts.accounts import AccountSessionHandler from evennia.accounts.accounts import AccountSessionHandler
from evennia.accounts.accounts import DefaultAccount, DefaultGuest from evennia.accounts.accounts import DefaultAccount, DefaultGuest
from evennia.server.session import Session
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
from evennia.utils import create from evennia.utils import create
from evennia.utils.test_resources import EvenniaTest
from django.conf import settings from django.conf import settings
@ -61,78 +59,78 @@ class TestAccountSessionHandler(TestCase):
def test_count(self): def test_count(self):
"Check count method" "Check count method"
self.assertEqual(self.handler.count(), len(self.handler.get())) self.assertEqual(self.handler.count(), len(self.handler.get()))
class TestDefaultGuest(EvenniaTest): class TestDefaultGuest(EvenniaTest):
"Check DefaultGuest class" "Check DefaultGuest class"
ip = '212.216.134.22' ip = '212.216.134.22'
def test_authenticate(self): def test_authenticate(self):
# Guest account should not be permitted # Guest account should not be permitted
account, errors = DefaultGuest.authenticate(ip=self.ip) account, errors = DefaultGuest.authenticate(ip=self.ip)
self.assertFalse(account, 'Guest account was created despite being disabled.') self.assertFalse(account, 'Guest account was created despite being disabled.')
settings.GUEST_ENABLED = True settings.GUEST_ENABLED = True
settings.GUEST_LIST = ['bruce_wayne'] settings.GUEST_LIST = ['bruce_wayne']
# Create a guest account # Create a guest account
account, errors = DefaultGuest.authenticate(ip=self.ip) account, errors = DefaultGuest.authenticate(ip=self.ip)
self.assertTrue(account, 'Guest account should have been created.') self.assertTrue(account, 'Guest account should have been created.')
# Create a second guest account # Create a second guest account
account, errors = DefaultGuest.authenticate(ip=self.ip) account, errors = DefaultGuest.authenticate(ip=self.ip)
self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!') self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!')
settings.GUEST_ENABLED = False settings.GUEST_ENABLED = False
class TestDefaultAccountAuth(EvenniaTest): class TestDefaultAccountAuth(EvenniaTest):
def setUp(self): def setUp(self):
super(TestDefaultAccountAuth, self).setUp() super(TestDefaultAccountAuth, self).setUp()
self.password = "testpassword" self.password = "testpassword"
self.account.delete() self.account.delete()
self.account = create.create_account("TestAccount%s" % randint(100000, 999999), email="test@test.com", password=self.password, typeclass=DefaultAccount) self.account = create.create_account("TestAccount%s" % randint(100000, 999999), email="test@test.com", password=self.password, typeclass=DefaultAccount)
def test_authentication(self): def test_authentication(self):
"Confirm Account authentication method is authenticating/denying users." "Confirm Account authentication method is authenticating/denying users."
# Valid credentials # Valid credentials
obj, errors = DefaultAccount.authenticate(self.account.name, self.password) obj, errors = DefaultAccount.authenticate(self.account.name, self.password)
self.assertTrue(obj, 'Account did not authenticate given valid credentials.') self.assertTrue(obj, 'Account did not authenticate given valid credentials.')
# Invalid credentials # Invalid credentials
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): def test_create(self):
"Confirm Account creation is working as expected." "Confirm Account creation is working as expected."
# Create a normal account # Create a normal account
account, errors = DefaultAccount.create(username='ziggy', password='stardust11') account, errors = DefaultAccount.create(username='ziggy', password='stardust11')
self.assertTrue(account, 'New account should have been created.') self.assertTrue(account, 'New account should have been created.')
# Try creating a duplicate account # Try creating a duplicate account
account2, errors = DefaultAccount.create(username='Ziggy', password='starman11') account2, errors = DefaultAccount.create(username='Ziggy', password='starman11')
self.assertFalse(account2, 'Duplicate account name should not have been allowed.') self.assertFalse(account2, 'Duplicate account name should not have been allowed.')
account.delete() account.delete()
def test_throttle(self): def test_throttle(self):
"Confirm throttle activates on too many failures." "Confirm throttle activates on too many failures."
for x in xrange(20): for x in xrange(20):
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy', ip='12.24.36.48') 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.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.') 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
result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯') result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯')
self.assertFalse(result, "Validator allowed kanji in username.") self.assertFalse(result, "Validator allowed kanji in username.")
# Should not allow duplicate username # Should not allow duplicate username
result, error = DefaultAccount.validate_username(self.account.name) result, error = DefaultAccount.validate_username(self.account.name)
self.assertFalse(result, "Duplicate username should not have passed validation.") self.assertFalse(result, "Duplicate username should not have passed validation.")
# Should not allow username too short # Should not allow username too short
result, error = DefaultAccount.validate_username('xx') result, error = DefaultAccount.validate_username('xx')
self.assertFalse(result, "2-character username passed validation.") self.assertFalse(result, "2-character username passed validation.")
@ -277,17 +275,17 @@ class TestDefaultAccount(TestCase):
class TestAccountPuppetDeletion(EvenniaTest): class TestAccountPuppetDeletion(EvenniaTest):
@override_settings(MULTISESSION_MODE=2) @override_settings(MULTISESSION_MODE=2)
def test_puppet_deletion(self): def test_puppet_deletion(self):
# Check for existing chars # Check for existing chars
self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.') self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.')
# Add char1 to account's playable characters # Add char1 to account's playable characters
self.account.db._playable_characters.append(self.char1) self.account.db._playable_characters.append(self.char1)
self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.') self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.')
# See what happens when we delete char1. # See what happens when we delete char1.
self.char1.delete() self.char1.delete()
# Playable char list should be empty. # Playable char list should be empty.
self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters) self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters)

View file

@ -2,17 +2,11 @@
Commands that are available from the connect screen. Commands that are available from the connect screen.
""" """
import re import re
import time
import datetime import datetime
from django.conf import settings 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.comms.models import ChannelDB from evennia.comms.models import ChannelDB
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.utils import class_from_module, 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
@ -26,6 +20,7 @@ __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
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.
@ -40,10 +35,10 @@ def create_guest_account(session):
""" """
enabled = settings.GUEST_ENABLED enabled = settings.GUEST_ENABLED
address = session.address address = session.address
# Get account class # Get account class
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS) Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
# Get an available guest account # Get an available guest account
# authenticate() handles its own throttling # authenticate() handles its own throttling
account, errors = Guest.authenticate(ip=address) account, errors = Guest.authenticate(ip=address)
@ -53,6 +48,7 @@ def create_guest_account(session):
session.msg("|R%s|n" % '\n'.join(errors)) session.msg("|R%s|n" % '\n'.join(errors))
return enabled, None return enabled, None
def create_normal_account(session, name, password): def create_normal_account(session, name, password):
""" """
Creates an account with the given name and password. Creates an account with the given name and password.
@ -67,9 +63,9 @@ def create_normal_account(session, name, password):
""" """
# Get account class # Get account class
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
address = session.address address = session.address
# Match account name and check password # Match account name and check password
# authenticate() handles all its own throttling # authenticate() handles all its own throttling
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session) account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
@ -108,19 +104,19 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
""" """
session = self.caller session = self.caller
address = session.address address = session.address
args = self.args args = self.args
# extract double quote parts # extract double quote 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()]
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":
# Get Guest typeclass # Get Guest typeclass
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS) Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
account, errors = Guest.authenticate(ip=address) account, errors = Guest.authenticate(ip=address)
if account: if account:
session.sessionhandler.login(session, account) session.sessionhandler.login(session, account)
@ -128,14 +124,14 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
else: else:
session.msg("|R%s|n" % '\n'.join(errors)) session.msg("|R%s|n" % '\n'.join(errors))
return 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
# Get account class # Get account class
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
name, password = parts name, password = parts
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session) account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
if account: if account:
@ -168,7 +164,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
args = self.args.strip() args = self.args.strip()
address = session.address address = session.address
# Get account class # Get account class
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS) Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
@ -182,7 +178,7 @@ 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
username, password = parts username, password = parts
# everything's ok. Create the new account account. # everything's ok. Create the new account account.

View file

@ -195,7 +195,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
# lockstring of newly created objects, for easy overloading. # lockstring of newly created objects, for easy overloading.
# Will be formatted with the appropriate attributes. # Will be formatted with the appropriate attributes.
lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)" lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)"
objects = ObjectManager() objects = ObjectManager()
# on-object properties # on-object properties
@ -863,66 +863,66 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
string = "This place should not exist ... contact an admin." string = "This place should not exist ... contact an admin."
obj.msg(_(string)) obj.msg(_(string))
obj.move_to(home) obj.move_to(home)
@classmethod @classmethod
def create(cls, key, account=None, **kwargs): def create(cls, key, account=None, **kwargs):
""" """
Creates a basic object with default parameters, unless otherwise Creates a basic object with default parameters, unless otherwise
specified or extended. specified or extended.
Provides a friendlier interface to the utils.create_object() function. Provides a friendlier interface to the utils.create_object() function.
Args: Args:
key (str): Name of the new object. key (str): Name of the new object.
account (Account): Account to attribute this object to. account (Account): Account to attribute this object to.
Kwargs: Kwargs:
description (str): Brief description for this object. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
Returns: Returns:
object (Object): A newly created object of the given typeclass. object (Object): A newly created object of the given typeclass.
errors (list): A list of errors in string form, if any. errors (list): A list of errors in string form, if any.
""" """
errors = [] errors = []
obj = None obj = None
# Get IP address of creator, if available # Get IP address of creator, if available
ip = kwargs.pop('ip', '') ip = kwargs.pop('ip', '')
# If no typeclass supplied, use this class # If no typeclass supplied, use this class
kwargs['typeclass'] = kwargs.pop('typeclass', cls) kwargs['typeclass'] = kwargs.pop('typeclass', cls)
# Set the supplied key as the name of the intended object # Set the supplied key as the name of the intended object
kwargs['key'] = key kwargs['key'] = key
# Get a supplied description, if any # Get a supplied description, if any
description = kwargs.pop('description', '') description = kwargs.pop('description', '')
# Create a sane lockstring if one wasn't supplied # Create a sane lockstring if one wasn't supplied
lockstring = kwargs.get('locks') lockstring = kwargs.get('locks')
if account and not lockstring: if account and not lockstring:
lockstring = cls.lockstring.format(account_id=account.id) lockstring = cls.lockstring.format(account_id=account.id)
kwargs['locks'] = lockstring kwargs['locks'] = lockstring
# Create object # Create object
try: try:
obj = create.create_object(**kwargs) obj = create.create_object(**kwargs)
# Record creator id and creation IP # Record creator id and creation IP
if ip: obj.db.creator_ip = ip if ip: obj.db.creator_ip = ip
if account: obj.db.creator_id = account.id if account: obj.db.creator_id = account.id
# Set description if there is none, or update it if provided # Set description if there is none, or update it if provided
if description or not obj.db.desc: if description or not obj.db.desc:
desc = description if description else "You see nothing special." desc = description if description else "You see nothing special."
obj.db.desc = desc obj.db.desc = desc
except Exception as e: except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key) errors.append("An error occurred while creating this '%s' object." % key)
logger.log_err(e) logger.log_err(e)
return obj, errors return obj, errors
def copy(self, new_key=None): def copy(self, new_key=None):
@ -1895,81 +1895,81 @@ class DefaultCharacter(DefaultObject):
# lockstring of newly created rooms, for easy overloading. # lockstring of newly created rooms, for easy overloading.
# Will be formatted with the appropriate attributes. # Will be formatted with the appropriate attributes.
lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer)" lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer)"
@classmethod @classmethod
def create(cls, key, account, **kwargs): def create(cls, key, account, **kwargs):
""" """
Creates a basic Character with default parameters, unless otherwise Creates a basic Character with default parameters, unless otherwise
specified or extended. specified or extended.
Provides a friendlier interface to the utils.create_character() function. Provides a friendlier interface to the utils.create_character() function.
Args: Args:
key (str): Name of the new Character. key (str): Name of the new Character.
account (obj): Account to associate this Character with. Required as account (obj): Account to associate this Character with. Required as
an argument, but one can fake it out by supplying None-- it will an argument, but one can fake it out by supplying None-- it will
change the default lockset and skip creator attribution. change the default lockset and skip creator attribution.
Kwargs: Kwargs:
description (str): Brief description for this object. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
Returns: Returns:
character (Object): A newly created Character of the given typeclass. character (Object): A newly created Character of the given typeclass.
errors (list): A list of errors in string form, if any. errors (list): A list of errors in string form, if any.
""" """
errors = [] errors = []
obj = None obj = None
# Get IP address of creator, if available # Get IP address of creator, if available
ip = kwargs.pop('ip', '') ip = kwargs.pop('ip', '')
# If no typeclass supplied, use this class # If no typeclass supplied, use this class
kwargs['typeclass'] = kwargs.pop('typeclass', cls) kwargs['typeclass'] = kwargs.pop('typeclass', cls)
# Set the supplied key as the name of the intended object # Set the supplied key as the name of the intended object
kwargs['key'] = key kwargs['key'] = key
# Get home for character # Get home for character
kwargs['home'] = ObjectDB.objects.get_id(kwargs.get('home', settings.DEFAULT_HOME)) kwargs['home'] = ObjectDB.objects.get_id(kwargs.get('home', settings.DEFAULT_HOME))
# Get permissions # Get permissions
kwargs['permissions'] = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT) kwargs['permissions'] = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
# Get description if provided # Get description if provided
description = kwargs.pop('description', '') description = kwargs.pop('description', '')
# Get locks if provided # Get locks if provided
locks = kwargs.pop('locks', '') locks = kwargs.pop('locks', '')
try: try:
# Create the Character # Create the Character
obj = create.create_object(**kwargs) obj = create.create_object(**kwargs)
# Record creator id and creation IP # Record creator id and creation IP
if ip: obj.db.creator_ip = ip if ip: obj.db.creator_ip = ip
if account: obj.db.creator_id = account.id if account: obj.db.creator_id = account.id
# Add locks # Add locks
if not locks and account: if not locks and account:
# Allow only the character itself and the creator account to puppet this character (and Developers). # Allow only the character itself and the creator account to puppet this character (and Developers).
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': account.id}) locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': account.id})
elif not locks and not account: elif not locks and not account:
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': -1}) locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': -1})
obj.locks.add(locks) obj.locks.add(locks)
# If no description is set, set a default description # If no description is set, set a default description
if description or not obj.db.desc: if description or not obj.db.desc:
obj.db.desc = description if description else "This is a character." obj.db.desc = description if description else "This is a character."
except Exception as e: except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key) errors.append("An error occurred while creating this '%s' object." % key)
logger.log_err(e) logger.log_err(e)
return obj, errors return obj, errors
def basetype_setup(self): def basetype_setup(self):
""" """
Setup character-specific security. Setup character-specific security.
@ -2097,60 +2097,60 @@ class DefaultRoom(DefaultObject):
""" """
Creates a basic Room with default parameters, unless otherwise Creates a basic Room with default parameters, unless otherwise
specified or extended. specified or extended.
Provides a friendlier interface to the utils.create_object() function. Provides a friendlier interface to the utils.create_object() function.
Args: Args:
key (str): Name of the new Room. key (str): Name of the new Room.
account (obj): Account to associate this Room with. account (obj): Account to associate this Room with.
Kwargs: Kwargs:
description (str): Brief description for this object. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
Returns: Returns:
room (Object): A newly created Room of the given typeclass. room (Object): A newly created Room of the given typeclass.
errors (list): A list of errors in string form, if any. errors (list): A list of errors in string form, if any.
""" """
errors = [] errors = []
obj = None obj = None
# Get IP address of creator, if available # Get IP address of creator, if available
ip = kwargs.pop('ip', '') ip = kwargs.pop('ip', '')
# If no typeclass supplied, use this class # If no typeclass supplied, use this class
kwargs['typeclass'] = kwargs.pop('typeclass', cls) kwargs['typeclass'] = kwargs.pop('typeclass', cls)
# Set the supplied key as the name of the intended object # Set the supplied key as the name of the intended object
kwargs['key'] = key kwargs['key'] = key
# Get who to send errors to # Get who to send errors to
kwargs['report_to'] = kwargs.pop('report_to', account) kwargs['report_to'] = kwargs.pop('report_to', account)
# Get description, if provided # Get description, if provided
description = kwargs.pop('description', '') description = kwargs.pop('description', '')
try: try:
# Create the Room # Create the Room
obj = create.create_object(**kwargs) obj = create.create_object(**kwargs)
# Set appropriate locks # Set appropriate locks
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id)) lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
obj.locks.add(lockstring) obj.locks.add(lockstring)
# Record creator id and creation IP # Record creator id and creation IP
if ip: obj.db.creator_ip = ip if ip: obj.db.creator_ip = ip
if account: obj.db.creator_id = account.id if account: obj.db.creator_id = account.id
# If no description is set, set a default description # If no description is set, set a default description
if description or not obj.db.desc: if description or not obj.db.desc:
obj.db.desc = description if description else "This is a room." obj.db.desc = description if description else "This is a room."
except Exception as e: except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key) errors.append("An error occurred while creating this '%s' object." % key)
logger.log_err(e) logger.log_err(e)
return obj, errors return obj, errors
def basetype_setup(self): def basetype_setup(self):
@ -2230,13 +2230,13 @@ class DefaultExit(DefaultObject):
exit_command = ExitCommand exit_command = ExitCommand
priority = 101 priority = 101
# lockstring of newly created exits, for easy overloading. # lockstring of newly created exits, for easy overloading.
# Will be formatted with the {id} of the creating object. # Will be formatted with the {id} of the creating object.
lockstring = "control:id({id}) or perm(Admin); " \ lockstring = "control:id({id}) or perm(Admin); " \
"delete:id({id}) or perm(Admin); " \ "delete:id({id}) or perm(Admin); " \
"edit:id({id}) or perm(Admin)" "edit:id({id}) or perm(Admin)"
# Helper classes and methods to implement the Exit. These need not # Helper classes and methods to implement the Exit. These need not
# be overloaded unless one want to change the foundation for how # be overloaded unless one want to change the foundation for how
# Exits work. See the end of the class for hook methods to overload. # Exits work. See the end of the class for hook methods to overload.
@ -2274,72 +2274,72 @@ class DefaultExit(DefaultObject):
return exit_cmdset return exit_cmdset
# Command hooks # Command hooks
@classmethod @classmethod
def create(cls, key, account, source, dest, **kwargs): def create(cls, key, account, source, dest, **kwargs):
""" """
Creates a basic Exit with default parameters, unless otherwise Creates a basic Exit with default parameters, unless otherwise
specified or extended. specified or extended.
Provides a friendlier interface to the utils.create_object() function. Provides a friendlier interface to the utils.create_object() function.
Args: Args:
key (str): Name of the new Exit, as it should appear from the key (str): Name of the new Exit, as it should appear from the
source room. source room.
account (obj): Account to associate this Exit with. account (obj): Account to associate this Exit with.
source (Room): The room to create this exit in. source (Room): The room to create this exit in.
dest (Room): The room to which this exit should go. dest (Room): The room to which this exit should go.
Kwargs: Kwargs:
description (str): Brief description for this object. description (str): Brief description for this object.
ip (str): IP address of creator (for object auditing). ip (str): IP address of creator (for object auditing).
Returns: Returns:
exit (Object): A newly created Room of the given typeclass. exit (Object): A newly created Room of the given typeclass.
errors (list): A list of errors in string form, if any. errors (list): A list of errors in string form, if any.
""" """
errors = [] errors = []
obj = None obj = None
# Get IP address of creator, if available # Get IP address of creator, if available
ip = kwargs.pop('ip', '') ip = kwargs.pop('ip', '')
# If no typeclass supplied, use this class # If no typeclass supplied, use this class
kwargs['typeclass'] = kwargs.pop('typeclass', cls) kwargs['typeclass'] = kwargs.pop('typeclass', cls)
# Set the supplied key as the name of the intended object # Set the supplied key as the name of the intended object
kwargs['key'] = key kwargs['key'] = key
# Get who to send errors to # Get who to send errors to
kwargs['report_to'] = kwargs.pop('report_to', account) kwargs['report_to'] = kwargs.pop('report_to', account)
# Set to/from rooms # Set to/from rooms
kwargs['location'] = source kwargs['location'] = source
kwargs['destination'] = dest kwargs['destination'] = dest
description = kwargs.pop('description', '') description = kwargs.pop('description', '')
try: try:
# Create the Exit # Create the Exit
obj = create.create_object(**kwargs) obj = create.create_object(**kwargs)
# Set appropriate locks # Set appropriate locks
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id)) lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
obj.locks.add(lockstring) obj.locks.add(lockstring)
# Record creator id and creation IP # Record creator id and creation IP
if ip: obj.db.creator_ip = ip if ip: obj.db.creator_ip = ip
if account: obj.db.creator_id = account.id if account: obj.db.creator_id = account.id
# If no description is set, set a default description # If no description is set, set a default description
if description or not obj.db.desc: if description or not obj.db.desc:
obj.db.desc = description if description else "This is an exit." obj.db.desc = description if description else "This is an exit."
except Exception as e: except Exception as e:
errors.append("An error occurred while creating this '%s' object." % key) errors.append("An error occurred while creating this '%s' object." % key)
logger.log_err(e) logger.log_err(e)
return obj, errors return obj, errors
def basetype_setup(self): def basetype_setup(self):

View file

@ -1,10 +1,11 @@
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
class DefaultObjectTest(EvenniaTest): class DefaultObjectTest(EvenniaTest):
ip = '212.216.139.14' ip = '212.216.139.14'
def test_object_create(self): def test_object_create(self):
description = 'A home for a grouch.' description = 'A home for a grouch.'
obj, errors = DefaultObject.create('trashcan', self.account, description=description, ip=self.ip) obj, errors = DefaultObject.create('trashcan', self.account, description=description, ip=self.ip)
@ -12,7 +13,7 @@ class DefaultObjectTest(EvenniaTest):
self.assertFalse(errors, errors) self.assertFalse(errors, errors)
self.assertEqual(description, obj.db.desc) self.assertEqual(description, obj.db.desc)
self.assertEqual(obj.db.creator_ip, self.ip) self.assertEqual(obj.db.creator_ip, self.ip)
def test_character_create(self): def test_character_create(self):
description = 'A furry green monster, reeking of garbage.' description = 'A furry green monster, reeking of garbage.'
obj, errors = DefaultCharacter.create('oscar', self.account, description=description, ip=self.ip) obj, errors = DefaultCharacter.create('oscar', self.account, description=description, ip=self.ip)
@ -20,7 +21,7 @@ class DefaultObjectTest(EvenniaTest):
self.assertFalse(errors, errors) self.assertFalse(errors, errors)
self.assertEqual(description, obj.db.desc) self.assertEqual(description, obj.db.desc)
self.assertEqual(obj.db.creator_ip, self.ip) self.assertEqual(obj.db.creator_ip, self.ip)
def test_room_create(self): def test_room_create(self):
description = 'A dimly-lit alley behind the local Chinese restaurant.' description = 'A dimly-lit alley behind the local Chinese restaurant.'
obj, errors = DefaultRoom.create('alley', self.account, description=description, ip=self.ip) obj, errors = DefaultRoom.create('alley', self.account, description=description, ip=self.ip)
@ -28,7 +29,7 @@ class DefaultObjectTest(EvenniaTest):
self.assertFalse(errors, errors) self.assertFalse(errors, errors)
self.assertEqual(description, obj.db.desc) self.assertEqual(description, obj.db.desc)
self.assertEqual(obj.db.creator_ip, self.ip) self.assertEqual(obj.db.creator_ip, self.ip)
def test_exit_create(self): def test_exit_create(self):
description = 'The steaming depths of the dumpster, ripe with refuse in various states of decomposition.' description = 'The steaming depths of the dumpster, ripe with refuse in various states of decomposition.'
obj, errors = DefaultExit.create('in', self.account, self.room1, self.room2, description=description, ip=self.ip) obj, errors = DefaultExit.create('in', self.account, self.room1, self.room2, description=description, ip=self.ip)
@ -43,4 +44,4 @@ class DefaultObjectTest(EvenniaTest):
self.assertTrue('admin' in self.char1.web_get_admin_url()) self.assertTrue('admin' in self.char1.web_get_admin_url())
self.assertTrue(self.room1.get_absolute_url()) self.assertTrue(self.room1.get_absolute_url())
self.assertTrue('admin' in self.room1.web_get_admin_url()) self.assertTrue('admin' in self.room1.web_get_admin_url())

View file

@ -323,31 +323,31 @@ class DefaultScript(ScriptBase):
or describe a state that changes under certain conditions. or describe a state that changes under certain conditions.
""" """
@classmethod @classmethod
def create(cls, key, **kwargs): def create(cls, key, **kwargs):
""" """
Provides a passthrough interface to the utils.create_script() function. Provides a passthrough interface to the utils.create_script() function.
Args: Args:
key (str): Name of the new object. key (str): Name of the new object.
Returns: Returns:
object (Object): A newly created object of the given typeclass. object (Object): A newly created object of the given typeclass.
errors (list): A list of errors in string form, if any. errors (list): A list of errors in string form, if any.
""" """
errors = [] errors = []
obj = None obj = None
kwargs['key'] = key kwargs['key'] = key
try: try:
obj = create.create_script(**kwargs) obj = create.create_script(**kwargs)
except Exception as e: except Exception as e:
errors.append("The script '%s' encountered errors and could not be created." % key) errors.append("The script '%s' encountered errors and could not be created." % key)
logger.log_err(e) logger.log_err(e)
return obj, errors return obj, errors
def at_script_creation(self): def at_script_creation(self):

View file

@ -2,25 +2,26 @@ from collections import defaultdict, deque
from evennia.utils import logger from evennia.utils import logger
import time import time
class Throttle(object): class Throttle(object):
""" """
Keeps a running count of failed actions per IP address. Keeps a running count of failed actions per IP address.
Available methods indicate whether or not the number of failures exceeds a Available methods indicate whether or not the number of failures exceeds a
particular threshold. particular threshold.
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 removes sparse keys when
no recent failures have been recorded. no recent failures have been recorded.
""" """
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.'
def __init__(self, **kwargs): def __init__(self, **kwargs):
""" """
Allows setting of throttle parameters. Allows setting of throttle parameters.
Kwargs: Kwargs:
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
@ -32,67 +33,67 @@ class Throttle(object):
self.storage = defaultdict(deque) self.storage = defaultdict(deque)
self.cache_size = self.limit = kwargs.get('limit', 5) self.cache_size = self.limit = kwargs.get('limit', 5)
self.timeout = kwargs.get('timeout', 5 * 60) self.timeout = kwargs.get('timeout', 5 * 60)
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.
Args: Args:
ip (str, optional): IP address of requestor ip (str, optional): IP address of requestor
Returns: Returns:
storage (dict): When no IP is provided, returns a dict of all storage (dict): When no IP is provided, returns a dict of all
current IPs being tracked and the timestamps of their recent current IPs being tracked and the timestamps of their recent
failures. failures.
timestamps (deque): When an IP is provided, returns a deque of timestamps (deque): When an IP is provided, returns a deque of
timestamps of recent failures only for that IP. timestamps of recent failures only for that IP.
""" """
if ip: return self.storage.get(ip, deque(maxlen=self.cache_size)) if ip: return self.storage.get(ip, deque(maxlen=self.cache_size))
else: return self.storage else: return self.storage
def update(self, ip, failmsg='Exceeded threshold.'): def update(self, ip, failmsg='Exceeded threshold.'):
""" """
Store the time of the latest failure. Store the time of the latest failure.
Args: Args:
ip (str): IP address of requestor ip (str): IP address of requestor
failmsg (str, optional): Message to display in logs upon activation failmsg (str, optional): Message to display in logs upon activation
of throttle. of throttle.
Returns: Returns:
None None
""" """
# Get current status # Get current status
previously_throttled = self.check(ip) previously_throttled = self.check(ip)
# Enforce length limits # Enforce length limits
if not self.storage[ip].maxlen: if not self.storage[ip].maxlen:
self.storage[ip] = deque(maxlen=self.cache_size) self.storage[ip] = deque(maxlen=self.cache_size)
self.storage[ip].append(time.time()) self.storage[ip].append(time.time())
# 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('Throttle Activated: %s (IP: %s, %i hits in %i seconds.)' % (failmsg, ip, self.limit, self.timeout)) logger.log_sec('Throttle Activated: %s (IP: %s, %i hits in %i seconds.)' % (failmsg, ip, self.limit, self.timeout))
def check(self, ip): def check(self, ip):
""" """
This will check the session's address against the This will check the session's address against the
storage dictionary to check they haven't spammed too many storage dictionary to check they haven't spammed too many
fails recently. fails recently.
Args: Args:
ip (str): IP address of requestor ip (str): IP address of requestor
Returns: Returns:
throttled (bool): True if throttling is active, throttled (bool): True if throttling is active,
False otherwise. False otherwise.
""" """
now = time.time() now = time.time()
ip = str(ip) ip = str(ip)
@ -110,5 +111,3 @@ class Throttle(object):
return False return False
else: else:
return False return False

View file

@ -4,31 +4,32 @@ from django.utils.translation import gettext as _
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
import re import re
class EvenniaUsernameAvailabilityValidator: class EvenniaUsernameAvailabilityValidator:
""" """
Checks to make sure a given username is not taken or otherwise reserved. Checks to make sure a given username is not taken or otherwise reserved.
""" """
def __call__(self, username): def __call__(self, username):
""" """
Validates a username to make sure it is not in use or reserved. Validates a username to make sure it is not in use or reserved.
Args: Args:
username (str): Username to validate username (str): Username to validate
Returns: Returns:
None (None): None if password successfully validated, None (None): None if password successfully validated,
raises ValidationError otherwise. raises ValidationError otherwise.
""" """
# 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',
) )
# Check database # Check database
exists = AccountDB.objects.filter(username__iexact=username).exists() exists = AccountDB.objects.filter(username__iexact=username).exists()
if exists: if exists:
@ -37,33 +38,36 @@ class EvenniaUsernameAvailabilityValidator:
code='evennia_username_taken', code='evennia_username_taken',
) )
class EvenniaPasswordValidator: class EvenniaPasswordValidator:
def __init__(self, regex=r"^[\w. @+\-',]+$", policy="Password should contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."): def __init__(self, regex=r"^[\w. @+\-',]+$",
policy="Password should contain a mix of letters, "
"spaces, digits and @/./+/-/_/'/, only."):
""" """
Constructs a standard Django password validator. Constructs a standard Django password validator.
Args: Args:
regex (str): Regex pattern of valid characters to allow. regex (str): Regex pattern of valid characters to allow.
policy (str): Brief explanation of what the defined regex permits. policy (str): Brief explanation of what the defined regex permits.
""" """
self.regex = regex self.regex = regex
self.policy = policy self.policy = policy
def validate(self, password, user=None): def validate(self, password, user=None):
""" """
Validates a password string to make sure it meets predefined Evennia Validates a password string to make sure it meets predefined Evennia
acceptable character policy. acceptable character policy.
Args: Args:
password (str): Password to validate password (str): Password to validate
user (None): Unused argument but required by Django user (None): Unused argument but required by Django
Returns: Returns:
None (None): None if password successfully validated, None (None): None if password successfully validated,
raises ValidationError otherwise. raises ValidationError otherwise.
""" """
# Check complexity # Check complexity
if not re.findall(self.regex, password): if not re.findall(self.regex, password):
@ -76,11 +80,12 @@ class EvenniaPasswordValidator:
""" """
Returns a user-facing explanation of the password policy defined Returns a user-facing explanation of the password policy defined
by this validator. by this validator.
Returns: Returns:
text (str): Explanation of password policy. text (str): Explanation of password policy.
""" """
return _( return _(
"%s From a terminal client, you can also use a phrase of multiple words if you enclose the password in double quotes." % self.policy "%s From a terminal client, you can also use a phrase of multiple words if "
) "you enclose the password in double quotes." % self.policy
)

View file

@ -222,7 +222,8 @@ COMMAND_RATE_WARNING = "You entered commands too fast. Wait a moment and try aga
# 0 or less. # 0 or less.
MAX_CHAR_LIMIT = 6000 MAX_CHAR_LIMIT = 6000
# The warning to echo back to users if they enter a very large string # The warning to echo back to users if they enter a very large string
MAX_CHAR_LIMIT_WARNING = "You entered a string that was too long. Please break it up into multiple parts." MAX_CHAR_LIMIT_WARNING = ("You entered a string that was too long. "
"Please break it up into multiple parts.")
# If this is true, errors and tracebacks from the engine will be # If this is true, errors and tracebacks from the engine will be
# echoed as text in-game as well as to the log. This can speed up # echoed as text in-game as well as to the log. This can speed up
# debugging. OBS: Showing full tracebacks to regular users could be a # debugging. OBS: Showing full tracebacks to regular users could be a
@ -410,12 +411,14 @@ CMDSET_CHARACTER = "commands.default_cmdsets.CharacterCmdSet"
CMDSET_ACCOUNT = "commands.default_cmdsets.AccountCmdSet" CMDSET_ACCOUNT = "commands.default_cmdsets.AccountCmdSet"
# Location to search for cmdsets if full path not given # Location to search for cmdsets if full path not given
CMDSET_PATHS = ["commands", "evennia", "contribs"] CMDSET_PATHS = ["commands", "evennia", "contribs"]
# Fallbacks for cmdset paths that fail to load. Note that if you change the path for your default cmdsets, # Fallbacks for cmdset paths that fail to load. Note that if you change the path for your
# you will also need to copy CMDSET_FALLBACKS after your change in your settings file for it to detect the change. # default cmdsets, you will also need to copy CMDSET_FALLBACKS after your change in your
CMDSET_FALLBACKS = {CMDSET_CHARACTER: 'evennia.commands.default.cmdset_character.CharacterCmdSet', # settings file for it to detect the change.
CMDSET_ACCOUNT: 'evennia.commands.default.cmdset_account.AccountCmdSet', CMDSET_FALLBACKS = {
CMDSET_SESSION: 'evennia.commands.default.cmdset_session.SessionCmdSet', CMDSET_CHARACTER: 'evennia.commands.default.cmdset_character.CharacterCmdSet',
CMDSET_UNLOGGEDIN: 'evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet'} CMDSET_ACCOUNT: 'evennia.commands.default.cmdset_account.AccountCmdSet',
CMDSET_SESSION: 'evennia.commands.default.cmdset_session.SessionCmdSet',
CMDSET_UNLOGGEDIN: 'evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet'}
# Parent class for all default commands. Changing this class will # Parent class for all default commands. Changing this class will
# modify all default commands, so do so carefully. # modify all default commands, so do so carefully.
COMMAND_DEFAULT_CLASS = "evennia.commands.default.muxcommand.MuxCommand" COMMAND_DEFAULT_CLASS = "evennia.commands.default.muxcommand.MuxCommand"
@ -810,7 +813,7 @@ AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
{'NAME': 'evennia.server.validators.EvenniaPasswordValidator'}] {'NAME': 'evennia.server.validators.EvenniaPasswordValidator'}]
# Username validation plugins # Username validation plugins
AUTH_USERNAME_VALIDATORS = [ AUTH_USERNAME_VALIDATORS = [
{'NAME': 'django.contrib.auth.validators.ASCIIUsernameValidator'}, {'NAME': 'django.contrib.auth.validators.ASCIIUsernameValidator'},
@ -830,7 +833,7 @@ TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
# Django extesions are useful third-party tools that are not # Django extesions are useful third-party tools that are not
# always included in the default django distro. # always included in the default django distro.
try: try:
import django_extensions import django_extensions # noqa
INSTALLED_APPS = INSTALLED_APPS + ('django_extensions',) INSTALLED_APPS = INSTALLED_APPS + ('django_extensions',)
except ImportError: except ImportError:
# Django extensions are not installed in all distros. # Django extensions are not installed in all distros.