python-ipware 2.0.3__tar.gz → 2.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-ipware
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: A Python package to retrieve user's IP address
5
5
  Author-email: Val Neekman <info@neekware.com>
6
6
  License: MIT
@@ -216,23 +216,41 @@ If your python server is behind a `known` number of proxies, but you deploy on m
216
216
  You can customize the proxy count by providing your `proxy_count` during initialization when calling `IpWare(proxy_count=2)`.
217
217
 
218
218
  ```python
219
- # In the above scenario, the total number of proxies can be used as a way to filter out unwanted requests.
220
219
  from python_ipware import IpWare
221
220
 
222
- # enforce proxy count
223
- ipw = IpWare(proxy_count=1)
221
+ # Enforce proxy count
222
+ # proxy_count=0 is valid
223
+ # proxy_count=None to disable proxy_count check
224
+ ipw = IpWare(proxy_count=2)
224
225
 
225
- # enforce proxy count and trusted proxies
226
- ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
226
+ # Example usage in non-strict mode:
227
+ # X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
228
+ # At least `proxy_count` number of proxies
229
+ ip, trusted_route = ipw.get_client_ip(meta=request.META)
227
230
 
231
+ # Example usage in strict mode:
232
+ # X-Forwarded-For format: <client>, <proxy1>, <proxy2>
233
+ # Exact `proxy_count` number of proxies
234
+ ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
235
+ ```
228
236
 
229
- # usage: non-strict mode (X-Forwarded-For: <fake>, <client>, <proxy1>, <proxy2>)
230
- # total number of ip addresses are greater than the total count
231
- ip, trusted_route = ipw.get_client_ip(meta=request.META)
237
+ ### Proxy Count & Trusted Proxy List Combo
238
+ In this example, we utilize the total number of proxies as a method to filter out unwanted requests while verifying the trust proxies.
232
239
 
240
+ ```python
241
+ from python_ipware import IpWare
233
242
 
234
- # usage: strict mode (X-Forwarded-For: <client>, <proxy1>, <proxy2>)
235
- # total number of ip addresses are exactly equal to client ip + proxy_count
243
+ # Enforce both proxy count and trusted proxies
244
+ ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
245
+
246
+ # Example usage in non-strict mode:
247
+ # X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
248
+ # At least `proxy_count` number of proxies
249
+ ip, trusted_route = ipw.get_client_ip(meta=request.META)
250
+
251
+ # Example usage in strict mode:
252
+ # X-Forwarded-For format: <client>, <proxy1>
253
+ # Exact `proxy_count` number of proxies
236
254
  ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
237
255
  ```
238
256
 
@@ -185,23 +185,41 @@ If your python server is behind a `known` number of proxies, but you deploy on m
185
185
  You can customize the proxy count by providing your `proxy_count` during initialization when calling `IpWare(proxy_count=2)`.
186
186
 
187
187
  ```python
188
- # In the above scenario, the total number of proxies can be used as a way to filter out unwanted requests.
189
188
  from python_ipware import IpWare
190
189
 
191
- # enforce proxy count
192
- ipw = IpWare(proxy_count=1)
190
+ # Enforce proxy count
191
+ # proxy_count=0 is valid
192
+ # proxy_count=None to disable proxy_count check
193
+ ipw = IpWare(proxy_count=2)
193
194
 
194
- # enforce proxy count and trusted proxies
195
- ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
195
+ # Example usage in non-strict mode:
196
+ # X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
197
+ # At least `proxy_count` number of proxies
198
+ ip, trusted_route = ipw.get_client_ip(meta=request.META)
196
199
 
200
+ # Example usage in strict mode:
201
+ # X-Forwarded-For format: <client>, <proxy1>, <proxy2>
202
+ # Exact `proxy_count` number of proxies
203
+ ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
204
+ ```
197
205
 
198
- # usage: non-strict mode (X-Forwarded-For: <fake>, <client>, <proxy1>, <proxy2>)
199
- # total number of ip addresses are greater than the total count
200
- ip, trusted_route = ipw.get_client_ip(meta=request.META)
206
+ ### Proxy Count & Trusted Proxy List Combo
207
+ In this example, we utilize the total number of proxies as a method to filter out unwanted requests while verifying the trust proxies.
201
208
 
209
+ ```python
210
+ from python_ipware import IpWare
202
211
 
203
- # usage: strict mode (X-Forwarded-For: <client>, <proxy1>, <proxy2>)
204
- # total number of ip addresses are exactly equal to client ip + proxy_count
212
+ # Enforce both proxy count and trusted proxies
213
+ ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
214
+
215
+ # Example usage in non-strict mode:
216
+ # X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
217
+ # At least `proxy_count` number of proxies
218
+ ip, trusted_route = ipw.get_client_ip(meta=request.META)
219
+
220
+ # Example usage in strict mode:
221
+ # X-Forwarded-For format: <client>, <proxy1>
222
+ # Exact `proxy_count` number of proxies
205
223
  ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
206
224
  ```
207
225
 
@@ -0,0 +1 @@
1
+ __version__ = "2.0.5"
@@ -0,0 +1,335 @@
1
+ import ipaddress
2
+ import logging
3
+
4
+ from typing import Dict, List, Optional, Tuple, Union
5
+
6
+ IpAddressType = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
7
+ OptionalIpAddressType = Optional[IpAddressType]
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class IpWareMeta:
13
+ """
14
+ A class to manage metadata extracted from HTTP request headers, primarily for identifying
15
+ the client's IP address behind load balancers and proxies.
16
+ """
17
+
18
+ def __init__(
19
+ self, precedence: Optional[Tuple[str, ...]] = None, leftmost: bool = True
20
+ ) -> None:
21
+ """
22
+ Initializes the metadata handler with a precedence list of HTTP headers and the position of the client IP.
23
+
24
+ :param precedence: A tuple of header names that define the order to check for a valid IP.
25
+ If not provided, a default list of commonly used headers is applied.
26
+ :param leftmost: A boolean that indicates whether to use the left-most IP in the header
27
+ when multiple IPs are listed (typically the case with proxies).
28
+ """
29
+ # Default precedence list of headers if none is provided
30
+ if precedence is None:
31
+ precedence = (
32
+ "X_FORWARDED_FOR", # Common header for proxies, load balancers, like AWS ELB. Default to the left-most IP.
33
+ "HTTP_X_FORWARDED_FOR", # Alternative header similar to `X_FORWARDED_FOR`.
34
+ "HTTP_CLIENT_IP", # Header used by some providers like Amazon EC2, Heroku.
35
+ "HTTP_X_REAL_IP", # Header used by some providers like Amazon EC2, Heroku.
36
+ "HTTP_X_FORWARDED", # Used by Squid and similar software.
37
+ "HTTP_X_CLUSTER_CLIENT_IP", # Used by Rackspace LB, Riverbed Stingray.
38
+ "HTTP_FORWARDED_FOR", # Standard header defined by RFC 7239.
39
+ "HTTP_FORWARDED", # Standard header defined by RFC 7239.
40
+ "HTTP_CF_CONNECTING_IP", # Used by CloudFlare.
41
+ "X-CLIENT-IP", # Used by Microsoft Azure.
42
+ "X-REAL-IP", # Commonly used by NGINX.
43
+ "X-CLUSTER-CLIENT-IP", # Used by Rackspace Cloud Load Balancers.
44
+ "X_FORWARDED", # Used by Squid.
45
+ "FORWARDED_FOR", # Standard header defined by RFC 7239.
46
+ "CF-CONNECTING-IP", # Used by CloudFlare.
47
+ "TRUE-CLIENT-IP", # Header for CloudFlare Enterprise.
48
+ "FASTLY-CLIENT-IP", # Used by Fastly, Firebase.
49
+ "FORWARDED", # Standard header defined by RFC 7239.
50
+ "CLIENT-IP", # Used by Akamai, Cloudflare's True-Client-IP, and Fastly's Fastly-Client-IP.
51
+ "REMOTE_ADDR", # The default IP address header (direct connection).
52
+ )
53
+
54
+ self.precedence = precedence
55
+ self.leftmost = leftmost
56
+
57
+
58
+ class IpWareIpAddress:
59
+ """
60
+ A class for handling and parsing IP address data from HTTP requests.
61
+ """
62
+
63
+ def extract_ipv4(self, ip_address: str) -> str:
64
+ """
65
+ Extracts the IPv4 address from a given string that may include a port number.
66
+
67
+ Args:
68
+ ip_address (str): An IPv4 address possibly including a port (e.g., "192.168.1.1:8080").
69
+
70
+ Returns:
71
+ str: The IPv4 address without the port. Returns an empty string if input is None.
72
+ """
73
+ if ip_address:
74
+ return ip_address.split(":")[0].strip()
75
+ return ""
76
+
77
+ def extract_ipv6(self, ip_address: str) -> str:
78
+ """
79
+ Extracts the IPv6 address from a given string that may include a port number.
80
+
81
+ Args:
82
+ ip_address (str): An IPv6 address possibly including a port (e.g., "[2001:db8::1]:8080").
83
+
84
+ Returns:
85
+ str: The IPv6 address without brackets or port. Returns an empty string if input is None.
86
+ """
87
+ if ip_address and "]:" in ip_address:
88
+ return ip_address.split("]:")[0].replace("[", "").strip()
89
+ return ip_address.strip() if ip_address else ""
90
+
91
+ def parse_ip_address(self, ip_str: str):
92
+ """
93
+ Parses the given IP address string to an IPv4Address or IPv6Address object.
94
+
95
+ Args:
96
+ ip_str (str): An IP address possibly including a port.
97
+
98
+ Returns:
99
+ ipaddress.IPv4Address|ipaddress.IPv6Address|None: The parsed IP object or None if the address is invalid.
100
+ """
101
+ try:
102
+ # First, try to parse as IPv6.
103
+ ipv6 = self.extract_ipv6(ip_str)
104
+ ip = ipaddress.IPv6Address(ipv6)
105
+ return ip.ipv4_mapped or ip
106
+ except ipaddress.AddressValueError:
107
+ try:
108
+ # If IPv6 parsing fails, try to parse as IPv4.
109
+ ipv4 = self.extract_ipv4(ip_str)
110
+ return ipaddress.IPv4Address(ipv4)
111
+ except ipaddress.AddressValueError:
112
+ # Log error if IP is invalid
113
+ # print(f"Invalid IP address: {ip_str}")
114
+ return None
115
+
116
+ def get_ips_from_string(
117
+ self,
118
+ ip_str: str,
119
+ strict: bool = False,
120
+ ) -> Optional[List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]]:
121
+ """
122
+ Parses a comma-separated list of IP addresses, each possibly including a port.
123
+
124
+ Args:
125
+ ip_str (str): A comma-separated list of IP addresses or address:port entries.
126
+ strict (bool): True to bail out on first invalid ip, False to skip invalid ip
127
+ Returns:
128
+ List[ipaddress.IPv4Address|ipaddress.IPv6Address]|None: A list of IP address objects or None if any IP is invalid.
129
+ """
130
+ ip_list = []
131
+ for ip_address in ip_str.split(","):
132
+ ip = self.parse_ip_address(ip_address.strip())
133
+ if ip:
134
+ ip_list.append(ip)
135
+ else:
136
+ if strict:
137
+ return None
138
+
139
+ if not self.leftmost:
140
+ ip_list.reverse()
141
+
142
+ return ip_list
143
+
144
+
145
+ class IpWareProxy:
146
+ """
147
+ A class to handle proxy data from an HTTP request, including validating
148
+ proxy counts and trusted proxy lists.
149
+ """
150
+
151
+ def __init__(
152
+ self, proxy_count: Optional[int] = None, proxy_list: Optional[List[str]] = None
153
+ ) -> None:
154
+ """
155
+ Initialize the IpWareProxy class with optional proxy count and proxy list.
156
+
157
+ Args:
158
+ proxy_count: The expected number of proxies, can be None if no specific count is enforced.
159
+ proxy_list: A list of partial IP addresses as trusted proxies, can be None.
160
+ """
161
+ if proxy_count is not None and proxy_count < 0:
162
+ raise ValueError("proxy_count must be non-negative")
163
+ self.proxy_count = proxy_count
164
+ self.proxy_list = self._validate_proxy_list(proxy_list or [])
165
+
166
+ def _validate_proxy_list(self, proxy_list: List[str]) -> List[str]:
167
+ """
168
+ Validates that the proxy list contains only strings.
169
+
170
+ Args:
171
+ proxy_list: A list of proxies.
172
+
173
+ Returns:
174
+ The proxy list if all items are valid strings.
175
+
176
+ Raises:
177
+ ValueError: If the proxy list is not a list of strings.
178
+ """
179
+ if not all(isinstance(ip, str) for ip in proxy_list):
180
+ raise ValueError("All elements in the proxy list must be strings.")
181
+ return proxy_list
182
+
183
+ def is_proxy_count_valid(self, ip_list: List[str], strict: bool = False) -> bool:
184
+ """
185
+ Validates the proxy count against a list of IP addresses.
186
+
187
+ Args:
188
+ ip_list: A list of IP addresses from the request headers.
189
+ strict: If True, the number of proxies must exactly match the proxy_count.
190
+
191
+ Returns:
192
+ True if the proxy count is valid, False otherwise.
193
+ """
194
+ if self.proxy_count is None:
195
+ return True # No proxy count specified, so always valid.
196
+
197
+ ip_count = len(ip_list)
198
+ if strict:
199
+ return ip_count - 1 == self.proxy_count
200
+ # Exact match required, excluding client's own IP.
201
+
202
+ return ip_count - 1 >= self.proxy_count
203
+ # Allow more proxies than the count, excluding client's IP.
204
+
205
+ def is_proxy_trusted_list_valid(
206
+ self, ip_list: List[str], strict: bool = False
207
+ ) -> bool:
208
+ """
209
+ Checks if all proxies in the incoming list are trusted based on the proxy_list.
210
+
211
+ Args:
212
+ ip_list: A list of IP addresses from the request headers.
213
+ strict: If True, the number of proxies must exactly match the length of proxy_list.
214
+
215
+ Returns:
216
+ True if all proxies are trusted, False otherwise.
217
+ """
218
+ if not self.proxy_list:
219
+ return True # No specific proxies to trust, so always valid.
220
+
221
+ ip_count = len(ip_list)
222
+ proxy_list_count = len(self.proxy_list)
223
+
224
+ if strict and ip_count - 1 != proxy_list_count:
225
+ return False # Strict mode: Exact count match required.
226
+
227
+ if ip_count - 1 < proxy_list_count:
228
+ return False # Not enough IPs to match the trusted proxies.
229
+
230
+ # Verify each proxy against the trusted list by comparing each corresponding element.
231
+ return all(
232
+ str(ip).startswith(trusted_proxy_pattern)
233
+ for ip, trusted_proxy_pattern in zip(
234
+ ip_list[-proxy_list_count:], self.proxy_list
235
+ )
236
+ )
237
+
238
+
239
+ class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
240
+ """
241
+ A class that makes a best effort to determine the client's IP address.
242
+ """
243
+
244
+ def __init__(
245
+ self,
246
+ precedence: Optional[Tuple[str, ...]] = None,
247
+ leftmost: bool = True,
248
+ proxy_count: Optional[int] = None,
249
+ proxy_list: Optional[List[str]] = None,
250
+ ) -> None:
251
+ IpWareMeta.__init__(self, precedence, leftmost)
252
+ IpWareProxy.__init__(self, proxy_count, proxy_list)
253
+
254
+ def get_meta_value(self, meta: Dict[str, str], key: str) -> str:
255
+ """
256
+ Returns a cleaned up version of the value for a given key.
257
+ @param key: The key to look up.
258
+ @return: The value of the key or an empty string if the key is not found.
259
+ """
260
+ meta = meta or {}
261
+ return meta.get(key, meta.get(key.replace("_", "-"), "")).strip()
262
+
263
+ def get_meta_values(self, meta: Dict[str, str]) -> List[str]:
264
+ """
265
+ Returns a list of cleaned up values for the keys defined in 'precedence'.
266
+ @return: A list of values.
267
+ """
268
+ meta_list: List[str] = []
269
+ for key in self.precedence:
270
+ value = self.get_meta_value(meta, key).strip()
271
+ if value:
272
+ meta_list.append(value)
273
+ return meta_list
274
+
275
+ def get_client_ip(
276
+ self,
277
+ meta: Dict[str, str],
278
+ strict: bool = False,
279
+ ) -> Tuple[OptionalIpAddressType, bool]:
280
+ """
281
+ Returns the client's IP address.
282
+ """
283
+ loopback_list: List[IpAddressType] = []
284
+ private_list: List[OptionalIpAddressType] = []
285
+
286
+ for ip_str in self.get_meta_values(meta):
287
+ ip_list = self.get_ips_from_string(ip_str, strict)
288
+ if not ip_list:
289
+ continue
290
+
291
+ proxy_count_validated = self.is_proxy_count_valid(ip_list, strict)
292
+ if not proxy_count_validated:
293
+ continue
294
+
295
+ proxy_list_validated = self.is_proxy_trusted_list_valid(ip_list, strict)
296
+ if not proxy_list_validated:
297
+ continue
298
+
299
+ client_ip, trusted_route = self.get_best_ip(ip_list)
300
+ if client_ip is not None:
301
+ if client_ip.is_global:
302
+ return client_ip, trusted_route
303
+ if client_ip.is_loopback:
304
+ loopback_list.append(client_ip)
305
+ else:
306
+ private_list.append(client_ip)
307
+
308
+ if private_list:
309
+ return private_list[0], False
310
+ if loopback_list:
311
+ return loopback_list[0], False
312
+ return None, False
313
+
314
+ def get_best_ip(
315
+ self,
316
+ ip_list: List[IpAddressType],
317
+ ) -> Tuple[OptionalIpAddressType, bool]:
318
+ """
319
+ Determines the best possible IP address for the client from a list of IP addresses.
320
+ """
321
+ if not ip_list:
322
+ logger.warning("Invalid IP list provided.")
323
+ return None, False
324
+
325
+ if self.proxy_list and len(self.proxy_list) > 0:
326
+ best_client_ip_index = len(self.proxy_list) + 1
327
+ best_client_ip = ip_list[-best_client_ip_index]
328
+ return best_client_ip, True
329
+
330
+ if self.proxy_count is not None:
331
+ best_client_ip_index = self.proxy_count + 1
332
+ best_client_ip = ip_list[-best_client_ip_index]
333
+ return best_client_ip, True
334
+
335
+ return ip_list[0], False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-ipware
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: A Python package to retrieve user's IP address
5
5
  Author-email: Val Neekman <info@neekware.com>
6
6
  License: MIT
@@ -216,23 +216,41 @@ If your python server is behind a `known` number of proxies, but you deploy on m
216
216
  You can customize the proxy count by providing your `proxy_count` during initialization when calling `IpWare(proxy_count=2)`.
217
217
 
218
218
  ```python
219
- # In the above scenario, the total number of proxies can be used as a way to filter out unwanted requests.
220
219
  from python_ipware import IpWare
221
220
 
222
- # enforce proxy count
223
- ipw = IpWare(proxy_count=1)
221
+ # Enforce proxy count
222
+ # proxy_count=0 is valid
223
+ # proxy_count=None to disable proxy_count check
224
+ ipw = IpWare(proxy_count=2)
224
225
 
225
- # enforce proxy count and trusted proxies
226
- ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
226
+ # Example usage in non-strict mode:
227
+ # X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
228
+ # At least `proxy_count` number of proxies
229
+ ip, trusted_route = ipw.get_client_ip(meta=request.META)
227
230
 
231
+ # Example usage in strict mode:
232
+ # X-Forwarded-For format: <client>, <proxy1>, <proxy2>
233
+ # Exact `proxy_count` number of proxies
234
+ ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
235
+ ```
228
236
 
229
- # usage: non-strict mode (X-Forwarded-For: <fake>, <client>, <proxy1>, <proxy2>)
230
- # total number of ip addresses are greater than the total count
231
- ip, trusted_route = ipw.get_client_ip(meta=request.META)
237
+ ### Proxy Count & Trusted Proxy List Combo
238
+ In this example, we utilize the total number of proxies as a method to filter out unwanted requests while verifying the trust proxies.
232
239
 
240
+ ```python
241
+ from python_ipware import IpWare
233
242
 
234
- # usage: strict mode (X-Forwarded-For: <client>, <proxy1>, <proxy2>)
235
- # total number of ip addresses are exactly equal to client ip + proxy_count
243
+ # Enforce both proxy count and trusted proxies
244
+ ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
245
+
246
+ # Example usage in non-strict mode:
247
+ # X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
248
+ # At least `proxy_count` number of proxies
249
+ ip, trusted_route = ipw.get_client_ip(meta=request.META)
250
+
251
+ # Example usage in strict mode:
252
+ # X-Forwarded-For format: <client>, <proxy1>
253
+ # Exact `proxy_count` number of proxies
236
254
  ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
237
255
  ```
238
256
 
@@ -77,12 +77,27 @@ class TestIPv4Common(unittest.TestCase):
77
77
  "HTTP_X_FORWARDED_FOR": "unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158",
78
78
  }
79
79
  r = self.ipware.get_client_ip(meta)
80
- self.assertEqual(r, (None, False))
80
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), False))
81
81
 
82
- def test_error_first(self):
82
+ def test_error_only_strict(self):
83
83
  meta = {
84
84
  "HTTP_X_FORWARDED_FOR": "unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158",
85
+ }
86
+ r = self.ipware.get_client_ip(meta, strict=True)
87
+ self.assertEqual(r, (None, False))
88
+
89
+ def test_error_only_first_strict(self):
90
+ meta = {
85
91
  "X_FORWARDED_FOR": "177.139.233.138, 198.84.193.157, 198.84.193.158",
92
+ "HTTP_X_FORWARDED_FOR": "unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158",
93
+ }
94
+ r = self.ipware.get_client_ip(meta, strict=True)
95
+ self.assertEqual(r, (IPv4Address("177.139.233.138"), False))
96
+
97
+ def test_error_first(self):
98
+ meta = {
99
+ "X_FORWARDED_FOR": "unknown, 177.139.233.138, 198.84.193.157, 198.84.193.158",
100
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
86
101
  }
87
102
  r = self.ipware.get_client_ip(meta)
88
103
  self.assertEqual(r, (IPv4Address("177.139.233.138"), False))
@@ -175,39 +190,69 @@ class TestIPv4Common(unittest.TestCase):
175
190
  class TestIPv4ProxyCount(unittest.TestCase):
176
191
  """IPv4 Proxy Count Test"""
177
192
 
178
- def setUp(self):
179
- self.ipware = IpWare(proxy_count=1)
180
-
181
- def tearDown(self):
182
- self.ipware = None
183
-
184
- def test_singleton_proxy_count(self):
193
+ def test_proxy_count_one_missing_proxy_fail(self):
194
+ ipware = IpWare(proxy_count=1)
185
195
  meta = {
186
196
  "HTTP_X_FORWARDED_FOR": "177.139.233.139",
187
197
  }
188
- r = self.ipware.get_client_ip(meta)
198
+ r = ipware.get_client_ip(meta)
199
+
189
200
  self.assertEqual(r, (None, False))
190
201
 
191
- def test_singleton_proxy_count_private(self):
202
+ def test_proxy_count_one_at_least_one_proxy_pass(self):
203
+ ipware = IpWare(proxy_count=1)
192
204
  meta = {
193
- "HTTP_X_FORWARDED_FOR": "10.0.0.0",
194
- "HTTP_X_REAL_IP": "177.139.233.139",
205
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
195
206
  }
196
- r = self.ipware.get_client_ip(meta)
207
+ r = ipware.get_client_ip(meta)
208
+ self.assertEqual(r, (IPv4Address("198.84.193.157"), True))
209
+
210
+ def test_proxy_count_one_exactly_one_proxy_fail(self):
211
+ ipware = IpWare(proxy_count=1)
212
+ meta = {
213
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
214
+ }
215
+ r = ipware.get_client_ip(meta, strict=True)
197
216
  self.assertEqual(r, (None, False))
198
217
 
199
- def test_proxy_count_relax(self):
218
+ def test_proxy_count_one_exactly_one_proxy_pass(self):
219
+ ipware = IpWare(proxy_count=1)
220
+ meta = {
221
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157",
222
+ }
223
+ r = ipware.get_client_ip(meta, strict=True)
224
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
225
+
226
+ def test_proxy_count_one_dont_care_proxy_pass(self):
227
+ ipware = IpWare()
200
228
  meta = {
201
229
  "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
202
230
  }
203
- r = self.ipware.get_client_ip(meta, strict=False)
204
- self.assertEqual(r, (IPv4Address("198.84.193.157"), True))
231
+ r = ipware.get_client_ip(meta)
232
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), False))
205
233
 
206
- def test_proxy_count_strict(self):
234
+ def test_proxy_count_zero_dont_care_proxy_pass(self):
235
+ ipware = IpWare(proxy_count=0)
207
236
  meta = {
208
- "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.158",
237
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
209
238
  }
210
- r = self.ipware.get_client_ip(meta, strict=True)
239
+ r = ipware.get_client_ip(meta)
240
+ self.assertEqual(r, (IPv4Address("198.84.193.158"), True))
241
+
242
+ def test_proxy_count_zero_exact_zero_proxy_pass(self):
243
+ ipware = IpWare(proxy_count=0)
244
+ meta = {
245
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139",
246
+ }
247
+ r = ipware.get_client_ip(meta, strict=True)
248
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
249
+
250
+ def test_proxy_count_zero_exact_zero_proxy_fail(self):
251
+ ipware = IpWare(proxy_count=0)
252
+ meta = {
253
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
254
+ }
255
+ r = ipware.get_client_ip(meta, strict=True)
211
256
  self.assertEqual(r, (None, False))
212
257
 
213
258
 
@@ -245,26 +290,68 @@ class TestIPv4ProxyList(unittest.TestCase):
245
290
  class TestIPv4ProxyCountProxyList(unittest.TestCase):
246
291
  """IPv4 Proxy Count Test"""
247
292
 
248
- def setUp(self):
249
- self.ipware = IpWare(
250
- proxy_count=2, proxy_list=["198.84.193.157", "198.84.193.158"]
251
- )
293
+ def test_proxy_list_relax(self):
294
+ ipware = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"])
295
+ meta = {
296
+ "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
297
+ }
298
+ r = ipware.get_client_ip(meta)
299
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
252
300
 
253
- def tearDown(self):
254
- self.ipware = None
301
+ def test_proxy_list_strict_pass(self):
302
+ ipware = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"])
303
+ meta = {
304
+ "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
305
+ }
306
+ r = ipware.get_client_ip(meta, strict=True)
307
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
255
308
 
256
- def test_proxy_list_relax(self):
309
+ def test_proxy_list_strict_fail(self):
310
+ ipware = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"])
257
311
  meta = {
258
312
  "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
259
313
  }
260
- r = self.ipware.get_client_ip(meta)
314
+ r = ipware.get_client_ip(meta, strict=True)
315
+ self.assertEqual(r, (None, False))
316
+
317
+ def test_proxy_list_relax_exact_pass(self):
318
+ ipware = IpWare(proxy_count=2, proxy_list=["198.84.193.157", "198.84.193.158"])
319
+ meta = {
320
+ "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
321
+ }
322
+ r = ipware.get_client_ip(meta)
261
323
  self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
262
324
 
263
- def test_proxy_list_strict(self):
325
+ def test_proxy_list_relax_count_under_pass(self):
326
+ ipware = IpWare(proxy_count=1, proxy_list=["198.84.193.157", "198.84.193.158"])
264
327
  meta = {
265
328
  "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
266
329
  }
267
- r = self.ipware.get_client_ip(meta, strict=True)
330
+ r = ipware.get_client_ip(meta)
331
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
332
+
333
+ def test_proxy_list_relax_count_over_pass(self):
334
+ ipware = IpWare(proxy_count=5, proxy_list=["198.84.193.157", "198.84.193.158"])
335
+ meta = {
336
+ "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
337
+ }
338
+ r = ipware.get_client_ip(meta)
339
+ self.assertEqual(r, (None, False))
340
+
341
+ def test_proxy_list_count_exact_pass(self):
342
+ ipware = IpWare(proxy_count=2, proxy_list=["198.84.193.157", "198.84.193.158"])
343
+ meta = {
344
+ "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
345
+ }
346
+ r = ipware.get_client_ip(meta)
347
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
348
+
349
+ def test_proxy_list_count_exact_fail(self):
350
+ ipware = IpWare(proxy_count=4, proxy_list=["198.84.193.157", "198.84.193.158"])
351
+ meta = {
352
+ "HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
353
+ }
354
+ r = ipware.get_client_ip(meta)
268
355
  self.assertEqual(r, (None, False))
269
356
 
270
357
 
@@ -69,14 +69,22 @@ class TestIPv6Common(unittest.TestCase):
69
69
  "X_FORWARDED_FOR": "unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 2606:4700:4700::1111, 2001:4860:4860::8888",
70
70
  }
71
71
  r = self.ipware.get_client_ip(meta)
72
+ self.assertEqual(r, (IPv6Address("3ffe:1900:4545:3:200:f8ff:fe21:67cf"), False))
73
+
74
+ def test_error_only_strict(self):
75
+ meta = {
76
+ "X_FORWARDED_FOR": "unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 2606:4700:4700::1111, 2001:4860:4860::8888",
77
+ }
78
+ r = self.ipware.get_client_ip(meta, strict=True)
72
79
  self.assertEqual(r, (None, False))
73
80
 
74
81
  def test_first_error_bailout(self):
75
82
  meta = {
76
83
  "HTTP_X_FORWARDED_FOR": "unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 2606:4700:4700::1111, 2001:4860:4860::8888",
84
+ "X_FORWARDED_FOR": "2606:4700:4700::1111, 2001:4860:4860::8888",
77
85
  }
78
86
  r = self.ipware.get_client_ip(meta)
79
- self.assertEqual(r, (None, False))
87
+ self.assertEqual(r, (IPv6Address("2606:4700:4700::1111"), False))
80
88
 
81
89
  def test_with_error_best_match(self):
82
90
  meta = {
@@ -1 +0,0 @@
1
- __version__ = "2.0.3"
@@ -1,346 +0,0 @@
1
- import ipaddress
2
- import logging
3
-
4
- from typing import Any, Dict, List, Optional, Tuple, Union
5
-
6
- IpAddressType = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
7
- OptionalIpAddressType = Optional[IpAddressType]
8
-
9
- logger = logging.getLogger(__name__)
10
-
11
-
12
- class IpWareMeta:
13
- """
14
- A class that handles meta data frm an HTTP request.
15
- """
16
-
17
- def __init__(
18
- self,
19
- precedence: Optional[Tuple[str, ...]] = None,
20
- leftmost: bool = True,
21
- ) -> None:
22
- self.precedence = precedence or (
23
- "X_FORWARDED_FOR", # Load balancers or proxies such as AWS ELB (default client is `left-most` [`<client>, <proxy1>, <proxy2>`])
24
- "HTTP_X_FORWARDED_FOR", # Similar to X_FORWARDED_TO
25
- "HTTP_CLIENT_IP", # Standard headers used by providers such as Amazon EC2, Heroku etc.
26
- "HTTP_X_REAL_IP", # Standard headers used by providers such as Amazon EC2, Heroku etc.
27
- "HTTP_X_FORWARDED", # Squid and others
28
- "HTTP_X_CLUSTER_CLIENT_IP", # Rackspace LB and Riverbed Stingray
29
- "HTTP_FORWARDED_FOR", # RFC 7239
30
- "HTTP_FORWARDED", # RFC 7239
31
- "HTTP_CF_CONNECTING_IP", # CloudFlare
32
- "X-CLIENT-IP", # Microsoft Azure
33
- "X-REAL-IP", # NGINX
34
- "X-CLUSTER-CLIENT-IP", # Rackspace Cloud Load Balancers
35
- "X_FORWARDED", # Squid
36
- "FORWARDED_FOR", # RFC 7239
37
- "CF-CONNECTING-IP", # CloudFlare
38
- "TRUE-CLIENT-IP", # CloudFlare Enterprise,
39
- "FASTLY-CLIENT-IP", # Firebase, Fastly
40
- "FORWARDED", # RFC 7239
41
- "CLIENT-IP", # Akamai and Cloudflare: True-Client-IP and Fastly: Fastly-Client-IP
42
- "REMOTE_ADDR", # Default
43
- )
44
- self.leftmost = leftmost
45
-
46
-
47
- class IpWareIpAddress:
48
- """
49
- A class that handles IP address data from an HTTP request.
50
- """
51
-
52
- def extract_ipv4_only(self, ip_address: str) -> str:
53
- """
54
- Given an IPv4 address or address:port, it extracts the IP address
55
- @param ip_str: IP address or address:port
56
- @return: IP address
57
- """
58
-
59
- if ip_address:
60
- # handle ipv4 addresses with port
61
- if ":" in ip_address:
62
- ip_address = ip_address.split(":")[0]
63
- return ip_address.strip()
64
-
65
- return ip_address.strip()
66
-
67
- return ""
68
-
69
- def extract_ipv6_only(self, ip_address: str) -> str:
70
- """
71
- Given an IPv6 address or address:port, it extracts the IP address
72
- @param ip_str: IP address or address:port
73
- @return: IP address
74
- """
75
-
76
- if ip_address:
77
- # handle ipv6 addresses with port
78
- if "]:" in ip_address:
79
- ip_address = ip_address.split("]:")[0]
80
- ip_address = ip_address.replace("[", "")
81
- return ip_address.strip()
82
-
83
- return ip_address.strip()
84
-
85
- return ""
86
-
87
- def get_ip_object(
88
- self,
89
- ip_str: str,
90
- ) -> OptionalIpAddressType:
91
- """
92
- Given an IP address or address:port, it parses the IP address
93
- @param ip_str: IP address or address:port
94
- @return: IP address of type IPv4Address or IPv6Address
95
- """
96
-
97
- ip: OptionalIpAddressType = None
98
- if ip_str:
99
- try:
100
- # try to parse as IPv6 address with optional port
101
- ipv6 = self.extract_ipv6_only(ip_str)
102
- ip = ipaddress.IPv6Address(ipv6)
103
- ip = ip.ipv4_mapped or ip
104
- except ipaddress.AddressValueError:
105
- try:
106
- # try to parse as IPv4 address with optional port
107
- ipv4 = self.extract_ipv4_only(ip_str)
108
- ip = ipaddress.IPv4Address(ipv4)
109
- except ipaddress.AddressValueError:
110
- # not a valid IP address, return None
111
- logger.info("Invalid ip address. {0}".format(ip_str))
112
- ip = None
113
- return ip
114
-
115
- def get_ips_from_string(
116
- self,
117
- ip_str: str,
118
- ) -> Optional[List[IpAddressType]]:
119
- """
120
- Given a comma separated list of IP addresses or address:port, it parses the IP addresses
121
- @param ip_str: comma separated list of IP addresses or address:port
122
- @return: list of IP addresses of type IPv4Address or IPv6Address
123
- """
124
- ip_list: List[IpAddressType] = []
125
-
126
- for ip_address in ip_str.split(","):
127
- ip = self.get_ip_object(ip_address.strip())
128
- if ip:
129
- ip_list.append(ip)
130
- else:
131
- # we have at least one invalid IP address, return empty list, instead
132
- return None
133
-
134
- if not self.leftmost:
135
- ip_list.reverse()
136
-
137
- return ip_list
138
-
139
-
140
- class IpWareProxy:
141
- """
142
- A class that handles proxy data from an HTTP request.
143
- """
144
-
145
- def __init__(
146
- self,
147
- proxy_count: int = 0,
148
- proxy_list: Optional[List[str]] = None,
149
- ) -> None:
150
- if proxy_count is None or proxy_count < 0:
151
- raise ValueError("proxy_count must be a positive integer")
152
-
153
- self.proxy_count = proxy_count
154
- self.proxy_list = self._is_valid_proxy_trusted_list(proxy_list or [])
155
-
156
- def _is_valid_proxy_trusted_list(self, proxy_list: Any) -> List[str]:
157
- """
158
- Checks if the proxy list is a valid list of strings
159
- @return: proxy list or raises an exception
160
- """
161
-
162
- if not isinstance(proxy_list, list):
163
- raise ValueError("Parameter must be a list")
164
- if not all(isinstance(x, str) for x in proxy_list):
165
- raise ValueError("All elements in list must be strings")
166
-
167
- return proxy_list
168
-
169
- def is_proxy_count_valid(
170
- self, ip_list: List[IpAddressType], strict: bool = False
171
- ) -> bool:
172
- """
173
- Checks if the proxy count is valid
174
- @param ip_list: list of ip addresses
175
- @param strict: if True, we must have exactly proxy_count proxies
176
- @return: True if the proxy count is valid, False otherwise
177
- """
178
- if self.proxy_count < 1:
179
- return True
180
-
181
- ip_count: int = len(ip_list)
182
- if ip_count < 1:
183
- return False
184
-
185
- if strict:
186
- # our first proxy takes the last ip address and treats it as client ip
187
- return self.proxy_count == ip_count - 1
188
-
189
- # the client could have gone through their own proxy and included extra ips
190
- # client could be sending in the header: X-Forwarded-For: <fake_ip>, <client_ip>
191
- return ip_count - 1 > self.proxy_count
192
-
193
- def is_proxy_trusted_list_valid(
194
- self,
195
- ip_list: List[IpAddressType],
196
- strict: bool = False,
197
- ) -> bool:
198
- """
199
- Checks if the proxy list is valid (all proxies are in the proxy_list)
200
- @param ip_list: list of ip addresses
201
- @param strict: if True, we must have exactly proxy_count proxies
202
- @return: client's best match ip address or False
203
- """
204
- if not self.proxy_list:
205
- return True
206
-
207
- ip_count = len(ip_list)
208
- proxy_list_count = len(self.proxy_list)
209
-
210
- # in strict mode, total ip count must be 1 more than proxy count
211
- if strict and ip_count - 1 != proxy_list_count:
212
- return False
213
-
214
- # total ip count (client + proxies) must be more than proxy count
215
- if ip_count - 1 < proxy_list_count:
216
- return False
217
-
218
- # start from the end, slice the incoming ip list to the same length as the trusted proxy list
219
- ip_list_slice = ip_list[-proxy_list_count:]
220
- for index, value in enumerate(ip_list_slice):
221
- if not str(value).startswith(self.proxy_list[index]):
222
- return False
223
-
224
- # now all we need is to return the first ip in the list that is not in the trusted proxy list
225
- # best_client_ip_index = proxy_list_count + 1
226
- # best_client_ip = ip_list[-best_client_ip_index]
227
-
228
- return True
229
-
230
-
231
- class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
232
- """
233
- A class that makes best effort to determine the client's IP address.
234
- """
235
-
236
- def __init__(
237
- self,
238
- precedence: Optional[Tuple[str, ...]] = None,
239
- leftmost: bool = True,
240
- proxy_count: int = 0,
241
- proxy_list: Optional[List[str]] = None,
242
- ) -> None:
243
- IpWareMeta.__init__(self, precedence, leftmost)
244
- IpWareProxy.__init__(self, proxy_count or 0, proxy_list or [])
245
-
246
- def get_meta_value(self, meta: Dict[str, str], key: str) -> str:
247
- """
248
- Given a key, it returns a cleaned up version of the value
249
- @param key: the key to lookup
250
- @return: the value of the key or empty string
251
- """
252
- meta = meta or {}
253
- return meta.get(key, meta.get(key.replace("_", "-"), "")).strip()
254
-
255
- def get_meta_values(self, meta: Dict[str, str]) -> List[str]:
256
- """
257
- Given a list of keys, it returns a list of cleaned up values
258
- @return: a list of values
259
- """
260
- return [self.get_meta_value(meta, key) for key in self.precedence]
261
-
262
- def get_client_ip(
263
- self,
264
- meta: Dict[str, str],
265
- strict: bool = False,
266
- ) -> Tuple[OptionalIpAddressType, bool]:
267
- """
268
- Returns the client's IP address.
269
- """
270
-
271
- loopback_list: List[IpAddressType] = []
272
- private_list: List[OptionalIpAddressType] = []
273
-
274
- for ip_str in self.get_meta_values(meta):
275
- if not ip_str:
276
- continue
277
-
278
- ip_list = self.get_ips_from_string(ip_str)
279
- if not ip_list:
280
- continue
281
-
282
- proxy_count_validated = self.is_proxy_count_valid(ip_list, strict)
283
- if not proxy_count_validated:
284
- continue
285
-
286
- proxy_list_validated = self.is_proxy_trusted_list_valid(ip_list, strict)
287
- if not proxy_list_validated:
288
- continue
289
-
290
- client_ip, trusted_route = self.get_best_ip(
291
- ip_list, proxy_count_validated, proxy_list_validated
292
- )
293
-
294
- # we found a global ip, return it
295
- if client_ip is not None and client_ip.is_global:
296
- return client_ip, trusted_route
297
-
298
- # we found a private ip, save it
299
- if client_ip is not None and client_ip.is_loopback:
300
- loopback_list.append(client_ip)
301
- else:
302
- # if not global (public) or loopback (local), we treat it asd private
303
- private_list.append(client_ip)
304
-
305
- # we have not been able to locate a global ip
306
- # it could be the server is running on the intranet
307
- # we will return the first private ip we found
308
- if private_list:
309
- return private_list[0], False
310
-
311
- # we have not been able to locate a global ip, nor a private ip
312
- # it could be the server is running on a loopback address serving local requests
313
- if loopback_list:
314
- return loopback_list[0], False
315
-
316
- # we were unable to find any ip address
317
- return None, False
318
-
319
- def get_best_ip(
320
- self,
321
- ip_list: List[IpAddressType],
322
- proxy_count_validated: bool = True,
323
- proxy_list_validated: bool = True,
324
- ) -> Tuple[OptionalIpAddressType, bool]:
325
- """
326
- Returns the best possible ip for the client.
327
- """
328
-
329
- if not ip_list:
330
- logger.warning("Invalid ip list provided.")
331
- return None, False
332
-
333
- # the incoming ips match our trusted proxy list
334
- if len(self.proxy_list) > 0 and proxy_list_validated:
335
- best_client_ip_index = len(self.proxy_list) + 1
336
- best_client_ip = ip_list[-best_client_ip_index]
337
- return best_client_ip, True
338
-
339
- # the incoming ips match our proxy count
340
- if self.proxy_count > 0 and proxy_count_validated:
341
- best_client_ip_index = self.proxy_count + 1
342
- best_client_ip = ip_list[-best_client_ip_index]
343
- return best_client_ip, True
344
-
345
- # we don't track proxy related info, so we just return the first ip
346
- return ip_list[0], False
File without changes
File without changes