diff --git a/axes/__init__.py b/axes/__init__.py index df00547..498db06 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,4 +1,30 @@ -VERSION = (0, 1, 0, 'pre') +VERSION = (0, 1, 1, 'alpha') def get_version(): - return '%s.%s.%s-%s' % VERSION \ No newline at end of file + return '%s.%s.%s-%s' % VERSION + +try: + from django.conf import settings + import logging, os + + LOGFILE = os.path.join(settings.DIRNAME, 'axes.log') + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + datefmt='%a, %d %b %Y %H:%M:%S', + filename=LOGFILE, + filemode='w') + + fileLog = logging.FileHandler(LOGFILE, 'w') + fileLog.setLevel(logging.DEBUG) + + # set a format which is simpler for console use + formatter = logging.Formatter('%(asctime)s %(name)-12s: %(levelname)-8s %(message)s') + + # tell the handler to use this format + fileLog.setFormatter(formatter) + + # add the handler to the root logger + logging.getLogger('').addHandler(fileLog) +except: + # if we have any problems, we most likely don't have a settings module loaded + pass \ No newline at end of file diff --git a/axes/decorators.py b/axes/decorators.py index 3d86e60..c244c53 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -1,5 +1,7 @@ -from axes.models import AccessAttempt from django.conf import settings +from axes.models import AccessAttempt +import axes +import logging # see if the user has overridden the failure limit if hasattr(settings, 'LOGIN_FAILURE_LIMIT'): @@ -16,21 +18,38 @@ else: def query2str(items): return '\n'.join(['%s=%s' % (k, v) for k,v in items]) +log = logging.getLogger('axes.watch_login') +log.info('BEGIN LOG') +log.info('Using django-axes ' + axes.get_version()) + def watch_login(func, failures): """ Used to decorate the django.contrib.admin.site.login method. """ - def new(*args, **kwargs): - request = args[0] + def decorated_login(request, *args, **kwargs): + # share some useful information + if func.__name__ != 'decorated_login': + log.info('Calling decorated function: %s' % func) + if args: log.info('args: %s' % args) + if kwargs: log.info('kwargs: %s' % kwargs) # call the login function - response = func(*args, **kwargs) + response = func(request, *args, **kwargs) + + if func.__name__ == 'decorated_login': + # if we're dealing with this function itself, don't bother checking + # for invalid login attempts. I suppose there's a bunch of + # recursion going on here that used to cause one failed login + # attempt to generate 10+ failed access attempt records (with 3 + # failed attempts each supposedly) + return response # only check when there's been an HTTP POST if request.method == 'POST': # see if the login was successful - if not response.has_header('location') and response.status_code != 302: + if response and not response.has_header('location') and response.status_code != 302: + log.debug('Failure dict (begin): %s' % failures) ip = request.META.get('REMOTE_ADDR', '') ua = request.META.get('HTTP_USER_AGENT', '') @@ -39,15 +58,23 @@ def watch_login(func, failures): # make sure we have an item for this key try: failures[key] + log.debug('Key %s exists' % key) except KeyError: + log.debug('Creating key %s' % key) failures[key] = 0 # add a failed attempt for this user failures[key] += 1 + log.info('Adding a failure for %s; %i failure(s)' % (key, failures[key])) + #log.debug('Request: %s' % request) + # if we reach or surpass the failure limit, create an # AccessAttempt record if failures[key] >= FAILURE_LIMIT: + log.info('=================================') + log.info('Creating access attempt record...') + log.info('=================================') attempt = AccessAttempt.objects.create( user_agent=ua, ip_address=ip, @@ -61,5 +88,8 @@ def watch_login(func, failures): if FAILURE_RESET: del(failures[key]) + log.debug('Failure dict (end): %s' % failures) + log.info('-' * 79) + return response - return new \ No newline at end of file + return decorated_login \ No newline at end of file