2014-12-31 22:00:45 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
from django.http import HttpResponse
|
|
|
|
|
from django.http import HttpResponseRedirect
|
|
|
|
|
from django.shortcuts import render_to_response
|
|
|
|
|
from django.template import RequestContext
|
2015-02-25 03:02:06 +00:00
|
|
|
from django.core.validators import validate_ipv46_address
|
|
|
|
|
from django.core.exceptions import ValidationError
|
2014-12-31 22:00:45 +00:00
|
|
|
|
2015-01-01 17:51:46 +00:00
|
|
|
from .connection import get_redis_connection
|
|
|
|
|
from . import config
|
2015-01-03 21:33:51 +00:00
|
|
|
from .data import store_login_attempt
|
2015-01-01 00:27:18 +00:00
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
REDIS_SERVER = get_redis_connection()
|
2014-12-31 22:00:45 +00:00
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
LOG = logging.getLogger(__name__)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_valid_ip(ip_address):
|
|
|
|
|
""" Check Validity of an IP address """
|
2015-02-24 23:16:08 +00:00
|
|
|
if not ip_address:
|
|
|
|
|
return False
|
|
|
|
|
ip_address = ip_address.strip()
|
2015-02-25 03:02:06 +00:00
|
|
|
try:
|
|
|
|
|
validate_ipv46_address(ip_address)
|
2015-02-24 23:16:08 +00:00
|
|
|
return True
|
2015-02-25 03:02:06 +00:00
|
|
|
except ValidationError:
|
|
|
|
|
return False
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_ip_address_from_request(request):
|
|
|
|
|
""" Makes the best attempt to get the client's real IP or return
|
|
|
|
|
the loopback """
|
2015-02-24 23:16:08 +00:00
|
|
|
remote_addr = request.META.get('REMOTE_ADDR', '')
|
|
|
|
|
if remote_addr and is_valid_ip(remote_addr):
|
2015-03-20 15:09:45 +00:00
|
|
|
return remote_addr.strip()
|
2015-02-24 23:16:08 +00:00
|
|
|
return '127.0.0.1'
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_ip(request):
|
|
|
|
|
""" get the ip address from the request """
|
2015-02-24 23:16:08 +00:00
|
|
|
if config.BEHIND_REVERSE_PROXY:
|
2015-03-20 15:09:45 +00:00
|
|
|
ip_address = request.META.get(config.REVERSE_PROXY_HEADER, '')
|
|
|
|
|
ip_address = ip_address.split(",", 1)[0].strip()
|
|
|
|
|
if ip_address == '':
|
|
|
|
|
ip_address = get_ip_address_from_request(request)
|
2015-02-24 23:16:08 +00:00
|
|
|
else:
|
2015-03-20 15:09:45 +00:00
|
|
|
ip_address = get_ip_address_from_request(request)
|
|
|
|
|
return ip_address
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
def get_ip_attempt_cache_key(ip_address):
|
2014-12-31 22:00:45 +00:00
|
|
|
""" get the cache key by ip """
|
2015-03-20 15:09:45 +00:00
|
|
|
return "{0}:failed:ip:{1}".format(config.CACHE_PREFIX, ip_address)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_username_attempt_cache_key(username):
|
|
|
|
|
""" get the cache key by username """
|
2015-01-01 17:51:46 +00:00
|
|
|
return "{0}:failed:username:{1}".format(config.CACHE_PREFIX, username)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
def get_ip_blocked_cache_key(ip_address):
|
2014-12-31 22:00:45 +00:00
|
|
|
""" get the cache key by ip """
|
2015-03-20 15:09:45 +00:00
|
|
|
return "{0}:blocked:ip:{1}".format(config.CACHE_PREFIX, ip_address)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_username_blocked_cache_key(username):
|
|
|
|
|
""" get the cache key by username """
|
2015-01-01 17:51:46 +00:00
|
|
|
return "{0}:blocked:username:{1}".format(config.CACHE_PREFIX, username)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
2015-01-27 23:06:56 +00:00
|
|
|
def strip_keys(key_list):
|
|
|
|
|
""" Given a list of keys, remove the prefix and remove just
|
|
|
|
|
the data we care about.
|
|
|
|
|
|
|
|
|
|
for example:
|
|
|
|
|
|
|
|
|
|
['defender:blocked:ip:ken', 'defender:blocked:ip:joffrey']
|
|
|
|
|
|
|
|
|
|
would result in:
|
|
|
|
|
|
|
|
|
|
['ken', 'joffrey']
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
return [key.split(":")[-1] for key in key_list]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_blocked_ips():
|
|
|
|
|
""" get a list of blocked ips from redis """
|
|
|
|
|
key = get_ip_blocked_cache_key("*")
|
2015-06-29 10:33:33 +00:00
|
|
|
key_list = [redis_key.decode('utf-8')
|
|
|
|
|
for redis_key in REDIS_SERVER.keys(key)]
|
2015-01-27 23:06:56 +00:00
|
|
|
return strip_keys(key_list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_blocked_usernames():
|
|
|
|
|
""" get a list of blocked usernames from redis """
|
|
|
|
|
key = get_username_blocked_cache_key("*")
|
2015-06-29 10:33:33 +00:00
|
|
|
key_list = [redis_key.decode('utf-8')
|
|
|
|
|
for redis_key in REDIS_SERVER.keys(key)]
|
2015-01-27 23:06:56 +00:00
|
|
|
return strip_keys(key_list)
|
|
|
|
|
|
|
|
|
|
|
2014-12-31 22:00:45 +00:00
|
|
|
def increment_key(key):
|
|
|
|
|
""" given a key increment the value """
|
2015-03-20 15:09:45 +00:00
|
|
|
pipe = REDIS_SERVER.pipeline()
|
2015-01-03 00:07:01 +00:00
|
|
|
pipe.incr(key, 1)
|
2015-01-02 20:12:57 +00:00
|
|
|
if config.COOLOFF_TIME:
|
2015-01-03 00:07:01 +00:00
|
|
|
pipe.expire(key, config.COOLOFF_TIME)
|
|
|
|
|
new_value = pipe.execute()[0]
|
2014-12-31 22:00:45 +00:00
|
|
|
return new_value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_user_attempts(request):
|
2015-01-01 00:44:21 +00:00
|
|
|
""" Returns number of access attempts for this ip, username
|
2014-12-31 22:00:45 +00:00
|
|
|
"""
|
2015-03-20 15:09:45 +00:00
|
|
|
ip_address = get_ip(request)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
2015-01-01 17:51:46 +00:00
|
|
|
username = request.POST.get(config.USERNAME_FORM_FIELD, None)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
# get by IP
|
2015-03-20 15:09:45 +00:00
|
|
|
ip_count = REDIS_SERVER.get(get_ip_attempt_cache_key(ip_address))
|
2014-12-31 22:00:45 +00:00
|
|
|
if not ip_count:
|
|
|
|
|
ip_count = 0
|
2015-01-02 21:26:43 +00:00
|
|
|
ip_count = int(ip_count)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
# get by username
|
2015-03-20 15:09:45 +00:00
|
|
|
username_count = REDIS_SERVER.get(get_username_attempt_cache_key(username))
|
2014-12-31 22:00:45 +00:00
|
|
|
if not username_count:
|
|
|
|
|
username_count = 0
|
2015-01-02 21:26:43 +00:00
|
|
|
username_count = int(username_count)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
# return the larger of the two.
|
|
|
|
|
return max(ip_count, username_count)
|
|
|
|
|
|
|
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
def block_ip(ip_address):
|
2015-01-01 00:44:21 +00:00
|
|
|
""" given the ip, block it """
|
2015-03-20 15:09:45 +00:00
|
|
|
if not ip_address:
|
2015-02-01 15:17:10 +00:00
|
|
|
# no reason to continue when there is no ip
|
|
|
|
|
return
|
2015-03-20 15:09:45 +00:00
|
|
|
key = get_ip_blocked_cache_key(ip_address)
|
2015-01-02 20:12:57 +00:00
|
|
|
if config.COOLOFF_TIME:
|
2015-03-20 15:09:45 +00:00
|
|
|
REDIS_SERVER.set(key, 'blocked', config.COOLOFF_TIME)
|
2015-01-02 20:12:57 +00:00
|
|
|
else:
|
2015-03-20 15:09:45 +00:00
|
|
|
REDIS_SERVER.set(key, 'blocked')
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def block_username(username):
|
|
|
|
|
""" given the username block it. """
|
2015-02-01 15:17:10 +00:00
|
|
|
if not username:
|
|
|
|
|
# no reason to continue when there is no username
|
|
|
|
|
return
|
2014-12-31 22:00:45 +00:00
|
|
|
key = get_username_blocked_cache_key(username)
|
2015-01-02 20:12:57 +00:00
|
|
|
if config.COOLOFF_TIME:
|
2015-03-20 15:09:45 +00:00
|
|
|
REDIS_SERVER.set(key, 'blocked', config.COOLOFF_TIME)
|
2015-01-02 20:12:57 +00:00
|
|
|
else:
|
2015-03-20 15:09:45 +00:00
|
|
|
REDIS_SERVER.set(key, 'blocked')
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
def record_failed_attempt(ip_address, username):
|
2014-12-31 22:00:45 +00:00
|
|
|
""" record the failed login attempt, if over limit return False,
|
|
|
|
|
if not over limit return True """
|
|
|
|
|
# increment the failed count, and get current number
|
2015-03-20 15:09:45 +00:00
|
|
|
ip_count = increment_key(get_ip_attempt_cache_key(ip_address))
|
2014-12-31 22:00:45 +00:00
|
|
|
user_count = increment_key(get_username_attempt_cache_key(username))
|
|
|
|
|
|
2015-01-30 01:02:35 +00:00
|
|
|
ip_block = False
|
|
|
|
|
user_block = False
|
2014-12-31 22:00:45 +00:00
|
|
|
# if either are over the limit, add to block
|
2015-01-30 01:02:35 +00:00
|
|
|
if ip_count > config.FAILURE_LIMIT:
|
2015-03-20 15:09:45 +00:00
|
|
|
block_ip(ip_address)
|
2015-01-30 01:02:35 +00:00
|
|
|
ip_block = True
|
|
|
|
|
if user_count > config.FAILURE_LIMIT:
|
2014-12-31 22:00:45 +00:00
|
|
|
block_username(username)
|
2015-01-30 01:02:35 +00:00
|
|
|
user_block = True
|
2015-04-21 22:22:17 +00:00
|
|
|
|
|
|
|
|
if config.LOCKOUT_BY_IP_USERNAME:
|
|
|
|
|
return not (ip_block and user_block)
|
|
|
|
|
|
2015-01-30 01:02:35 +00:00
|
|
|
# if any blocks return False, no blocks return True
|
|
|
|
|
return not (ip_block or user_block)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
def unblock_ip(ip_address, pipe=None):
|
2015-01-27 23:06:56 +00:00
|
|
|
""" unblock the given IP """
|
|
|
|
|
do_commit = False
|
|
|
|
|
if not pipe:
|
2015-03-20 15:09:45 +00:00
|
|
|
pipe = REDIS_SERVER.pipeline()
|
2015-01-27 23:06:56 +00:00
|
|
|
do_commit = True
|
2015-03-20 15:09:45 +00:00
|
|
|
if ip_address:
|
|
|
|
|
pipe.delete(get_ip_attempt_cache_key(ip_address))
|
|
|
|
|
pipe.delete(get_ip_blocked_cache_key(ip_address))
|
2015-01-27 23:06:56 +00:00
|
|
|
if do_commit:
|
|
|
|
|
pipe.execute()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def unblock_username(username, pipe=None):
|
|
|
|
|
""" unblock the given Username """
|
|
|
|
|
do_commit = False
|
|
|
|
|
if not pipe:
|
2015-03-20 15:09:45 +00:00
|
|
|
pipe = REDIS_SERVER.pipeline()
|
2015-01-27 23:06:56 +00:00
|
|
|
do_commit = True
|
2014-12-31 22:00:45 +00:00
|
|
|
if username:
|
2015-01-03 00:07:01 +00:00
|
|
|
pipe.delete(get_username_attempt_cache_key(username))
|
|
|
|
|
pipe.delete(get_username_blocked_cache_key(username))
|
2015-01-27 23:06:56 +00:00
|
|
|
if do_commit:
|
|
|
|
|
pipe.execute()
|
|
|
|
|
|
|
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
def reset_failed_attempts(ip_address=None, username=None):
|
2015-01-27 23:06:56 +00:00
|
|
|
""" reset the failed attempts for these ip's and usernames
|
|
|
|
|
"""
|
2015-03-20 15:09:45 +00:00
|
|
|
pipe = REDIS_SERVER.pipeline()
|
2015-01-27 23:06:56 +00:00
|
|
|
|
2015-03-20 15:09:45 +00:00
|
|
|
unblock_ip(ip_address, pipe=pipe)
|
2015-01-27 23:06:56 +00:00
|
|
|
unblock_username(username, pipe=pipe)
|
|
|
|
|
|
2015-01-03 00:07:01 +00:00
|
|
|
pipe.execute()
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def lockout_response(request):
|
|
|
|
|
""" if we are locked out, here is the response """
|
2015-01-01 17:51:46 +00:00
|
|
|
if config.LOCKOUT_TEMPLATE:
|
2014-12-31 22:00:45 +00:00
|
|
|
context = {
|
2015-01-29 13:10:19 +00:00
|
|
|
'cooloff_time_seconds': config.COOLOFF_TIME,
|
|
|
|
|
'cooloff_time_minutes': config.COOLOFF_TIME / 60,
|
2015-01-01 17:51:46 +00:00
|
|
|
'failure_limit': config.FAILURE_LIMIT,
|
2014-12-31 22:00:45 +00:00
|
|
|
}
|
2015-01-01 17:51:46 +00:00
|
|
|
return render_to_response(config.LOCKOUT_TEMPLATE, context,
|
2014-12-31 22:00:45 +00:00
|
|
|
context_instance=RequestContext(request))
|
|
|
|
|
|
2015-01-01 17:51:46 +00:00
|
|
|
if config.LOCKOUT_URL:
|
|
|
|
|
return HttpResponseRedirect(config.LOCKOUT_URL)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
2015-01-01 17:51:46 +00:00
|
|
|
if config.COOLOFF_TIME:
|
2014-12-31 22:00:45 +00:00
|
|
|
return HttpResponse("Account locked: too many login attempts. "
|
|
|
|
|
"Please try again later.")
|
|
|
|
|
else:
|
|
|
|
|
return HttpResponse("Account locked: too many login attempts. "
|
|
|
|
|
"Contact an admin to unlock your account.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_already_locked(request):
|
|
|
|
|
""" Is this IP/username already locked? """
|
|
|
|
|
ip_address = get_ip(request)
|
2015-01-01 17:51:46 +00:00
|
|
|
username = request.POST.get(config.USERNAME_FORM_FIELD, None)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
# ip blocked?
|
2015-03-20 15:09:45 +00:00
|
|
|
ip_blocked = REDIS_SERVER.get(get_ip_blocked_cache_key(ip_address))
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
# username blocked?
|
2015-03-20 15:09:45 +00:00
|
|
|
user_blocked = REDIS_SERVER.get(get_username_blocked_cache_key(username))
|
2015-04-21 22:22:17 +00:00
|
|
|
|
|
|
|
|
if config.LOCKOUT_BY_IP_USERNAME:
|
|
|
|
|
LOG.info("Block by ip & username")
|
|
|
|
|
if ip_blocked and user_blocked:
|
2015-04-21 23:24:01 +00:00
|
|
|
# if both this IP and this username are present the request is
|
|
|
|
|
# blocked
|
2015-04-21 22:22:17 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
if ip_blocked:
|
|
|
|
|
# short circuit no need to check username if ip is already blocked.
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
if user_blocked:
|
|
|
|
|
return True
|
2014-12-31 22:00:45 +00:00
|
|
|
|
2015-01-12 17:42:38 +00:00
|
|
|
# if the username nor ip is blocked, the request is not blocked
|
|
|
|
|
return False
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_request(request, login_unsuccessful):
|
|
|
|
|
""" check the request, and process results"""
|
|
|
|
|
ip_address = get_ip(request)
|
2015-01-01 17:51:46 +00:00
|
|
|
username = request.POST.get(config.USERNAME_FORM_FIELD, None)
|
2014-12-31 22:00:45 +00:00
|
|
|
|
|
|
|
|
if not login_unsuccessful:
|
|
|
|
|
# user logged in -- forget the failed attempts
|
2015-03-20 15:09:45 +00:00
|
|
|
reset_failed_attempts(ip_address=ip_address, username=username)
|
2014-12-31 22:00:45 +00:00
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
# add a failed attempt for this user
|
|
|
|
|
return record_failed_attempt(ip_address, username)
|
2015-01-01 00:27:18 +00:00
|
|
|
|
|
|
|
|
|
2015-01-03 21:33:51 +00:00
|
|
|
def add_login_attempt_to_db(request, login_valid):
|
|
|
|
|
""" Create a record for the login attempt If using celery call celery
|
|
|
|
|
task, if not, call the method normally """
|
2015-03-20 14:09:39 +00:00
|
|
|
|
|
|
|
|
if not config.STORE_ACCESS_ATTEMPTS:
|
|
|
|
|
# If we don't want to store in the database, then don't proceed.
|
|
|
|
|
return
|
|
|
|
|
|
2015-01-03 21:33:51 +00:00
|
|
|
user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
|
|
|
|
|
ip_address = get_ip(request)
|
|
|
|
|
username = request.POST.get(config.USERNAME_FORM_FIELD, None)
|
|
|
|
|
http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')
|
|
|
|
|
path_info = request.META.get('PATH_INFO', '<unknown>')
|
|
|
|
|
|
|
|
|
|
if config.USE_CELERY:
|
|
|
|
|
from .tasks import add_login_attempt_task
|
|
|
|
|
add_login_attempt_task.delay(user_agent, ip_address, username,
|
|
|
|
|
http_accept, path_info, login_valid)
|
|
|
|
|
else:
|
|
|
|
|
store_login_attempt(user_agent, ip_address, username,
|
|
|
|
|
http_accept, path_info, login_valid)
|