diff --git a/.travis.yml b/.travis.yml index 5df10d4..f488e0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: script: - coverage run -a --source=axes runtests.py - coverage run -a --source=axes runtests_proxy.py +- coverage run -a --source=axes runtests_num_proxies.py - coverage run -a --source=axes runtests_proxy_custom_header.py - coverage report after_success: diff --git a/axes/decorators.py b/axes/decorators.py index bb47feb..5cb8458 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -113,15 +113,15 @@ def get_ip(request): if BEHIND_REVERSE_PROXY: # For requests originating from behind a reverse proxy, # resolve the IP address from the given AXES_REVERSE_PROXY_HEADER. - # AXES_REVERSE_PROXY_HEADER defaults to HTTP_X_FORWARDED_FOR if not given, - # which is the Django calling format for the HTTP X-Forwarder-For header. + # AXES_REVERSE_PROXY_HEADER defaults to HTTP_X_FORWARDED_FOR, + # which is the Django name for the HTTP X-Forwarder-For header. # Please see RFC7239 for additional information: # https://tools.ietf.org/html/rfc7239#section-5 # The REVERSE_PROXY_HEADER HTTP header is a list # of potentionally unsecure IPs, for example: # X-Forwarded-For: 1.1.1.1, 11.11.11.11:8080, 111.111.111.111 - ip = request.META.get(REVERSE_PROXY_HEADER, '') + ip_str = request.META.get(REVERSE_PROXY_HEADER, '') # We need to know the number of proxies present in the request chain # in order to securely calculate the one IP that is the real client IP. @@ -130,23 +130,45 @@ def get_ip(request): # configurations, with e.g. the X-Forwarded-For header containing # the originating client IP, proxies and possibly spoofed values. # - # If you are using a special header for client calculation such as - # the X-Real-IP or the like with nginx, please check this configuration. + # If you are using a special header for client calculation such as the + # X-Real-IP or the like with nginx, please check this configuration. # # Please see discussion for more information: # https://github.com/jazzband/django-axes/issues/224 - ip = [ip.strip() for ip in ip.split(',')][-NUM_PROXIES] + ip_list = [ip.strip() for ip in ip_str.split(',')] - # Fix IIS adding client port number to 'X-Forwarded-For' header (strip port) - if not is_ipv6(ip): - ip = ip.split(':', 1)[0] + # Pick the nth last IP in the given list of addresses after parsing + if len(ip_list) >= NUM_PROXIES: + ip = ip_list[-NUM_PROXIES] + + # Fix IIS adding client port number to the + # 'X-Forwarded-For' header (strip port) + if not is_ipv6(ip): + ip = ip.split(':', 1)[0] + + # If nth last is not found, default to no IP and raise a warning + else: + ip = '' + raise Warning( + 'AXES: Axes is configured for operation behind a ' + 'reverse proxy but received too few IPs in the HTTP ' + 'AXES_REVERSE_PROXY_HEADER. Check your ' + 'AXES_NUM_PROXIES configuration. ' + 'Header name: {0}, value: {1}'.format( + REVERSE_PROXY_HEADER, ip_str + ) + ) if not ip: raise Warning( - 'AXES: Axes is configured for operation behind a reverse proxy ' - 'but could not find an HTTP header value. Check your proxy ' - 'server settings to make sure this header value is being ' - 'passed. Header name {0}'.format(REVERSE_PROXY_HEADER) + 'AXES: Axes is configured for operation behind a reverse ' + 'proxy but could not find a suitable IP in the specified ' + 'HTTP header. Check your proxy server settings to make ' + 'sure correct headers are being passed to Django in ' + 'AXES_REVERSE_PROXY_HEADER. ' + 'Header name: {0}, value: {1}'.format( + REVERSE_PROXY_HEADER, ip_str + ) ) return ip @@ -233,11 +255,18 @@ def _get_user_attempts(request): ) if not attempts: - params = {'ip_address': ip, 'trusted': False} + params = {'trusted': False} + + if AXES_ONLY_USER_FAILURES: + params['username'] = username + elif LOCK_OUT_BY_COMBINATION_USER_AND_IP: + params['username'] = username + params['ip_address'] = ip + else: + params['ip_address'] = ip + if USE_USER_AGENT: params['user_agent'] = ua - if LOCK_OUT_BY_COMBINATION_USER_AND_IP: - params['username'] = username attempts = AccessAttempt.objects.filter(**params) @@ -577,23 +606,30 @@ def get_cache_key(request_or_object): :param request_or_object: Request or AccessAttempt object :return cache-key: String, key to be used in cache system """ - ua = None - ip = None - if isinstance(request_or_object, AccessAttempt): ip = request_or_object.ip_address + un = request_or_object.username ua = request_or_object.user_agent else: ip = get_ip(request_or_object) + un = request_or_object.POST.get(USERNAME_FORM_FIELD, None) ua = request_or_object.META.get('HTTP_USER_AGENT', '')[:255] - ip = ip.encode('utf-8') + ip = ip.encode('utf-8') if ip else ''.encode('utf-8') + un = un.encode('utf-8') if un else ''.encode('utf-8') + ua = ua.encode('utf-8') if ua else ''.encode('utf-8') - if ua: - ua = ua.encode('utf-8') - cache_hash_key = 'axes-{}'.format(md5(ip+ua).hexdigest()) + if AXES_ONLY_USER_FAILURES: + attributes = un + elif LOCK_OUT_BY_COMBINATION_USER_AND_IP: + attributes = ip+un else: - cache_hash_key = 'axes-{}'.format(md5(ip).hexdigest()) + attributes = ip + + if USE_USER_AGENT: + attributes += ua + + cache_hash_key = 'axes-{}'.format(md5(attributes).hexdigest()) return cache_hash_key diff --git a/axes/test_settings.py b/axes/test_settings.py index b8a47d4..46ba0b5 100644 --- a/axes/test_settings.py +++ b/axes/test_settings.py @@ -1,5 +1,3 @@ -from datetime import timedelta - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -55,4 +53,3 @@ USE_TZ = False LOGIN_REDIRECT_URL = '/admin/' AXES_LOGIN_FAILURE_LIMIT = 10 -AXES_COOLOFF_TIME = timedelta(seconds=2) diff --git a/axes/test_settings_num_proxies.py b/axes/test_settings_num_proxies.py new file mode 100644 index 0000000..53f3166 --- /dev/null +++ b/axes/test_settings_num_proxies.py @@ -0,0 +1,5 @@ +from .test_settings import * + +AXES_BEHIND_REVERSE_PROXY = True +AXES_REVERSE_PROXY_HEADER = 'HTTP_X_FORWARDED_FOR' +AXES_NUM_PROXIES = 2 diff --git a/axes/tests.py b/axes/tests.py index 387615f..16e86ec 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -16,13 +16,15 @@ from django.utils import six from django.test.client import RequestFactory from axes.decorators import get_ip, get_cache_key, get_client_str -from axes.settings import COOLOFF_TIME from axes.settings import FAILURE_LIMIT from axes.models import AccessAttempt, AccessLog from axes.signals import user_locked_out from axes.utils import reset, iso8601 +TEST_COOLOFF_TIME = datetime.timedelta(seconds=2) + + class MockRequest: def __init__(self): self.META = dict() @@ -140,17 +142,19 @@ class AccessAttemptTest(TestCase): self.assertNotEquals(AccessLog.objects.latest('id').logout_time, None) self.assertContains(response, 'Logged out') + @patch('axes.decorators.COOLOFF_TIME', TEST_COOLOFF_TIME) def test_cooling_off(self): """Tests if the cooling time allows a user to login """ self.test_failure_limit_once() # Wait for the cooling off period - time.sleep(COOLOFF_TIME.total_seconds()) + time.sleep(TEST_COOLOFF_TIME.total_seconds()) # It should be possible to login again, make sure it is. self.test_valid_login() + @patch('axes.decorators.COOLOFF_TIME', TEST_COOLOFF_TIME) def test_cooling_off_for_trusted_user(self): """Test the cooling time for a trusted user """ @@ -213,7 +217,7 @@ class AccessAttemptTest(TestCase): ip = '127.0.0.1'.encode('utf-8') ua = ''.encode('utf-8') - cache_hash_key_checker = 'axes-{}'.format(md5((ip+ua)).hexdigest()) + cache_hash_key_checker = 'axes-{}'.format(md5((ip)).hexdigest()) request_factory = RequestFactory() request = request_factory.post('/admin/login/', @@ -443,6 +447,469 @@ class AccessAttemptTest(TestCase): self.assertEqual(response.status_code, 200) +class AccessAttemptConfigTest(TestCase): + """ This set of tests checks for lockouts under different configurations + and circumstances to prevent false positives and false negatives. + Always block attempted logins for the same user from the same IP. + Always allow attempted logins for a different user from a different IP. + """ + + IP_1 = '10.1.1.1' + IP_2 = '10.2.2.2' + USER_1 = 'valid-user-1' + USER_2 = 'valid-user-2' + VALID_PASSWORD = 'valid-password' + WRONG_PASSWORD = 'wrong-password' + LOCKED_MESSAGE = 'Account locked: too many login attempts.' + LOGIN_FORM_KEY = '' + ALLOWED = 302 + BLOCKED = 403 + + def _login(self, username, password, ip_addr='127.0.0.1', + is_json=False, **kwargs): + """Login a user and get the response. + IP address can be configured to test IP blocking functionality. + """ + try: + admin_login = reverse('admin:login') + except NoReverseMatch: + admin_login = reverse('admin:index') + + headers = { + 'user_agent': 'test-browser' + } + post_data = { + 'username': username, + 'password': password, + 'this_is_the_login_form': 1, + } + post_data.update(kwargs) + + if is_json: + headers.update({ + 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', + 'content_type': 'application/json', + }) + post_data = json.dumps(post_data) + + response = self.client.post( + admin_login, post_data, REMOTE_ADDR=ip_addr, **headers + ) + return response + + def _lockout_user1_from_ip1(self): + for i in range(1, FAILURE_LIMIT+1): + response = self._login( + username=self.USER_1, + password=self.WRONG_PASSWORD, + ip_addr=self.IP_1 + ) + return response + + def setUp(self): + """Create two valid users for authentication. + """ + + self.user = User.objects.create_superuser( + username=self.USER_1, + email='test_1@example.com', + password=self.VALID_PASSWORD, + ) + self.user = User.objects.create_superuser( + username=self.USER_2, + email='test_2@example.com', + password=self.VALID_PASSWORD, + ) + + # Test for true and false positives when blocking by IP *OR* user (default). + # Cache disabled. Default settings. + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_blocks_when_same_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_allows_when_same_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 can still login from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_blocks_when_diff_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 is also locked out from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_allows_when_diff_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by user only. + # Cache disabled. When AXES_ONLY_USER_FAILURES = True + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_blocks_when_same_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_blocks_when_same_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is also locked out from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_allows_when_diff_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_allows_when_diff_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by user and IP together. + # Cache disabled. When LOCK_OUT_BY_COMBINATION_USER_AND_IP = True + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_blocks_when_same_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_allows_when_same_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 can still login from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_allows_when_diff_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_allows_when_diff_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by IP *OR* user (default). + # With cache enabled. Default criteria. + def test_lockout_by_ip_blocks_when_same_user_same_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + def test_lockout_by_ip_allows_when_same_user_diff_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 can still login from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + def test_lockout_by_ip_blocks_when_diff_user_same_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 is also locked out from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + def test_lockout_by_ip_allows_when_diff_user_diff_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by user only. + # With cache enabled. When AXES_ONLY_USER_FAILURES = True + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + def test_lockout_by_user_blocks_when_same_user_same_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + def test_lockout_by_user_blocks_when_same_user_diff_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is also locked out from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + def test_lockout_by_user_allows_when_diff_user_same_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + def test_lockout_by_user_allows_when_diff_user_diff_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by user and IP together. + # With cache enabled. When LOCK_OUT_BY_COMBINATION_USER_AND_IP = True + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + def test_lockout_by_user_and_ip_blocks_when_same_user_same_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + def test_lockout_by_user_and_ip_allows_when_same_user_diff_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 can still login from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + def test_lockout_by_user_and_ip_allows_when_diff_user_same_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + def test_lockout_by_user_and_ip_allows_when_diff_user_diff_ip_using_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + class UtilsTest(TestCase): def test_iso8601(self): """Tests iso8601 correctly translates datetime.timdelta to ISO 8601 @@ -562,3 +1029,33 @@ class GetIPProxyCustomHeaderTest(TestCase): for header in valid_headers: self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header self.assertEqual(self.ip, get_ip(self.request)) + +class GetIPNumProxiesTest(TestCase): + """Test that get_ip returns the correct last IP when NUM_PROXIES is configured + """ + + def setUp(self): + self.request = MockRequest() + + def test_header_ordering(self): + self.ip = '2.2.2.2' + + valid_headers = [ + '4.4.4.4, 3.3.3.3, 2.2.2.2, 1.1.1.1', + ' 3.3.3.3, 2.2.2.2, 1.1.1.1', + ' 2.2.2.2, 1.1.1.1', + ] + + for header in valid_headers: + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header + self.assertEqual(self.ip, get_ip(self.request)) + + def test_invalid_headers_too_few(self): + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = '1.1.1.1' + with self.assertRaises(Warning): + get_ip(self.request) + + def test_invalid_headers_no_ip(self): + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = '' + with self.assertRaises(Warning): + get_ip(self.request) diff --git a/runtests.py b/runtests.py index f76e943..085472a 100755 --- a/runtests.py +++ b/runtests.py @@ -20,5 +20,6 @@ def run_tests(settings_module, *modules): if __name__ == '__main__': run_tests('axes.test_settings', [ 'axes.tests.AccessAttemptTest', + 'axes.tests.AccessAttemptConfigTest', 'axes.tests.UtilsTest', ]) diff --git a/runtests_num_proxies.py b/runtests_num_proxies.py new file mode 100644 index 0000000..a3dbba8 --- /dev/null +++ b/runtests_num_proxies.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +from runtests import run_tests + +if __name__ == '__main__': + run_tests('axes.test_settings_num_proxies', [ + 'axes.tests.GetIPNumProxiesTest', + ]) diff --git a/setup.py b/setup.py index a8a4668..c937536 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ setup( url='https://github.com/django-pci/django-axes', license='MIT', package_dir={'axes': 'axes'}, + install_requires=['pytz'], include_package_data=True, packages=find_packages(), classifiers=[