Added django-ipware

This commit is contained in:
Camilo Nova 2017-12-13 11:19:19 -05:00
parent 6945880ea2
commit da0c4b429a
9 changed files with 9 additions and 202 deletions

View file

@ -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):

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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):

View file

@ -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.

View file

@ -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=[

View file

@ -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 =