fastapi-ipware 0.1.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.
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.3
2
+ Name: fastapi-ipware
3
+ Version: 0.1.0
4
+ Summary: FastAPI/Starlette-native wrapper for python-ipware to get client IP addresses
5
+ Keywords: fastapi,starlette,ipware,ip-address,client-ip,proxy,x-forwarded-for
6
+ Author: Michael Bianco
7
+ Author-email: Michael Bianco <mike@mikebian.co>
8
+ Requires-Dist: python-ipware>=3.0.0
9
+ Requires-Dist: starlette>=0.49.1
10
+ Requires-Python: >=3.10
11
+ Project-URL: Repository, https://github.com/iloveitaly/fastapi-ipware
12
+ Description-Content-Type: text/markdown
13
+
14
+ # fastapi-ipware
15
+
16
+ A FastAPI/Starlette-native wrapper for [python-ipware](https://github.com/un33k/python-ipware) that eliminates the need for WSGI-style header conversion.
17
+
18
+ ## Features
19
+
20
+ - **Zero conversion overhead** - Headers converted once at initialization, not on every request
21
+ - **FastAPI-native API** - Works directly with FastAPI/Starlette `Request` objects
22
+ - **Customizable precedence** - Easy to configure header priority for your infrastructure
23
+ - **Proxy validation** - Supports trusted proxy lists and proxy count validation
24
+ - **Thin wrapper** - Leverages all the battle-tested logic from python-ipware
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install fastapi-ipware
30
+ ```
31
+
32
+ Or with uv:
33
+
34
+ ```bash
35
+ uv add fastapi-ipware
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ from fastapi import FastAPI, Request
42
+ from fastapi_ipware import FastAPIIpWare
43
+
44
+ app = FastAPI()
45
+ ipware = FastAPIIpWare()
46
+
47
+ @app.get("/")
48
+ async def get_ip(request: Request):
49
+ ip, trusted = ipware.get_client_ip_from_request(request)
50
+
51
+ if ip:
52
+ return {
53
+ "ip": str(ip),
54
+ "trusted": trusted,
55
+ "is_public": ip.is_global,
56
+ "is_private": ip.is_private,
57
+ }
58
+
59
+ return {"error": "Could not determine IP"}
60
+ ```
61
+
62
+ ## Usage
63
+
64
+ ### Basic Usage
65
+
66
+ ```python
67
+ from fastapi_ipware import FastAPIIpWare
68
+
69
+ # Use default configuration (optimized for FastAPI/cloud deployments)
70
+ ipware = FastAPIIpWare()
71
+
72
+ ip, trusted = ipware.get_client_ip_from_request(request)
73
+ ```
74
+
75
+ ### Custom Header Precedence
76
+
77
+ Customize which headers are checked and in what order:
78
+
79
+ ```python
80
+ # Prioritize Cloudflare headers
81
+ ipware = FastAPIIpWare(
82
+ precedence=(
83
+ "CF-Connecting-IP",
84
+ "X-Forwarded-For",
85
+ "X-Real-IP",
86
+ )
87
+ )
88
+
89
+ # NGINX configuration
90
+ ipware = FastAPIIpWare(
91
+ precedence=(
92
+ "X-Real-IP",
93
+ "X-Forwarded-For",
94
+ )
95
+ )
96
+ ```
97
+
98
+ ### Proxy Count Validation
99
+
100
+ Validate that requests pass through the expected number of proxies:
101
+
102
+ ```python
103
+ # Expect exactly 1 proxy (e.g., AWS ALB)
104
+ ipware = FastAPIIpWare(proxy_count=1)
105
+
106
+ # In strict mode, must be exactly 1 proxy
107
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=True)
108
+
109
+ # In non-strict mode, allow 1 or more proxies
110
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=False)
111
+ ```
112
+
113
+ ### Trusted Proxy List
114
+
115
+ Validate that requests pass through specific trusted proxies:
116
+
117
+ ```python
118
+ # Trust specific proxy IP prefixes
119
+ ipware = FastAPIIpWare(
120
+ proxy_list=["10.0.", "10.1."] # AWS internal IPs
121
+ )
122
+
123
+ ip, trusted = ipware.get_client_ip_from_request(request)
124
+
125
+ # trusted=True only if request came through specified proxies
126
+ ```
127
+
128
+ ### Combined Validation
129
+
130
+ Use both proxy count and trusted proxy list:
131
+
132
+ ```python
133
+ # Expect 1 proxy from a specific IP range
134
+ ipware = FastAPIIpWare(
135
+ proxy_count=1,
136
+ proxy_list=["10.0."]
137
+ )
138
+ ```
139
+
140
+ ## Real-World Examples
141
+
142
+ ### AWS Application Load Balancer
143
+
144
+ ```python
145
+ ipware = FastAPIIpWare(
146
+ proxy_count=1,
147
+ proxy_list=["10.0."] # Your VPC CIDR
148
+ )
149
+ ```
150
+
151
+ ### Cloudflare
152
+
153
+ ```python
154
+ ipware = FastAPIIpWare(
155
+ precedence=("CF-Connecting-IP",)
156
+ )
157
+ ```
158
+
159
+ ### Multiple Proxies (CDN + Load Balancer)
160
+
161
+ ```python
162
+ ipware = FastAPIIpWare(
163
+ proxy_count=2,
164
+ proxy_list=["10.1.", "10.2."] # CDN and LB IPs
165
+ )
166
+ ```
167
+
168
+ ### NGINX Reverse Proxy
169
+
170
+ ```python
171
+ ipware = FastAPIIpWare(
172
+ precedence=("X-Real-IP", "X-Forwarded-For"),
173
+ proxy_count=1
174
+ )
175
+ ```
176
+
177
+ ## IP Address Types
178
+
179
+ The returned IP address object has useful properties:
180
+
181
+ ```python
182
+ ip, _ = ipware.get_client_ip_from_request(request)
183
+
184
+ if ip:
185
+ print(f"Is public: {ip.is_global}")
186
+ print(f"Is private: {ip.is_private}")
187
+ print(f"Is loopback: {ip.is_loopback}")
188
+ print(f"Is multicast: {ip.is_multicast}")
189
+ ```
190
+
191
+ python-ipware automatically prefers:
192
+ 1. Public (global) IPs first
193
+ 2. Private IPs second
194
+ 3. Loopback IPs last
195
+
196
+ ## Default Header Precedence
197
+
198
+ The default precedence order is optimized for modern cloud deployments. See the [default precedence configuration](https://github.com/iloveitaly/fastapi-ipware/blob/main/fastapi_ipware/__init__.py#L48-L58) in the source code.
199
+
200
+ ## Why fastapi-ipware?
201
+
202
+ `python-ipware` expects WSGI-style headers (`HTTP_X_FORWARDED_FOR`), but FastAPI uses natural header names (`X-Forwarded-For`). This wrapper handles the conversion automatically so you don't have to.
203
+
204
+ ## Contributing
205
+
206
+ Contributions welcome! This is a thin wrapper around python-ipware, so most IP detection logic lives there.
207
+
208
+ ## License
209
+
210
+ [MIT License](LICENSE.md)
211
+
212
+ ## Credits
213
+
214
+ Built on top of [python-ipware](https://github.com/un33k/python-ipware) by un33k.
@@ -0,0 +1,201 @@
1
+ # fastapi-ipware
2
+
3
+ A FastAPI/Starlette-native wrapper for [python-ipware](https://github.com/un33k/python-ipware) that eliminates the need for WSGI-style header conversion.
4
+
5
+ ## Features
6
+
7
+ - **Zero conversion overhead** - Headers converted once at initialization, not on every request
8
+ - **FastAPI-native API** - Works directly with FastAPI/Starlette `Request` objects
9
+ - **Customizable precedence** - Easy to configure header priority for your infrastructure
10
+ - **Proxy validation** - Supports trusted proxy lists and proxy count validation
11
+ - **Thin wrapper** - Leverages all the battle-tested logic from python-ipware
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install fastapi-ipware
17
+ ```
18
+
19
+ Or with uv:
20
+
21
+ ```bash
22
+ uv add fastapi-ipware
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ from fastapi import FastAPI, Request
29
+ from fastapi_ipware import FastAPIIpWare
30
+
31
+ app = FastAPI()
32
+ ipware = FastAPIIpWare()
33
+
34
+ @app.get("/")
35
+ async def get_ip(request: Request):
36
+ ip, trusted = ipware.get_client_ip_from_request(request)
37
+
38
+ if ip:
39
+ return {
40
+ "ip": str(ip),
41
+ "trusted": trusted,
42
+ "is_public": ip.is_global,
43
+ "is_private": ip.is_private,
44
+ }
45
+
46
+ return {"error": "Could not determine IP"}
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Basic Usage
52
+
53
+ ```python
54
+ from fastapi_ipware import FastAPIIpWare
55
+
56
+ # Use default configuration (optimized for FastAPI/cloud deployments)
57
+ ipware = FastAPIIpWare()
58
+
59
+ ip, trusted = ipware.get_client_ip_from_request(request)
60
+ ```
61
+
62
+ ### Custom Header Precedence
63
+
64
+ Customize which headers are checked and in what order:
65
+
66
+ ```python
67
+ # Prioritize Cloudflare headers
68
+ ipware = FastAPIIpWare(
69
+ precedence=(
70
+ "CF-Connecting-IP",
71
+ "X-Forwarded-For",
72
+ "X-Real-IP",
73
+ )
74
+ )
75
+
76
+ # NGINX configuration
77
+ ipware = FastAPIIpWare(
78
+ precedence=(
79
+ "X-Real-IP",
80
+ "X-Forwarded-For",
81
+ )
82
+ )
83
+ ```
84
+
85
+ ### Proxy Count Validation
86
+
87
+ Validate that requests pass through the expected number of proxies:
88
+
89
+ ```python
90
+ # Expect exactly 1 proxy (e.g., AWS ALB)
91
+ ipware = FastAPIIpWare(proxy_count=1)
92
+
93
+ # In strict mode, must be exactly 1 proxy
94
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=True)
95
+
96
+ # In non-strict mode, allow 1 or more proxies
97
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=False)
98
+ ```
99
+
100
+ ### Trusted Proxy List
101
+
102
+ Validate that requests pass through specific trusted proxies:
103
+
104
+ ```python
105
+ # Trust specific proxy IP prefixes
106
+ ipware = FastAPIIpWare(
107
+ proxy_list=["10.0.", "10.1."] # AWS internal IPs
108
+ )
109
+
110
+ ip, trusted = ipware.get_client_ip_from_request(request)
111
+
112
+ # trusted=True only if request came through specified proxies
113
+ ```
114
+
115
+ ### Combined Validation
116
+
117
+ Use both proxy count and trusted proxy list:
118
+
119
+ ```python
120
+ # Expect 1 proxy from a specific IP range
121
+ ipware = FastAPIIpWare(
122
+ proxy_count=1,
123
+ proxy_list=["10.0."]
124
+ )
125
+ ```
126
+
127
+ ## Real-World Examples
128
+
129
+ ### AWS Application Load Balancer
130
+
131
+ ```python
132
+ ipware = FastAPIIpWare(
133
+ proxy_count=1,
134
+ proxy_list=["10.0."] # Your VPC CIDR
135
+ )
136
+ ```
137
+
138
+ ### Cloudflare
139
+
140
+ ```python
141
+ ipware = FastAPIIpWare(
142
+ precedence=("CF-Connecting-IP",)
143
+ )
144
+ ```
145
+
146
+ ### Multiple Proxies (CDN + Load Balancer)
147
+
148
+ ```python
149
+ ipware = FastAPIIpWare(
150
+ proxy_count=2,
151
+ proxy_list=["10.1.", "10.2."] # CDN and LB IPs
152
+ )
153
+ ```
154
+
155
+ ### NGINX Reverse Proxy
156
+
157
+ ```python
158
+ ipware = FastAPIIpWare(
159
+ precedence=("X-Real-IP", "X-Forwarded-For"),
160
+ proxy_count=1
161
+ )
162
+ ```
163
+
164
+ ## IP Address Types
165
+
166
+ The returned IP address object has useful properties:
167
+
168
+ ```python
169
+ ip, _ = ipware.get_client_ip_from_request(request)
170
+
171
+ if ip:
172
+ print(f"Is public: {ip.is_global}")
173
+ print(f"Is private: {ip.is_private}")
174
+ print(f"Is loopback: {ip.is_loopback}")
175
+ print(f"Is multicast: {ip.is_multicast}")
176
+ ```
177
+
178
+ python-ipware automatically prefers:
179
+ 1. Public (global) IPs first
180
+ 2. Private IPs second
181
+ 3. Loopback IPs last
182
+
183
+ ## Default Header Precedence
184
+
185
+ The default precedence order is optimized for modern cloud deployments. See the [default precedence configuration](https://github.com/iloveitaly/fastapi-ipware/blob/main/fastapi_ipware/__init__.py#L48-L58) in the source code.
186
+
187
+ ## Why fastapi-ipware?
188
+
189
+ `python-ipware` expects WSGI-style headers (`HTTP_X_FORWARDED_FOR`), but FastAPI uses natural header names (`X-Forwarded-For`). This wrapper handles the conversion automatically so you don't have to.
190
+
191
+ ## Contributing
192
+
193
+ Contributions welcome! This is a thin wrapper around python-ipware, so most IP detection logic lives there.
194
+
195
+ ## License
196
+
197
+ [MIT License](LICENSE.md)
198
+
199
+ ## Credits
200
+
201
+ Built on top of [python-ipware](https://github.com/un33k/python-ipware) by un33k.
@@ -0,0 +1,110 @@
1
+ import ipaddress
2
+
3
+ from python_ipware.python_ipware import IpWare # type: ignore[import-not-found]
4
+ from starlette.requests import Request
5
+
6
+
7
+ class FastAPIIpWare(IpWare):
8
+ """
9
+ A FastAPI/Starlette-native wrapper around python-ipware that eliminates
10
+ the need for WSGI-style header conversion at request time.
11
+
12
+ This class accepts natural header names (e.g., "X-Forwarded-For") and
13
+ handles the conversion to ipware's expected format internally.
14
+
15
+ Example:
16
+ >>> from fastapi_ipware import FastAPIIpWare
17
+ >>> ipware = FastAPIIpWare()
18
+ >>> ip, trusted = ipware.get_client_ip_from_request(request)
19
+
20
+ >>> # With custom precedence
21
+ >>> ipware = FastAPIIpWare(
22
+ ... precedence=("CF-Connecting-IP", "X-Forwarded-For"),
23
+ ... proxy_count=1
24
+ ... )
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ precedence: tuple[str, ...] | None = None,
30
+ leftmost: bool = True,
31
+ proxy_count: int | None = None,
32
+ proxy_list: list[str] | None = None,
33
+ ):
34
+ """
35
+ Initialize FastAPIIpWare with optional configuration.
36
+
37
+ Args:
38
+ precedence: Tuple of header names to check in order. Uses natural header
39
+ names with dashes (e.g., "X-Forwarded-For", "X-Real-IP").
40
+ If None, uses FastAPI-optimized defaults.
41
+ leftmost: If True, use leftmost IP in comma-separated list (standard).
42
+ If False, use rightmost IP (rare legacy configurations).
43
+ proxy_count: Expected number of proxies between client and server.
44
+ Used to validate and extract the correct client IP.
45
+ proxy_list: List of trusted proxy IP prefixes (e.g., ["10.1.", "10.2.3"]).
46
+ """
47
+ # FastAPI-native precedence using actual header names with dashes
48
+ if precedence is None:
49
+ precedence = (
50
+ "X-Forwarded-For", # Most common, used by AWS ELB, nginx, etc.
51
+ "X-Real-IP", # NGINX
52
+ "CF-Connecting-IP", # Cloudflare
53
+ "True-Client-IP", # Cloudflare Enterprise
54
+ "Fastly-Client-IP", # Fastly, Firebase
55
+ "X-Client-IP", # Microsoft Azure
56
+ "X-Cluster-Client-IP", # Rackspace Cloud Load Balancers
57
+ "Forwarded-For", # RFC 7239
58
+ "Forwarded", # RFC 7239
59
+ "Client-IP", # Akamai, Cloudflare
60
+ )
61
+
62
+ # Store FastAPI-style precedence for reference
63
+ self._fastapi_precedence = precedence
64
+
65
+ # Convert user-friendly header names (with dashes) to WSGI format once
66
+ # This happens only at initialization, not on every request
67
+ wsgi_precedence = tuple(
68
+ f"HTTP_{header.upper().replace('-', '_')}" for header in precedence
69
+ )
70
+
71
+ # Initialize parent class with WSGI-style headers
72
+ super().__init__(wsgi_precedence, leftmost, proxy_count, proxy_list)
73
+
74
+ def get_client_ip_from_request(
75
+ self, request: Request, strict: bool = False
76
+ ) -> tuple[ipaddress.IPv4Address | ipaddress.IPv6Address | None, bool]:
77
+ """
78
+ Get client IP address from a FastAPI/Starlette Request object.
79
+
80
+ This is the main method you should use with FastAPI/Starlette applications.
81
+ It handles the header conversion automatically.
82
+
83
+ Args:
84
+ request: FastAPI/Starlette Request object
85
+ strict: If True, enforce exact proxy count/list match.
86
+ If False, allow more proxies than specified.
87
+
88
+ Returns:
89
+ Tuple of (ip_address, trusted_route) where:
90
+ - ip_address: IPv4Address or IPv6Address object (or None if not found)
91
+ - trusted_route: True if request came through trusted proxies, False otherwise
92
+
93
+ Example:
94
+ >>> ip, trusted = ipware.get_client_ip_from_request(request)
95
+ >>> if ip:
96
+ ... print(f"Client IP: {ip}")
97
+ ... print(f"Is global: {ip.is_global}")
98
+ ... print(f"Is private: {ip.is_private}")
99
+ """
100
+ # Convert Starlette headers to WSGI-style dict that parent class expects
101
+ # This happens once per request
102
+ meta = {
103
+ f"HTTP_{name.upper().replace('-', '_')}": value
104
+ for name, value in request.headers.items()
105
+ }
106
+
107
+ return self.get_client_ip(meta, strict=strict)
108
+
109
+
110
+ __all__ = ["FastAPIIpWare"]
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "fastapi-ipware"
3
+ version = "0.1.0"
4
+ description = "FastAPI/Starlette-native wrapper for python-ipware to get client IP addresses"
5
+ keywords = ["fastapi", "starlette", "ipware", "ip-address", "client-ip", "proxy", "x-forwarded-for"]
6
+ readme = "README.md"
7
+ requires-python = ">=3.10"
8
+ dependencies = [
9
+ "python-ipware>=3.0.0",
10
+ "starlette>=0.49.1",
11
+ ]
12
+ authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
13
+ urls = { "Repository" = "https://github.com/iloveitaly/fastapi-ipware" }
14
+
15
+ # additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.8.11,<0.9.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.uv.build-backend]
22
+ # avoids the src/ directory structure
23
+ module-root = ""
24
+
25
+ [dependency-groups]
26
+ dev = ["pytest>=8.3.3","pyright>=1.1.380","ruff>=0.8.0"]
27
+
28
+ [tool.pyright]
29
+ exclude = ["example.py", ".venv", "**/__pycache__"]
30
+