python-ipware 2.0.4__tar.gz → 3.0.0__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.4
3
+ Version: 3.0.0
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
@@ -0,0 +1 @@
1
+ __version__ = "3.0.0"
@@ -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.4
3
+ Version: 3.0.0
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
@@ -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))
@@ -222,7 +237,7 @@ class TestIPv4ProxyCount(unittest.TestCase):
222
237
  "HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
223
238
  }
224
239
  r = ipware.get_client_ip(meta)
225
- self.assertEqual(r, (IPv4Address("177.139.233.139"), False))
240
+ self.assertEqual(r, (IPv4Address("198.84.193.158"), True))
226
241
 
227
242
  def test_proxy_count_zero_exact_zero_proxy_pass(self):
228
243
  ipware = IpWare(proxy_count=0)
@@ -230,7 +245,7 @@ class TestIPv4ProxyCount(unittest.TestCase):
230
245
  "HTTP_X_FORWARDED_FOR": "177.139.233.139",
231
246
  }
232
247
  r = ipware.get_client_ip(meta, strict=True)
233
- self.assertEqual(r, (IPv4Address("177.139.233.139"), False))
248
+ self.assertEqual(r, (IPv4Address("177.139.233.139"), True))
234
249
 
235
250
  def test_proxy_count_zero_exact_zero_proxy_fail(self):
236
251
  ipware = IpWare(proxy_count=0)
@@ -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.4"
@@ -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: Optional[int] = None,
148
- proxy_list: Optional[List[str]] = None,
149
- ) -> None:
150
- self.proxy_count = proxy_count
151
- self.proxy_list = self._is_valid_proxy_trusted_list(proxy_list or [])
152
-
153
- def _is_valid_proxy_trusted_list(self, proxy_list: Any) -> List[str]:
154
- """
155
- Checks if the proxy list is a valid list of strings
156
- @return: proxy list or raises an exception
157
- """
158
-
159
- if not isinstance(proxy_list, list):
160
- raise ValueError("Parameter must be a list")
161
- if not all(isinstance(x, str) for x in proxy_list):
162
- raise ValueError("All elements in list must be strings")
163
-
164
- return proxy_list
165
-
166
- def is_proxy_count_valid(
167
- self, ip_list: List[IpAddressType], strict: bool = False
168
- ) -> bool:
169
- """
170
- Checks if the proxy count is valid
171
- @param ip_list: list of ip addresses
172
- @param strict: if True, we must have exactly proxy_count proxies, including `0` proxies
173
- @return: True if the proxy count is valid, False otherwise
174
- """
175
- # No proxy count check is required
176
- if self.proxy_count is None:
177
- return True
178
-
179
- ip_count: int = len(ip_list)
180
-
181
- if strict:
182
- # our first proxy takes the last ip address and treats it as client ip
183
- return self.proxy_count == ip_count - 1
184
-
185
- # the client could have gone through their own proxy and included extra ips
186
- # client could be sending in the header: X-Forwarded-For: <fake_ip>, <client_ip>
187
- return ip_count - 1 > self.proxy_count
188
-
189
- def is_proxy_trusted_list_valid(
190
- self,
191
- ip_list: List[IpAddressType],
192
- strict: bool = False,
193
- ) -> bool:
194
- """
195
- Checks if the proxy list is valid (all proxies are in the proxy_list)
196
- @param ip_list: list of ip addresses
197
- @param strict: if True, we must have exactly proxy_count proxies
198
- @return: client's best match ip address or False
199
- """
200
- if not self.proxy_list:
201
- return True
202
-
203
- ip_count = len(ip_list)
204
- proxy_list_count = len(self.proxy_list)
205
-
206
- # in strict mode, total ip count must be 1 more than proxy count
207
- if strict and ip_count - 1 != proxy_list_count:
208
- return False
209
-
210
- # total ip count (client + proxies) must be more than proxy count
211
- if ip_count - 1 < proxy_list_count:
212
- return False
213
-
214
- # start from the end, slice the incoming ip list to the same length as the trusted proxy list
215
- ip_list_slice = ip_list[-proxy_list_count:]
216
- for index, value in enumerate(ip_list_slice):
217
- if not str(value).startswith(self.proxy_list[index]):
218
- return False
219
-
220
- return True
221
-
222
-
223
- class IpWare(IpWareMeta, IpWareProxy, IpWareIpAddress):
224
- """
225
- A class that makes best effort to determine the client's IP address.
226
- """
227
-
228
- def __init__(
229
- self,
230
- precedence: Optional[Tuple[str, ...]] = None,
231
- leftmost: bool = True,
232
- proxy_count: Optional[int] = None,
233
- proxy_list: Optional[List[str]] = None,
234
- ) -> None:
235
- IpWareMeta.__init__(self, precedence, leftmost)
236
- IpWareProxy.__init__(self, proxy_count, proxy_list)
237
-
238
- def get_meta_value(self, meta: Dict[str, str], key: str) -> str:
239
- """
240
- Given a key, it returns a cleaned up version of the value
241
- @param key: the key to lookup
242
- @return: the value of the key or empty string
243
- """
244
- meta = meta or {}
245
- return meta.get(key, meta.get(key.replace("_", "-"), "")).strip()
246
-
247
- def get_meta_values(self, meta: Dict[str, str]) -> List[str]:
248
- """
249
- Given a list of keys, it returns a list of cleaned up values
250
- @return: a list of values
251
- """
252
- meta_list: List[str] = []
253
- for key in self.precedence:
254
- value = self.get_meta_value(meta, key).strip()
255
- if value:
256
- meta_list.append(value)
257
-
258
- return meta_list
259
-
260
- def get_client_ip(
261
- self,
262
- meta: Dict[str, str],
263
- strict: bool = False,
264
- ) -> Tuple[OptionalIpAddressType, bool]:
265
- """
266
- Returns the client's IP address.
267
- """
268
-
269
- loopback_list: List[IpAddressType] = []
270
- private_list: List[OptionalIpAddressType] = []
271
-
272
- for ip_str in self.get_meta_values(meta):
273
-
274
- ip_list = self.get_ips_from_string(ip_str)
275
- if not ip_list:
276
- continue
277
-
278
- proxy_count_validated = self.is_proxy_count_valid(ip_list, strict)
279
- if not proxy_count_validated:
280
- continue
281
-
282
- proxy_list_validated = self.is_proxy_trusted_list_valid(ip_list, strict)
283
- if not proxy_list_validated:
284
- continue
285
-
286
- client_ip, trusted_route = self.get_best_ip(
287
- ip_list, proxy_count_validated, proxy_list_validated
288
- )
289
-
290
- # we found a global ip, return it
291
- if client_ip is not None and client_ip.is_global:
292
- return client_ip, trusted_route
293
-
294
- # we found a private ip, save it
295
- if client_ip is not None and client_ip.is_loopback:
296
- loopback_list.append(client_ip)
297
- else:
298
- # if not global (public) or loopback (local), we treat it asd private
299
- private_list.append(client_ip)
300
-
301
- # we have not been able to locate a global ip
302
- # it could be the server is running on the intranet
303
- # we will return the first private ip we found
304
- if private_list:
305
- return private_list[0], False
306
-
307
- # we have not been able to locate a global ip, nor a private ip
308
- # it could be the server is running on a loopback address serving local requests
309
- if loopback_list:
310
- return loopback_list[0], False
311
-
312
- # we were unable to find any ip address
313
- return None, False
314
-
315
- def get_best_ip(
316
- self,
317
- ip_list: List[IpAddressType],
318
- proxy_count_validated: bool = True,
319
- proxy_list_validated: bool = True,
320
- ) -> Tuple[OptionalIpAddressType, bool]:
321
- """
322
- Returns the best possible ip for the client.
323
- """
324
-
325
- if not ip_list:
326
- logger.warning("Invalid ip list provided.")
327
- return None, False
328
-
329
- # the incoming ips match our trusted proxy list
330
- if len(self.proxy_list) > 0 and proxy_list_validated:
331
- best_client_ip_index = len(self.proxy_list) + 1
332
- best_client_ip = ip_list[-best_client_ip_index]
333
- return best_client_ip, True
334
-
335
- # the incoming ips match our proxy count
336
- if (
337
- self.proxy_count is not None
338
- and self.proxy_count > 0
339
- and proxy_count_validated
340
- ):
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
File without changes