fastapi-ipware 0.1.1__tar.gz → 0.2.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,230 @@
1
+ Metadata-Version: 2.3
2
+ Name: fastapi-ipware
3
+ Version: 0.2.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
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: python-ipware>=4.1.0
14
+ Requires-Dist: starlette>=0.49.1
15
+ Requires-Python: >=3.12
16
+ Project-URL: Repository, https://github.com/iloveitaly/fastapi-ipware
17
+ Description-Content-Type: text/markdown
18
+
19
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/fastapi-ipware)](https://github.com/iloveitaly/fastapi-ipware/releases)
20
+ [![Downloads](https://static.pepy.tech/badge/fastapi-ipware/month)](https://pepy.tech/project/fastapi-ipware)
21
+ ![GitHub CI Status](https://github.com/iloveitaly/fastapi-ipware/actions/workflows/build_and_publish.yml/badge.svg)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
23
+
24
+ # fastapi-ipware
25
+
26
+ A FastAPI/Starlette-native wrapper for [python-ipware](https://github.com/un33k/python-ipware) that eliminates the need for WSGI-style header conversion.
27
+
28
+ `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.
29
+
30
+ Also, 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. This is different from the
31
+ default ordering ipware which deprioritizes platform-specific headers, which is often the wrong ordering if you are using something like CloudFlare.
32
+
33
+ ## Features
34
+
35
+ - **Zero conversion overhead** - Headers converted once at initialization, not on every request
36
+ - **FastAPI-native API** - Works directly with FastAPI/Starlette `Request` objects
37
+ - **Customizable precedence** - Easy to configure header priority for your infrastructure
38
+ - **Proxy validation** - Supports trusted proxy lists and proxy count validation
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ uv add fastapi-ipware
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ `trusted` is true only when the request came through the proxies configured with `proxy_count` or `proxy_list`.
49
+
50
+ `proxy_count=N` returns the address just left of the N rightmost proxies, instead of the first public address, and rejects a shorter chain. `X-Forwarded-For: 203.0.113.10, 10.0.0.1, 10.0.0.2` returns `203.0.113.10` with no `proxy_count`, and `10.0.0.1` with `proxy_count=1`.
51
+
52
+ ### Using FastAPI Dependency Injection
53
+
54
+ ```python
55
+ from typing import Annotated
56
+ from fastapi import Depends, FastAPI
57
+ from fastapi_ipware import ClientIpResult, FastAPIIpWare
58
+
59
+ app = FastAPI()
60
+ ipware = FastAPIIpWare()
61
+
62
+
63
+ @app.get("/")
64
+ async def get_ip(client: Annotated[ClientIpResult, Depends(ipware)]):
65
+ ip, trusted = client
66
+ return {
67
+ "ip": str(ip) if ip else None,
68
+ "trusted": trusted,
69
+ "is_public": ip.is_global if ip else None,
70
+ }
71
+
72
+
73
+ # Or inject only the IP object or string directly:
74
+ @app.get("/ip-string")
75
+ async def get_ip_string(ip_str: Annotated[str | None, Depends(ipware.get_ip_str)]):
76
+ return {"ip": ip_str}
77
+ ```
78
+
79
+ ### Using Request Directly
80
+
81
+ ```python
82
+ from fastapi import FastAPI, Request
83
+ from fastapi_ipware import FastAPIIpWare
84
+
85
+ app = FastAPI()
86
+ ipware = FastAPIIpWare()
87
+
88
+
89
+ @app.get("/")
90
+ async def get_ip(request: Request):
91
+ ip, trusted = ipware.get_client_ip_from_request(request)
92
+ return {"ip": str(ip) if ip else None, "trusted": trusted}
93
+ ```
94
+
95
+ ### Using ASGI Middleware
96
+
97
+ Automatically extract the client IP onto `request.state` for every request:
98
+
99
+ ```python
100
+ from fastapi import FastAPI, Request
101
+ from fastapi_ipware import IpWareMiddleware
102
+
103
+ app = FastAPI()
104
+ app.add_middleware(IpWareMiddleware)
105
+
106
+
107
+ @app.get("/")
108
+ async def get_ip(request: Request):
109
+ return {
110
+ "ip": request.state.client_ip_str,
111
+ "trusted": request.state.ip_trusted,
112
+ }
113
+ ```
114
+
115
+ Pass a configured resolver when you need one. Omit `strict` to use that resolver's `default_strict`; pass `strict=` to override it.
116
+
117
+ ```python
118
+ from fastapi_ipware import FastAPIIpWare, IpWareMiddleware
119
+
120
+ ipware = FastAPIIpWare(proxy_count=1, default_strict=True)
121
+ app.add_middleware(IpWareMiddleware, ipware=ipware)
122
+ ```
123
+
124
+ ### Custom Header Precedence
125
+
126
+ Customize which headers are checked and in what order:
127
+
128
+ ```python
129
+ # Prioritize Cloudflare headers
130
+ ipware = FastAPIIpWare(
131
+ precedence=(
132
+ "CF-Connecting-IP",
133
+ "X-Forwarded-For",
134
+ "X-Real-IP",
135
+ )
136
+ )
137
+
138
+ # NGINX configuration
139
+ ipware = FastAPIIpWare(
140
+ precedence=(
141
+ "X-Real-IP",
142
+ "X-Forwarded-For",
143
+ )
144
+ )
145
+ ```
146
+
147
+ ### Proxy Count Validation
148
+
149
+ Validate that requests pass through the expected number of proxies:
150
+
151
+ ```python
152
+ # Expect exactly 1 proxy (e.g., AWS ALB).
153
+ # default_strict applies to Depends(ipware), dependency(), and IpWareMiddleware.
154
+ ipware = FastAPIIpWare(proxy_count=1, default_strict=True)
155
+
156
+ # In strict mode, must be exactly 1 proxy
157
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=True)
158
+
159
+ # In non-strict mode, allow 1 or more proxies
160
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=False)
161
+ ```
162
+
163
+ ### Trusted Proxy List
164
+
165
+ Validate that requests pass through specific trusted proxies (supports IP prefixes, exact IPs, and CIDR networks):
166
+
167
+ ```python
168
+ # Trust specific proxy IP prefixes or CIDR networks
169
+ ipware = FastAPIIpWare(
170
+ proxy_list=["10.0.", "10.1.", "100.64.0.0/10"] # AWS internal IPs and CGNAT CIDR
171
+ )
172
+
173
+ ip, trusted = ipware.get_client_ip_from_request(request)
174
+
175
+ # trusted=True only if request came through specified proxies
176
+ ```
177
+
178
+ ### Combined Validation
179
+
180
+ Use both proxy count and trusted proxy list:
181
+
182
+ ```python
183
+ # Expect 1 proxy from a specific IP range
184
+ ipware = FastAPIIpWare(proxy_count=1, proxy_list=["10.0."])
185
+ ```
186
+
187
+ ### Algorithm Engine Selection
188
+
189
+ Choose between python-ipware 4.x engines (`auto` / `modern` / `legacy`):
190
+
191
+ ```python
192
+ # Modern engine (default): enhanced header parsing and RFC 7239 support
193
+ ipware = FastAPIIpWare(algorithm="modern")
194
+
195
+ # Legacy engine: frozen byte-for-byte v3 behavior
196
+ ipware = FastAPIIpWare(algorithm="legacy")
197
+ ```
198
+
199
+ ## IP Address Types
200
+
201
+ The returned IP address object has useful properties:
202
+
203
+ ```python
204
+ ip, _ = ipware.get_client_ip_from_request(request)
205
+
206
+ if ip:
207
+ print(f"Is public: {ip.is_global}")
208
+ print(f"Is private: {ip.is_private}")
209
+ print(f"Is loopback: {ip.is_loopback}")
210
+ print(f"Is multicast: {ip.is_multicast}")
211
+ ```
212
+
213
+ python-ipware automatically prefers:
214
+
215
+ 1. Public (global) IPs first
216
+ 2. Private IPs second
217
+ 3. Loopback IPs last
218
+
219
+ ## License
220
+
221
+ [MIT License](LICENSE.md)
222
+
223
+ ## Credits
224
+
225
+ - Built on top of [python-ipware](https://github.com/un33k/python-ipware) by un33k.
226
+ - https://github.com/long2ice/fastapi-limiter/blob/8d179c058fa2aaf98f3450c9026a7300ae2b3bdd/fastapi_limiter/__init__.py#L11
227
+
228
+ ---
229
+
230
+ *This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
@@ -0,0 +1,212 @@
1
+ [![Release Notes](https://img.shields.io/github/release/iloveitaly/fastapi-ipware)](https://github.com/iloveitaly/fastapi-ipware/releases)
2
+ [![Downloads](https://static.pepy.tech/badge/fastapi-ipware/month)](https://pepy.tech/project/fastapi-ipware)
3
+ ![GitHub CI Status](https://github.com/iloveitaly/fastapi-ipware/actions/workflows/build_and_publish.yml/badge.svg)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ # fastapi-ipware
7
+
8
+ A FastAPI/Starlette-native wrapper for [python-ipware](https://github.com/un33k/python-ipware) that eliminates the need for WSGI-style header conversion.
9
+
10
+ `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.
11
+
12
+ Also, 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. This is different from the
13
+ default ordering ipware which deprioritizes platform-specific headers, which is often the wrong ordering if you are using something like CloudFlare.
14
+
15
+ ## Features
16
+
17
+ - **Zero conversion overhead** - Headers converted once at initialization, not on every request
18
+ - **FastAPI-native API** - Works directly with FastAPI/Starlette `Request` objects
19
+ - **Customizable precedence** - Easy to configure header priority for your infrastructure
20
+ - **Proxy validation** - Supports trusted proxy lists and proxy count validation
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ uv add fastapi-ipware
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ `trusted` is true only when the request came through the proxies configured with `proxy_count` or `proxy_list`.
31
+
32
+ `proxy_count=N` returns the address just left of the N rightmost proxies, instead of the first public address, and rejects a shorter chain. `X-Forwarded-For: 203.0.113.10, 10.0.0.1, 10.0.0.2` returns `203.0.113.10` with no `proxy_count`, and `10.0.0.1` with `proxy_count=1`.
33
+
34
+ ### Using FastAPI Dependency Injection
35
+
36
+ ```python
37
+ from typing import Annotated
38
+ from fastapi import Depends, FastAPI
39
+ from fastapi_ipware import ClientIpResult, FastAPIIpWare
40
+
41
+ app = FastAPI()
42
+ ipware = FastAPIIpWare()
43
+
44
+
45
+ @app.get("/")
46
+ async def get_ip(client: Annotated[ClientIpResult, Depends(ipware)]):
47
+ ip, trusted = client
48
+ return {
49
+ "ip": str(ip) if ip else None,
50
+ "trusted": trusted,
51
+ "is_public": ip.is_global if ip else None,
52
+ }
53
+
54
+
55
+ # Or inject only the IP object or string directly:
56
+ @app.get("/ip-string")
57
+ async def get_ip_string(ip_str: Annotated[str | None, Depends(ipware.get_ip_str)]):
58
+ return {"ip": ip_str}
59
+ ```
60
+
61
+ ### Using Request Directly
62
+
63
+ ```python
64
+ from fastapi import FastAPI, Request
65
+ from fastapi_ipware import FastAPIIpWare
66
+
67
+ app = FastAPI()
68
+ ipware = FastAPIIpWare()
69
+
70
+
71
+ @app.get("/")
72
+ async def get_ip(request: Request):
73
+ ip, trusted = ipware.get_client_ip_from_request(request)
74
+ return {"ip": str(ip) if ip else None, "trusted": trusted}
75
+ ```
76
+
77
+ ### Using ASGI Middleware
78
+
79
+ Automatically extract the client IP onto `request.state` for every request:
80
+
81
+ ```python
82
+ from fastapi import FastAPI, Request
83
+ from fastapi_ipware import IpWareMiddleware
84
+
85
+ app = FastAPI()
86
+ app.add_middleware(IpWareMiddleware)
87
+
88
+
89
+ @app.get("/")
90
+ async def get_ip(request: Request):
91
+ return {
92
+ "ip": request.state.client_ip_str,
93
+ "trusted": request.state.ip_trusted,
94
+ }
95
+ ```
96
+
97
+ Pass a configured resolver when you need one. Omit `strict` to use that resolver's `default_strict`; pass `strict=` to override it.
98
+
99
+ ```python
100
+ from fastapi_ipware import FastAPIIpWare, IpWareMiddleware
101
+
102
+ ipware = FastAPIIpWare(proxy_count=1, default_strict=True)
103
+ app.add_middleware(IpWareMiddleware, ipware=ipware)
104
+ ```
105
+
106
+ ### Custom Header Precedence
107
+
108
+ Customize which headers are checked and in what order:
109
+
110
+ ```python
111
+ # Prioritize Cloudflare headers
112
+ ipware = FastAPIIpWare(
113
+ precedence=(
114
+ "CF-Connecting-IP",
115
+ "X-Forwarded-For",
116
+ "X-Real-IP",
117
+ )
118
+ )
119
+
120
+ # NGINX configuration
121
+ ipware = FastAPIIpWare(
122
+ precedence=(
123
+ "X-Real-IP",
124
+ "X-Forwarded-For",
125
+ )
126
+ )
127
+ ```
128
+
129
+ ### Proxy Count Validation
130
+
131
+ Validate that requests pass through the expected number of proxies:
132
+
133
+ ```python
134
+ # Expect exactly 1 proxy (e.g., AWS ALB).
135
+ # default_strict applies to Depends(ipware), dependency(), and IpWareMiddleware.
136
+ ipware = FastAPIIpWare(proxy_count=1, default_strict=True)
137
+
138
+ # In strict mode, must be exactly 1 proxy
139
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=True)
140
+
141
+ # In non-strict mode, allow 1 or more proxies
142
+ ip, trusted = ipware.get_client_ip_from_request(request, strict=False)
143
+ ```
144
+
145
+ ### Trusted Proxy List
146
+
147
+ Validate that requests pass through specific trusted proxies (supports IP prefixes, exact IPs, and CIDR networks):
148
+
149
+ ```python
150
+ # Trust specific proxy IP prefixes or CIDR networks
151
+ ipware = FastAPIIpWare(
152
+ proxy_list=["10.0.", "10.1.", "100.64.0.0/10"] # AWS internal IPs and CGNAT CIDR
153
+ )
154
+
155
+ ip, trusted = ipware.get_client_ip_from_request(request)
156
+
157
+ # trusted=True only if request came through specified proxies
158
+ ```
159
+
160
+ ### Combined Validation
161
+
162
+ Use both proxy count and trusted proxy list:
163
+
164
+ ```python
165
+ # Expect 1 proxy from a specific IP range
166
+ ipware = FastAPIIpWare(proxy_count=1, proxy_list=["10.0."])
167
+ ```
168
+
169
+ ### Algorithm Engine Selection
170
+
171
+ Choose between python-ipware 4.x engines (`auto` / `modern` / `legacy`):
172
+
173
+ ```python
174
+ # Modern engine (default): enhanced header parsing and RFC 7239 support
175
+ ipware = FastAPIIpWare(algorithm="modern")
176
+
177
+ # Legacy engine: frozen byte-for-byte v3 behavior
178
+ ipware = FastAPIIpWare(algorithm="legacy")
179
+ ```
180
+
181
+ ## IP Address Types
182
+
183
+ The returned IP address object has useful properties:
184
+
185
+ ```python
186
+ ip, _ = ipware.get_client_ip_from_request(request)
187
+
188
+ if ip:
189
+ print(f"Is public: {ip.is_global}")
190
+ print(f"Is private: {ip.is_private}")
191
+ print(f"Is loopback: {ip.is_loopback}")
192
+ print(f"Is multicast: {ip.is_multicast}")
193
+ ```
194
+
195
+ python-ipware automatically prefers:
196
+
197
+ 1. Public (global) IPs first
198
+ 2. Private IPs second
199
+ 3. Loopback IPs last
200
+
201
+ ## License
202
+
203
+ [MIT License](LICENSE.md)
204
+
205
+ ## Credits
206
+
207
+ - Built on top of [python-ipware](https://github.com/un33k/python-ipware) by un33k.
208
+ - https://github.com/long2ice/fastapi-limiter/blob/8d179c058fa2aaf98f3450c9026a7300ae2b3bdd/fastapi_limiter/__init__.py#L11
209
+
210
+ ---
211
+
212
+ *This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
@@ -0,0 +1,240 @@
1
+ import ipaddress
2
+ from collections.abc import Callable
3
+ from typing import Literal
4
+
5
+ from python_ipware import IpWare
6
+ from starlette.requests import HTTPConnection
7
+ from starlette.types import ASGIApp, Receive, Scope, Send
8
+
9
+ from .version import __version__
10
+
11
+ Algorithm = Literal["auto", "modern", "legacy"]
12
+ IpAddressType = ipaddress.IPv4Address | ipaddress.IPv6Address
13
+ ClientIpResult = tuple[IpAddressType | None, bool]
14
+
15
+ DEFAULT_PRECEDENCE: tuple[str, ...] = (
16
+ # Provider-specific headers (highest reliability)
17
+ "CF-Connecting-IP", # Cloudflare
18
+ "True-Client-IP", # Cloudflare Enterprise, Akamai
19
+ "Fastly-Client-IP", # Fastly, Firebase
20
+ "Fly-Client-IP", # Fly.io
21
+ "X-Client-IP", # Microsoft Azure
22
+ "X-Azure-ClientIP", # Azure Front Door
23
+ "DO-Connecting-IP", # DigitalOcean App Platform
24
+ "X-Cluster-Client-IP", # Rackspace Cloud Load Balancers
25
+ "X-Appengine-User-IP", # Google App Engine
26
+ "X-Envoy-External-Address", # Envoy / Istio
27
+ # Generic headers (fallback)
28
+ "X-Forwarded-For", # Generic, used by AWS ELB, nginx, etc.
29
+ "X-Real-IP", # NGINX
30
+ "Forwarded-For", # Plain IP list variant
31
+ "Forwarded", # RFC 7239 (for=...;proto=...)
32
+ "Client-IP", # Akamai, Cloudflare fallback
33
+ "REMOTE_ADDR", # Direct connection fallback
34
+ )
35
+
36
+
37
+ class FastAPIIpWare(IpWare):
38
+ """
39
+ A FastAPI/Starlette-native wrapper around python-ipware that eliminates
40
+ the need for WSGI-style header conversion at request time.
41
+
42
+ This class accepts natural header names (e.g., "X-Forwarded-For") and
43
+ handles the conversion to ipware's expected format internally.
44
+
45
+ Example:
46
+ >>> from fastapi_ipware import FastAPIIpWare
47
+ >>> ipware = FastAPIIpWare()
48
+ >>> ip, trusted = ipware.get_client_ip_from_request(request)
49
+
50
+ >>> # Direct FastAPI route dependency
51
+ >>> @app.get("/")
52
+ ... def endpoint(client: ClientIpResult = Depends(ipware)):
53
+ ... ip, trusted = client
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ precedence: tuple[str, ...] | None = None,
59
+ leftmost: bool = True,
60
+ proxy_count: int | None = None,
61
+ proxy_list: list[str] | None = None,
62
+ algorithm: Algorithm = "auto",
63
+ default_strict: bool = False,
64
+ ):
65
+ """
66
+ Initialize FastAPIIpWare with optional configuration.
67
+
68
+ Args:
69
+ precedence: Tuple of header names to check in order. Uses natural header
70
+ names with dashes (e.g., "X-Forwarded-For", "X-Real-IP").
71
+ If None, uses FastAPI-optimized defaults.
72
+ leftmost: If True, use leftmost IP in comma-separated list (standard).
73
+ If False, use rightmost IP (rare legacy configurations).
74
+ proxy_count: Expected number of proxies between client and server.
75
+ Used to validate and extract the correct client IP.
76
+ proxy_list: List of trusted proxy IP prefixes, complete IPs, or CIDR networks
77
+ (e.g., ["10.1.", "198.84.193.157", "100.64.0.0/10"]).
78
+ algorithm: Algorithm engine to use: "auto" (default, uses modern),
79
+ "modern" (v4 enhanced engine), or "legacy" (frozen v3 engine).
80
+ default_strict: Used whenever strict is omitted: Depends(self),
81
+ dependency() with no argument, IpWareMiddleware, and
82
+ get_client_ip_from_request.
83
+ """
84
+ if precedence is None:
85
+ precedence = DEFAULT_PRECEDENCE
86
+
87
+ # Store FastAPI-style precedence for reference
88
+ self._fastapi_precedence = precedence
89
+ self.default_strict = default_strict
90
+
91
+ # Expand precedence to support both natural and WSGI formats
92
+ expanded_precedence: list[str] = []
93
+ for header in precedence:
94
+ if header == "REMOTE_ADDR":
95
+ if "REMOTE_ADDR" not in expanded_precedence:
96
+ expanded_precedence.append("REMOTE_ADDR")
97
+ elif header.startswith("HTTP_"):
98
+ if header not in expanded_precedence:
99
+ expanded_precedence.append(header)
100
+ non_http = header[5:].replace("_", "-")
101
+ if non_http not in expanded_precedence:
102
+ expanded_precedence.append(non_http)
103
+ else:
104
+ if header not in expanded_precedence:
105
+ expanded_precedence.append(header)
106
+ wsgi_form = f"HTTP_{header.upper().replace('-', '_')}"
107
+ if wsgi_form not in expanded_precedence:
108
+ expanded_precedence.append(wsgi_form)
109
+
110
+ super().__init__(
111
+ tuple(expanded_precedence),
112
+ leftmost=leftmost,
113
+ proxy_count=proxy_count,
114
+ proxy_list=proxy_list,
115
+ algorithm=algorithm,
116
+ )
117
+
118
+ @property
119
+ def precedence(self) -> tuple[str, ...]:
120
+ """Header precedence tuple from the active engine."""
121
+ return getattr(self.engine, "precedence", ())
122
+
123
+ @property
124
+ def leftmost(self) -> bool:
125
+ """Leftmost setting from the active engine."""
126
+ return getattr(self.engine, "leftmost", True)
127
+
128
+ @property
129
+ def proxy_count(self) -> int | None:
130
+ """Proxy count setting from the active engine."""
131
+ return getattr(self.engine, "proxy_count", None)
132
+
133
+ @property
134
+ def proxy_list(self) -> list[str]:
135
+ """Proxy list setting from the active engine."""
136
+ return getattr(self.engine, "proxy_list", [])
137
+
138
+ def __call__(self, request: HTTPConnection) -> ClientIpResult:
139
+ """FastAPI route dependency injection entrypoint."""
140
+ return self.get_client_ip_from_request(request, strict=self.default_strict)
141
+
142
+ def dependency(
143
+ self, strict: bool | None = None
144
+ ) -> Callable[[HTTPConnection], ClientIpResult]:
145
+ """Return a FastAPI dependency. An omitted strict flag uses default_strict."""
146
+
147
+ def _dep(request: HTTPConnection) -> ClientIpResult:
148
+ return self.get_client_ip_from_request(request, strict=strict)
149
+
150
+ return _dep
151
+
152
+ def get_ip(self, request: HTTPConnection) -> IpAddressType | None:
153
+ """Return only the client IP address (or None if unresolvable)."""
154
+ ip, _ = self.get_client_ip_from_request(request, strict=self.default_strict)
155
+ return ip
156
+
157
+ def get_ip_str(self, request: HTTPConnection) -> str | None:
158
+ """Return the client IP address as a string (or None if unresolvable)."""
159
+ ip = self.get_ip(request)
160
+ return str(ip) if ip is not None else None
161
+
162
+ def get_client_ip_from_request(
163
+ self, request: HTTPConnection, strict: bool | None = None
164
+ ) -> ClientIpResult:
165
+ """
166
+ Get client IP address from a FastAPI/Starlette Request or WebSocket connection.
167
+
168
+ Args:
169
+ request: FastAPI/Starlette Request or HTTPConnection object
170
+ strict: If True, enforce exact proxy count/list match.
171
+ If False, allow more proxies than specified.
172
+ If None, use self.default_strict.
173
+
174
+ Returns:
175
+ Tuple of (ip_address, trusted_route) where:
176
+ - ip_address: IPv4Address or IPv6Address object (or None if not found)
177
+ - trusted_route: True if request came through trusted proxies, False otherwise
178
+ """
179
+ if strict is None:
180
+ strict = self.default_strict
181
+
182
+ # Populate both WSGI-style headers and original Starlette headers
183
+ meta = {
184
+ f"HTTP_{name.upper().replace('-', '_')}": value
185
+ for name, value in request.headers.items()
186
+ }
187
+ for name, value in request.headers.items():
188
+ meta[name] = value
189
+
190
+ if request.client:
191
+ # Map Starlette's connection info to REMOTE_ADDR fallback
192
+ meta["REMOTE_ADDR"] = request.client.host
193
+
194
+ return self.get_client_ip(meta, strict=strict)
195
+
196
+
197
+ class IpWareMiddleware:
198
+ """
199
+ ASGI middleware that resolves the client IP address and attaches it to request state:
200
+ - `request.state.client_ip`: IPv4Address | IPv6Address | None
201
+ - `request.state.ip_trusted`: bool
202
+ - `request.state.client_ip_str`: str | None
203
+
204
+ Pass `ipware` to use a configured resolver. `strict` overrides that
205
+ resolver's `default_strict`; omit it to use `default_strict`.
206
+ """
207
+
208
+ def __init__(
209
+ self,
210
+ app: ASGIApp,
211
+ ipware: FastAPIIpWare | None = None,
212
+ strict: bool | None = None,
213
+ ) -> None:
214
+ self.app = app
215
+ self.ipware = ipware if ipware is not None else FastAPIIpWare()
216
+ self.strict = strict
217
+
218
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
219
+ if scope["type"] in ("http", "websocket"):
220
+ conn = HTTPConnection(scope)
221
+ ip, trusted = self.ipware.get_client_ip_from_request(
222
+ conn, strict=self.strict
223
+ )
224
+ scope.setdefault("state", {})
225
+ scope["state"]["client_ip"] = ip
226
+ scope["state"]["ip_trusted"] = trusted
227
+ scope["state"]["client_ip_str"] = str(ip) if ip is not None else None
228
+
229
+ await self.app(scope, receive, send)
230
+
231
+
232
+ __all__ = [
233
+ "DEFAULT_PRECEDENCE",
234
+ "Algorithm",
235
+ "ClientIpResult",
236
+ "FastAPIIpWare",
237
+ "IpAddressType",
238
+ "IpWareMiddleware",
239
+ "__version__",
240
+ ]