mirror of
https://github.com/jazzband/django-axes.git
synced 2026-03-16 22:30:23 +00:00
Added django-ipware
This commit is contained in:
parent
6945880ea2
commit
da0c4b429a
9 changed files with 9 additions and 202 deletions
|
|
@ -5,9 +5,10 @@ from django.contrib.auth import get_user_model
|
|||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
|
||||
from ipware.ip import get_ip
|
||||
|
||||
from axes.conf import settings
|
||||
from axes.models import AccessAttempt
|
||||
from axes.utils import get_ip
|
||||
|
||||
|
||||
def _query_user_attempts(request):
|
||||
|
|
|
|||
|
|
@ -25,19 +25,6 @@ if settings.AXES_VERBOSE:
|
|||
log.info('AXES: blocking by IP only.')
|
||||
|
||||
|
||||
if settings.AXES_BEHIND_REVERSE_PROXY:
|
||||
log.debug('AXES: Axes is configured to be behind reverse proxy')
|
||||
log.debug(
|
||||
'AXES: Looking for header value %s', settings.AXES_REVERSE_PROXY_HEADER
|
||||
)
|
||||
log.debug(
|
||||
'AXES: Number of proxies configured: {} '
|
||||
'(please check this if you are using a custom header)'.format(
|
||||
settings.AXES_NUM_PROXIES
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def axes_dispatch(func):
|
||||
def inner(request, *args, **kwargs):
|
||||
if is_already_locked(request):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ from django.dispatch import receiver
|
|||
from django.dispatch import Signal
|
||||
from django.utils import timezone
|
||||
|
||||
from ipware.ip import get_ip
|
||||
|
||||
from axes.conf import settings
|
||||
from axes.attempts import get_cache_key
|
||||
from axes.attempts import get_cache_timeout
|
||||
|
|
@ -17,7 +19,6 @@ from axes.attempts import is_user_lockable
|
|||
from axes.attempts import ip_in_whitelist
|
||||
from axes.models import AccessLog, AccessAttempt
|
||||
from axes.utils import get_client_str
|
||||
from axes.utils import get_ip
|
||||
from axes.utils import query2str
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ class AccessAttemptTest(TestCase):
|
|||
# Make a login attempt again
|
||||
self.test_valid_login()
|
||||
|
||||
@patch('axes.utils.get_ip', return_value='127.0.0.1')
|
||||
@patch('ipware.ip.get_ip', return_value='127.0.0.1')
|
||||
def test_get_cache_key(self, get_ip_mock):
|
||||
""" Test the cache key format"""
|
||||
# Getting cache key from request
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
from django.test import TestCase, override_settings
|
||||
|
||||
from axes.conf import settings
|
||||
from axes.utils import get_ip
|
||||
|
||||
|
||||
class MockRequest:
|
||||
def __init__(self):
|
||||
self.META = dict()
|
||||
|
||||
|
||||
@override_settings(AXES_BEHIND_REVERSE_PROXY=True)
|
||||
class GetIPProxyTest(TestCase):
|
||||
"""Test get_ip returns correct addresses with proxy
|
||||
"""
|
||||
def setUp(self):
|
||||
self.request = MockRequest()
|
||||
|
||||
def test_iis_ipv4_port_stripping(self):
|
||||
self.ip = '192.168.1.1'
|
||||
|
||||
valid_headers = [
|
||||
'192.168.1.1:6112',
|
||||
'192.168.1.1:6033, 192.168.1.2:9001',
|
||||
]
|
||||
|
||||
for header in valid_headers:
|
||||
self.request.META['HTTP_X_FORWARDED_FOR'] = header
|
||||
self.assertEqual(self.ip, get_ip(self.request))
|
||||
|
||||
def test_valid_ipv4_parsing(self):
|
||||
self.ip = '192.168.1.1'
|
||||
|
||||
valid_headers = [
|
||||
'192.168.1.1',
|
||||
'192.168.1.1, 192.168.1.2',
|
||||
' 192.168.1.1 , 192.168.1.2 ',
|
||||
' 192.168.1.1 , 2001:db8:cafe::17 ',
|
||||
]
|
||||
|
||||
for header in valid_headers:
|
||||
self.request.META['HTTP_X_FORWARDED_FOR'] = header
|
||||
self.assertEqual(self.ip, get_ip(self.request))
|
||||
|
||||
def test_valid_ipv6_parsing(self):
|
||||
self.ip = '2001:db8:cafe::17'
|
||||
|
||||
valid_headers = [
|
||||
'2001:db8:cafe::17',
|
||||
'2001:db8:cafe::17 , 2001:db8:cafe::18',
|
||||
'2001:db8:cafe::17, 2001:db8:cafe::18, 192.168.1.1',
|
||||
]
|
||||
|
||||
for header in valid_headers:
|
||||
self.request.META['HTTP_X_FORWARDED_FOR'] = header
|
||||
self.assertEqual(self.ip, get_ip(self.request))
|
||||
|
||||
|
||||
@override_settings(AXES_BEHIND_REVERSE_PROXY=True)
|
||||
@override_settings(AXES_REVERSE_PROXY_HEADER='HTTP_X_FORWARDED_FOR')
|
||||
@override_settings(AXES_NUM_PROXIES=2)
|
||||
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)
|
||||
|
||||
|
||||
@override_settings(AXES_BEHIND_REVERSE_PROXY=True)
|
||||
@override_settings(AXES_REVERSE_PROXY_HEADER='HTTP_X_AXES_CUSTOM_HEADER')
|
||||
class GetIPProxyCustomHeaderTest(TestCase):
|
||||
"""Test that get_ip returns correct addresses with a custom proxy header
|
||||
"""
|
||||
def setUp(self):
|
||||
self.request = MockRequest()
|
||||
|
||||
def test_custom_header_parsing(self):
|
||||
self.ip = '2001:db8:cafe::17'
|
||||
|
||||
valid_headers = [
|
||||
' 2001:db8:cafe::17 , 2001:db8:cafe::18',
|
||||
]
|
||||
|
||||
for header in valid_headers:
|
||||
self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header
|
||||
self.assertEqual(self.ip, get_ip(self.request))
|
||||
|
|
@ -3,6 +3,7 @@ from socket import inet_pton, AF_INET6, error
|
|||
from django.core.cache import cache
|
||||
from django.utils import six
|
||||
|
||||
from axes.attempts import get_cache_key
|
||||
from axes.conf import settings
|
||||
from axes.models import AccessAttempt
|
||||
|
||||
|
|
@ -49,75 +50,6 @@ def is_ipv6(ip):
|
|||
return True
|
||||
|
||||
|
||||
def get_ip(request):
|
||||
"""Parse IP address from REMOTE_ADDR or
|
||||
AXES_REVERSE_PROXY_HEADER if AXES_BEHIND_REVERSE_PROXY is set."""
|
||||
if settings.AXES_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,
|
||||
# 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_str = request.META.get(settings.AXES_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.
|
||||
#
|
||||
# This is because IP headers can have multiple IPs in different
|
||||
# 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.
|
||||
#
|
||||
# Please see discussion for more information:
|
||||
# https://github.com/jazzband/django-axes/issues/224
|
||||
ip_list = [ip.strip() for ip in ip_str.split(',')]
|
||||
|
||||
# Pick the nth last IP in the given list of addresses after parsing
|
||||
if len(ip_list) >= settings.AXES_NUM_PROXIES:
|
||||
ip = ip_list[-settings.AXES_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(
|
||||
settings.AXES_REVERSE_PROXY_HEADER, ip_str
|
||||
)
|
||||
)
|
||||
|
||||
if not ip:
|
||||
raise Warning(
|
||||
'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(
|
||||
settings.AXES_REVERSE_PROXY_HEADER, ip_str
|
||||
)
|
||||
)
|
||||
|
||||
return ip
|
||||
|
||||
return request.META.get('REMOTE_ADDR', '')
|
||||
|
||||
|
||||
def reset(ip=None, username=None):
|
||||
"""Reset records that match ip or username, and
|
||||
return the count of removed attempts.
|
||||
|
|
@ -132,8 +64,6 @@ def reset(ip=None, username=None):
|
|||
|
||||
if attempts:
|
||||
count = attempts.count()
|
||||
# import should be here to avoid circular dependency with get_ip
|
||||
from axes.attempts import get_cache_key
|
||||
for attempt in attempts:
|
||||
cache_hash_key = get_cache_key(attempt)
|
||||
if cache.get(cache_hash_key):
|
||||
|
|
|
|||
|
|
@ -59,10 +59,6 @@ These should be defined in your ``settings.py`` file.
|
|||
* ``AXES_NEVER_LOCKOUT_WHITELIST``: If ``True``, users can always login from whitelisted IP addresses.
|
||||
Default: ``False``
|
||||
* ``AXES_IP_WHITELIST``: A list of IP's to be whitelisted. For example: AXES_IP_WHITELIST=['0.0.0.0']. Default: []
|
||||
* ``AXES_BEHIND_REVERSE_PROXY``: If ``True``, it will look for the IP address from the header defined at ``AXES_REVERSE_PROXY_HEADER``. Please make sure if you enable this setting to configure your proxy to set the correct value for the header, otherwise you could be attacked by setting this header directly in every request.
|
||||
Default: ``False``
|
||||
* ``AXES_REVERSE_PROXY_HEADER``: If ``AXES_BEHIND_REVERSE_PROXY`` is ``True``, it will look for the IP address from this header.
|
||||
Default: ``HTTP_X_FORWARDED_FOR``
|
||||
* ``AXES_NUM_PROXIES``: If ``AXES_BEHIND_REVERSE_PROXY`` is ``True``, use this value to calculate the end user IP address from the end of the list of IPs in header ``AXES_REVERSE_PROXY_HEADER``. For example, if you have one (1) proxy configured and set ``AXES_NUM_PROXIES = 1`` we, choose IP ``[ip.strip() for ip in request.META.get(AXES_REVERSE_PROXY_HEADER).split(',')][-1]``. For ``X-Forwarded-For: a, b, client-ip`` this would pick the value ``client-ip``. This configuration is used to prevent ``X-Forwarded-For`` (XFF) header spoofing or injection by the end user, because the ``X-Forwarded-For`` headers can be added to the request by the end user, circumventing the IP locking mechanisms in Axes. If you are running with Apache, nginx, or Elastic Load Balancer, you should set this to ``1``. It is by default configured to ``0`` for backwards compatibility. Default: ``0``
|
||||
* ``AXES_DISABLE_ACCESS_LOG``: If ``True``, disable all access logging, so the admin interface will be empty.
|
||||
* ``AXES_DISABLE_SUCCESS_ACCESS_LOG``: If ``True``, successful logins will not be logged, so the access log shown in the admin interface will only list unsuccessful login attempts.
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -20,7 +20,7 @@ setup(
|
|||
url='https://github.com/jazzband/django-axes',
|
||||
license='MIT',
|
||||
package_dir={'axes': 'axes'},
|
||||
install_requires=['pytz', 'django-appconf'],
|
||||
install_requires=['pytz', 'django-appconf', 'django-ipware'],
|
||||
include_package_data=True,
|
||||
packages=find_packages(),
|
||||
classifiers=[
|
||||
|
|
|
|||
3
tox.ini
3
tox.ini
|
|
@ -8,9 +8,10 @@ envlist =
|
|||
deps =
|
||||
py27: mock
|
||||
django-appconf
|
||||
django-ipware
|
||||
coveralls
|
||||
django-111: Django>=1.11,<2.0
|
||||
django-20: Django>=2.0a1,<2.1
|
||||
django-20: Django>=2.0,<2.1
|
||||
django-master: https://github.com/django/django/archive/master.tar.gz
|
||||
usedevelop = True
|
||||
ignore_outcome =
|
||||
|
|
|
|||
Loading…
Reference in a new issue