Skip to content

Commit a0455e3

Browse files
authored
Configure ip headers (#58)
Adds a way of configuring the SDK to extract IP headers.
1 parent 464243a commit a0455e3

File tree

6 files changed

+77
-0
lines changed

6 files changed

+77
-0
lines changed

HISTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [#59](https://github.com/castle/castle-python/pull/59) drop requests min version in ci
44
- [#56](https://github.com/castle/castle-python/pull/56) drop special ip header behavior
5+
- [#58](https://github.com/castle/castle-python/pull/58) Adds `ip_header` configuration option
56

67
### Breaking Changes:
78

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ import and configure the library with your Castle API secret.
4343
# some headers are always scrubbed, for security reasons.
4444
configuration.blacklisted = ['HTTP-X-header']
4545
46+
# Castle needs the original IP of the client, not the IP of your proxy or load balancer.
47+
# If that IP is sent as a header you can configure the SDK to extract it automatically.
48+
# Note that format, it should be prefixed with `HTTP`, capitalized and separated by underscores.
49+
configuration.ip_headers = ["HTTP_X_FORWARDED_FOR"]
50+
4651
Tracking
4752
--------
4853

castle/configuration.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(self):
3737
self.blacklisted = []
3838
self.request_timeout = REQUEST_TIMEOUT
3939
self.failover_strategy = 'allow'
40+
self.ip_headers = []
4041

4142
@property
4243
def api_secret(self):
@@ -111,6 +112,17 @@ def failover_strategy(self, value):
111112
else:
112113
raise ConfigurationError
113114

115+
@property
116+
def ip_headers(self):
117+
return self.__ip_headers
118+
119+
@ip_headers.setter
120+
def ip_headers(self, value):
121+
if isinstance(value, list):
122+
self.__ip_headers = value
123+
else:
124+
raise ConfigurationError
125+
114126

115127
# pylint: disable=invalid-name
116128
configuration = Configuration()

castle/extractors/ip.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
from castle.configuration import configuration
2+
3+
14
class ExtractorsIp(object):
25
def __init__(self, request):
36
self.request = request
47

58
def call(self):
9+
ip_address = self.get_ip_from_headers()
10+
if ip_address:
11+
return ip_address
12+
613
if hasattr(self.request, 'ip'):
714
return self.request.ip
815

916
return self.request.environ.get('REMOTE_ADDR')
17+
18+
def get_ip_from_headers(self):
19+
for header in configuration.ip_headers:
20+
value = self.request.environ.get(header)
21+
if value:
22+
return value
23+
return None

castle/test/configuration_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def test_default_values(self):
1515
self.assertEqual(config.blacklisted, [])
1616
self.assertEqual(config.request_timeout, 500)
1717
self.assertEqual(config.failover_strategy, 'allow')
18+
self.assertEqual(config.ip_headers, [])
1819

1920
def test_api_secret_setter(self):
2021
config = Configuration()
@@ -80,3 +81,14 @@ def test_failover_strategy_setter_invalid(self):
8081
config = Configuration()
8182
with self.assertRaises(ConfigurationError):
8283
config.failover_strategy = 'invalid'
84+
85+
def test_ip_headers_setter_valid(self):
86+
config = Configuration()
87+
ip_headers = ['HTTP_X_FORWARDED_FOR']
88+
config.ip_headers = ip_headers
89+
self.assertEqual(config.ip_headers, ip_headers)
90+
91+
def test_ip_headers_setter_invalid(self):
92+
config = Configuration()
93+
with self.assertRaises(ConfigurationError):
94+
config.ip_headers = 'invalid'

castle/test/extractors/ip_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from castle.test import unittest, mock
22
from castle.extractors.ip import ExtractorsIp
3+
from castle.configuration import configuration
34

45

56
def request_ip():
@@ -22,7 +23,23 @@ def request_with_ip_remote_addr():
2223
return req
2324

2425

26+
def request_with_ip_x_forwarded_for():
27+
req = mock.Mock(spec=['environ'])
28+
req.environ = {'HTTP_X_FORWARDED_FOR': request_ip()}
29+
return req
30+
31+
32+
def request_with_ip_cf_connecting_ip():
33+
req = mock.Mock(spec=['environ'])
34+
req.environ = {'HTTP_CF_CONNECTING_IP': request_ip_next()}
35+
return req
36+
37+
2538
class ExtractorsIpTestCase(unittest.TestCase):
39+
@classmethod
40+
def tearDownClass(cls):
41+
configuration.ip_headers = []
42+
2643
def test_extract_ip(self):
2744
self.assertEqual(ExtractorsIp(request()).call(), request_ip())
2845

@@ -31,3 +48,19 @@ def test_extract_ip_from_wsgi_request_remote_addr(self):
3148
ExtractorsIp(request_with_ip_remote_addr()).call(),
3249
request_ip()
3350
)
51+
52+
def test_extract_ip_from_wsgi_request_configured_ip_header_first(self):
53+
configuration.ip_headers = ["HTTP_CF_CONNECTING_IP"]
54+
self.assertEqual(
55+
ExtractorsIp(request_with_ip_cf_connecting_ip()).call(),
56+
request_ip_next()
57+
)
58+
configuration.ip_headers = []
59+
60+
def test_extract_ip_from_wsgi_request_configured_ip_header_second(self):
61+
configuration.ip_headers = ["HTTP_CF_CONNECTING_IP", "HTTP_X_FORWARDED_FOR"]
62+
self.assertEqual(
63+
ExtractorsIp(request_with_ip_x_forwarded_for()).call(),
64+
request_ip()
65+
)
66+
configuration.ip_headers = []

0 commit comments

Comments
 (0)