Przeglądaj źródła

Closes #22192: Introduce HTTP_CLIENT_IP_HEADERS configuration parameter (#22197)

Jeremy Stretch 1 tydzień temu
rodzic
commit
329c041224

+ 20 - 0
docs/configuration/system.md

@@ -80,6 +80,26 @@ The hostname displayed in the user interface identifying the system on which Net
 
 ---
 
+## HTTP_CLIENT_IP_HEADERS
+
+!!! info "This parameter was introduced in NetBox v4.6.1."
+
+Default:
+
+```python
+(
+    'HTTP_X_REAL_IP',
+    'HTTP_X_FORWARDED_FOR',
+    'REMOTE_ADDR',
+)
+```
+
+An ordered list of HTTP request headers inspected to determine the source IP address of a client request. The first header in the list which is present on the request is used; if none are found, the client IP cannot be determined. This is most commonly required when NetBox is deployed behind a reverse proxy which injects a proprietary client IP header (e.g. `HTTP_CF_CONNECTING_IP` for Cloudflare).
+
+The client IP is used for source-address restrictions on API tokens and for logging failed login attempts.
+
+---
+
 ## HTTP_PROXIES
 
 Default: `None`

+ 5 - 0
netbox/netbox/settings.py

@@ -132,6 +132,11 @@ GRAPHQL_DEFAULT_VERSION = getattr(configuration, 'GRAPHQL_DEFAULT_VERSION', 1)
 GRAPHQL_MAX_ALIASES = getattr(configuration, 'GRAPHQL_MAX_ALIASES', 10)
 GRAPHQL_MAX_QUERY_DEPTH = getattr(configuration, 'GRAPHQL_MAX_QUERY_DEPTH', None)
 HOSTNAME = getattr(configuration, 'HOSTNAME', platform.node())
+HTTP_CLIENT_IP_HEADERS = getattr(configuration, 'HTTP_CLIENT_IP_HEADERS', (
+    'HTTP_X_REAL_IP',
+    'HTTP_X_FORWARDED_FOR',
+    'REMOTE_ADDR',
+))
 HTTP_PROXIES = getattr(configuration, 'HTTP_PROXIES', {})
 INTERNAL_IPS = getattr(configuration, 'INTERNAL_IPS', ('127.0.0.1', '::1'))
 ISOLATED_DEPLOYMENT = getattr(configuration, 'ISOLATED_DEPLOYMENT', False)

+ 7 - 7
netbox/utilities/request.py

@@ -2,6 +2,7 @@ import warnings
 from contextlib import ExitStack, contextmanager
 from urllib.parse import urlparse
 
+from django.conf import settings
 from django.utils.http import url_has_allowed_host_and_scheme
 from django.utils.translation import gettext_lazy as _
 from netaddr import AddrFormatError, IPAddress
@@ -71,15 +72,14 @@ def copy_safe_request(request, include_files=True):
 
 def get_client_ip(request, additional_headers=()):
     """
-    Return the client (source) IP address of the given request.
+    Return the client (source) IP address of the given request. Accepts an optional list of headers to inspect in
+    addition to those configured under HTTP_CLIENT_IP_HEADERS.
     """
-    HTTP_HEADERS = (
-        'HTTP_X_REAL_IP',
-        'HTTP_X_FORWARDED_FOR',
-        'REMOTE_ADDR',
-        *additional_headers
+    headers = (
+        *settings.HTTP_CLIENT_IP_HEADERS,
+        *additional_headers,
     )
-    for header in HTTP_HEADERS:
+    for header in headers:
         if header in request.META:
             ip = request.META[header].split(',')[0].strip()
             try:

+ 29 - 1
netbox/utilities/tests/test_request.py

@@ -1,5 +1,5 @@
 from django.contrib.auth.models import AnonymousUser
-from django.test import RequestFactory, TestCase
+from django.test import RequestFactory, TestCase, override_settings
 from netaddr import IPAddress
 
 from utilities.request import copy_safe_request, get_client_ip
@@ -61,3 +61,31 @@ class GetClientIPTestCase(TestCase):
         request = self.factory.get('/', HTTP_X_FORWARDED_FOR='invalid_ip')
         with self.assertRaises(ValueError):
             get_client_ip(request)
+
+    def test_no_matching_header(self):
+        request = self.factory.get('/')
+        request.META.pop('REMOTE_ADDR', None)
+        self.assertIsNone(get_client_ip(request))
+
+    def test_additional_headers_argument(self):
+        """Headers passed via `additional_headers` are checked after the configured defaults."""
+        request = self.factory.get('/', HTTP_CF_CONNECTING_IP='10.0.0.1')
+        request.META.pop('REMOTE_ADDR', None)
+        self.assertEqual(
+            get_client_ip(request, additional_headers=('HTTP_CF_CONNECTING_IP',)),
+            IPAddress('10.0.0.1'),
+        )
+
+    def test_default_headers_precede_additional_headers(self):
+        """Headers from HTTP_CLIENT_IP_HEADERS take precedence over `additional_headers`."""
+        request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1', HTTP_CF_CONNECTING_IP='10.0.0.1')
+        self.assertEqual(
+            get_client_ip(request, additional_headers=('HTTP_CF_CONNECTING_IP',)),
+            IPAddress('192.168.1.1'),
+        )
+
+    @override_settings(HTTP_CLIENT_IP_HEADERS=('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'))
+    def test_custom_configured_headers(self):
+        """get_client_ip() should honor the HTTP_CLIENT_IP_HEADERS setting."""
+        request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1', HTTP_CF_CONNECTING_IP='10.0.0.1')
+        self.assertEqual(get_client_ip(request), IPAddress('10.0.0.1'))