Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run ruff
run: ruff .
run: ruff check .
- name: Run test
run: coverage run --source=python_ipware -m unittest discover
- name: Coveralls
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.0.4

Enhance:
- Added `proxy_count=0` as an option (@FraKraBa)

## 2.0.3

Enhance:
Expand Down
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,23 +185,41 @@ If your python server is behind a `known` number of proxies, but you deploy on m
You can customize the proxy count by providing your `proxy_count` during initialization when calling `IpWare(proxy_count=2)`.

```python
# In the above scenario, the total number of proxies can be used as a way to filter out unwanted requests.
from python_ipware import IpWare

# enforce proxy count
ipw = IpWare(proxy_count=1)
# Enforce proxy count
# proxy_count=0 is valid
# proxy_count=None to disable proxy_count check
ipw = IpWare(proxy_count=2)

# enforce proxy count and trusted proxies
ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
# Example usage in non-strict mode:
# X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
# At least `proxy_count` number of proxies
ip, trusted_route = ipw.get_client_ip(meta=request.META)

# Example usage in strict mode:
# X-Forwarded-For format: <client>, <proxy1>, <proxy2>
# Exact `proxy_count` number of proxies
ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
```

# usage: non-strict mode (X-Forwarded-For: <fake>, <client>, <proxy1>, <proxy2>)
# total number of ip addresses are greater than the total count
ip, trusted_route = ipw.get_client_ip(meta=request.META)
### Proxy Count & Trusted Proxy List Combo
In this example, we utilize the total number of proxies as a method to filter out unwanted requests while verifying the trust proxies.

```python
from python_ipware import IpWare

# usage: strict mode (X-Forwarded-For: <client>, <proxy1>, <proxy2>)
# total number of ip addresses are exactly equal to client ip + proxy_count
# Enforce both proxy count and trusted proxies
ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])

# Example usage in non-strict mode:
# X-Forwarded-For format: <fake>, <client>, <proxy1>, <proxy2>
# At least `proxy_count` number of proxies
ip, trusted_route = ipw.get_client_ip(meta=request.META)

# Example usage in strict mode:
# X-Forwarded-For format: <client>, <proxy1>
# Exact `proxy_count` number of proxies
ip, trusted_route = ipw.get_client_ip(meta=request.META, strict=True)
```

Expand Down
2 changes: 1 addition & 1 deletion format.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash

ruff .
ruff check .
2 changes: 1 addition & 1 deletion python_ipware/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.3"
__version__ = "2.0.4"
36 changes: 18 additions & 18 deletions python_ipware/python_ipware.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,9 @@ class IpWareProxy:

def __init__(
self,
proxy_count: int = 0,
proxy_count: Optional[int] = None,
proxy_list: Optional[List[str]] = None,
) -> None:
if proxy_count is None or proxy_count < 0:
raise ValueError("proxy_count must be a positive integer")

self.proxy_count = proxy_count
self.proxy_list = self._is_valid_proxy_trusted_list(proxy_list or [])

Expand All @@ -172,15 +169,14 @@ def is_proxy_count_valid(
"""
Checks if the proxy count is valid
@param ip_list: list of ip addresses
@param strict: if True, we must have exactly proxy_count proxies
@param strict: if True, we must have exactly proxy_count proxies, including `0` proxies
@return: True if the proxy count is valid, False otherwise
"""
if self.proxy_count < 1:
# No proxy count check is required
if self.proxy_count is None:
return True

ip_count: int = len(ip_list)
if ip_count < 1:
return False

if strict:
# our first proxy takes the last ip address and treats it as client ip
Expand Down Expand Up @@ -221,10 +217,6 @@ def is_proxy_trusted_list_valid(
if not str(value).startswith(self.proxy_list[index]):
return False

# now all we need is to return the first ip in the list that is not in the trusted proxy list
# best_client_ip_index = proxy_list_count + 1
# best_client_ip = ip_list[-best_client_ip_index]

return True


Expand All @@ -237,11 +229,11 @@ def __init__(
self,
precedence: Optional[Tuple[str, ...]] = None,
leftmost: bool = True,
proxy_count: int = 0,
proxy_count: Optional[int] = None,
proxy_list: Optional[List[str]] = None,
) -> None:
IpWareMeta.__init__(self, precedence, leftmost)
IpWareProxy.__init__(self, proxy_count or 0, proxy_list or [])
IpWareProxy.__init__(self, proxy_count, proxy_list)

def get_meta_value(self, meta: Dict[str, str], key: str) -> str:
"""
Expand All @@ -257,7 +249,13 @@ def get_meta_values(self, meta: Dict[str, str]) -> List[str]:
Given a list of keys, it returns a list of cleaned up values
@return: a list of values
"""
return [self.get_meta_value(meta, key) for key in self.precedence]
meta_list: List[str] = []
for key in self.precedence:
value = self.get_meta_value(meta, key).strip()
if value:
meta_list.append(value)

return meta_list

def get_client_ip(
self,
Expand All @@ -272,8 +270,6 @@ def get_client_ip(
private_list: List[OptionalIpAddressType] = []

for ip_str in self.get_meta_values(meta):
if not ip_str:
continue

ip_list = self.get_ips_from_string(ip_str)
if not ip_list:
Expand Down Expand Up @@ -337,7 +333,11 @@ def get_best_ip(
return best_client_ip, True

# the incoming ips match our proxy count
if self.proxy_count > 0 and proxy_count_validated:
if (
self.proxy_count is not None
and self.proxy_count > 0
and proxy_count_validated
):
best_client_ip_index = self.proxy_count + 1
best_client_ip = ip_list[-best_client_ip_index]
return best_client_ip, True
Expand Down
128 changes: 100 additions & 28 deletions tests/tests_ipv4.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,39 +175,69 @@ def test_proxy_order_right_most(self):
class TestIPv4ProxyCount(unittest.TestCase):
"""IPv4 Proxy Count Test"""

def setUp(self):
self.ipware = IpWare(proxy_count=1)

def tearDown(self):
self.ipware = None

def test_singleton_proxy_count(self):
def test_proxy_count_one_missing_proxy_fail(self):
ipware = IpWare(proxy_count=1)
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139",
}
r = self.ipware.get_client_ip(meta)
r = ipware.get_client_ip(meta)

self.assertEqual(r, (None, False))

def test_singleton_proxy_count_private(self):
def test_proxy_count_one_at_least_one_proxy_pass(self):
ipware = IpWare(proxy_count=1)
meta = {
"HTTP_X_FORWARDED_FOR": "10.0.0.0",
"HTTP_X_REAL_IP": "177.139.233.139",
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = self.ipware.get_client_ip(meta)
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("198.84.193.157"), True))

def test_proxy_count_one_exactly_one_proxy_fail(self):
ipware = IpWare(proxy_count=1)
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta, strict=True)
self.assertEqual(r, (None, False))

def test_proxy_count_relax(self):
def test_proxy_count_one_exactly_one_proxy_pass(self):
ipware = IpWare(proxy_count=1)
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157",
}
r = ipware.get_client_ip(meta, strict=True)
self.assertEqual(r, (IPv4Address("177.139.233.139"), True))

def test_proxy_count_one_dont_care_proxy_pass(self):
ipware = IpWare()
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = self.ipware.get_client_ip(meta, strict=False)
self.assertEqual(r, (IPv4Address("198.84.193.157"), True))
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("177.139.233.139"), False))

def test_proxy_count_strict(self):
def test_proxy_count_zero_dont_care_proxy_pass(self):
ipware = IpWare(proxy_count=0)
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.158",
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = self.ipware.get_client_ip(meta, strict=True)
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("177.139.233.139"), False))

def test_proxy_count_zero_exact_zero_proxy_pass(self):
ipware = IpWare(proxy_count=0)
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139",
}
r = ipware.get_client_ip(meta, strict=True)
self.assertEqual(r, (IPv4Address("177.139.233.139"), False))

def test_proxy_count_zero_exact_zero_proxy_fail(self):
ipware = IpWare(proxy_count=0)
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta, strict=True)
self.assertEqual(r, (None, False))


Expand Down Expand Up @@ -245,26 +275,68 @@ def test_proxy_list_success(self):
class TestIPv4ProxyCountProxyList(unittest.TestCase):
"""IPv4 Proxy Count Test"""

def setUp(self):
self.ipware = IpWare(
proxy_count=2, proxy_list=["198.84.193.157", "198.84.193.158"]
)
def test_proxy_list_relax(self):
ipware = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("177.139.233.139"), True))

def tearDown(self):
self.ipware = None
def test_proxy_list_strict_pass(self):
ipware = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta, strict=True)
self.assertEqual(r, (IPv4Address("177.139.233.139"), True))

def test_proxy_list_relax(self):
def test_proxy_list_strict_fail(self):
ipware = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = self.ipware.get_client_ip(meta)
r = ipware.get_client_ip(meta, strict=True)
self.assertEqual(r, (None, False))

def test_proxy_list_relax_exact_pass(self):
ipware = IpWare(proxy_count=2, proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("177.139.233.139"), True))

def test_proxy_list_strict(self):
def test_proxy_list_relax_count_under_pass(self):
ipware = IpWare(proxy_count=1, proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = self.ipware.get_client_ip(meta, strict=True)
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("177.139.233.139"), True))

def test_proxy_list_relax_count_over_pass(self):
ipware = IpWare(proxy_count=5, proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta)
self.assertEqual(r, (None, False))

def test_proxy_list_count_exact_pass(self):
ipware = IpWare(proxy_count=2, proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta)
self.assertEqual(r, (IPv4Address("177.139.233.139"), True))

def test_proxy_list_count_exact_fail(self):
ipware = IpWare(proxy_count=4, proxy_list=["198.84.193.157", "198.84.193.158"])
meta = {
"HTTP_X_FORWARDED_FOR": "177.139.233.138, 177.139.233.139, 198.84.193.157, 198.84.193.158",
}
r = ipware.get_client_ip(meta)
self.assertEqual(r, (None, False))


Expand Down