uvicorn 0.30.6__py3-none-any.whl → 0.31.1__py3-none-any.whl
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.
- uvicorn/__init__.py +1 -1
- uvicorn/config.py +1 -1
- uvicorn/main.py +5 -3
- uvicorn/middleware/proxy_headers.py +122 -50
- uvicorn/protocols/websockets/websockets_impl.py +4 -8
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.1.dist-info}/METADATA +1 -1
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.1.dist-info}/RECORD +10 -10
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.1.dist-info}/WHEEL +0 -0
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.1.dist-info}/entry_points.txt +0 -0
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.1.dist-info}/licenses/LICENSE.md +0 -0
uvicorn/__init__.py
CHANGED
uvicorn/config.py
CHANGED
@@ -213,7 +213,7 @@ class Config:
|
|
213
213
|
timeout_notify: int = 30,
|
214
214
|
timeout_graceful_shutdown: int | None = None,
|
215
215
|
callback_notify: Callable[..., Awaitable[None]] | None = None,
|
216
|
-
ssl_keyfile: str | None = None,
|
216
|
+
ssl_keyfile: str | os.PathLike[str] | None = None,
|
217
217
|
ssl_certfile: str | os.PathLike[str] | None = None,
|
218
218
|
ssl_keyfile_password: str | None = None,
|
219
219
|
ssl_version: int = SSL_PROTOCOL_VERSION,
|
uvicorn/main.py
CHANGED
@@ -240,8 +240,10 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
|
240
240
|
"--forwarded-allow-ips",
|
241
241
|
type=str,
|
242
242
|
default=None,
|
243
|
-
help="Comma separated list of
|
244
|
-
"
|
243
|
+
help="Comma separated list of IP Addresses, IP Networks, or literals "
|
244
|
+
"(e.g. UNIX Socket path) to trust with proxy headers. Defaults to the "
|
245
|
+
"$FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. "
|
246
|
+
"The literal '*' means trust everything.",
|
245
247
|
)
|
246
248
|
@click.option(
|
247
249
|
"--root-path",
|
@@ -495,7 +497,7 @@ def run(
|
|
495
497
|
limit_max_requests: int | None = None,
|
496
498
|
timeout_keep_alive: int = 5,
|
497
499
|
timeout_graceful_shutdown: int | None = None,
|
498
|
-
ssl_keyfile: str | None = None,
|
500
|
+
ssl_keyfile: str | os.PathLike[str] | None = None,
|
499
501
|
ssl_certfile: str | os.PathLike[str] | None = None,
|
500
502
|
ssl_keyfile_password: str | None = None,
|
501
503
|
ssl_version: int = SSL_PROTOCOL_VERSION,
|
@@ -1,70 +1,142 @@
|
|
1
|
-
|
2
|
-
This middleware can be used when a known proxy is fronting the application,
|
3
|
-
and is trusted to be properly setting the `X-Forwarded-Proto` and
|
4
|
-
`X-Forwarded-For` headers with the connecting client information.
|
1
|
+
from __future__ import annotations
|
5
2
|
|
6
|
-
|
7
|
-
the connecting client, rather that the connecting proxy.
|
3
|
+
import ipaddress
|
8
4
|
|
9
|
-
|
10
|
-
"""
|
5
|
+
from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope
|
11
6
|
|
12
|
-
from __future__ import annotations
|
13
7
|
|
14
|
-
|
8
|
+
class ProxyHeadersMiddleware:
|
9
|
+
"""Middleware for handling known proxy headers
|
15
10
|
|
16
|
-
|
11
|
+
This middleware can be used when a known proxy is fronting the application,
|
12
|
+
and is trusted to be properly setting the `X-Forwarded-Proto` and
|
13
|
+
`X-Forwarded-For` headers with the connecting client information.
|
17
14
|
|
15
|
+
Modifies the `client` and `scheme` information so that they reference
|
16
|
+
the connecting client, rather that the connecting proxy.
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
) -> None:
|
18
|
+
References:
|
19
|
+
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies>
|
20
|
+
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For>
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None:
|
25
24
|
self.app = app
|
26
|
-
|
27
|
-
self.trusted_hosts = {item.strip() for item in trusted_hosts.split(",")}
|
28
|
-
else:
|
29
|
-
self.trusted_hosts = set(trusted_hosts)
|
30
|
-
self.always_trust = "*" in self.trusted_hosts
|
25
|
+
self.trusted_hosts = _TrustedHosts(trusted_hosts)
|
31
26
|
|
32
|
-
def
|
33
|
-
if
|
34
|
-
return
|
27
|
+
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
28
|
+
if scope["type"] == "lifespan":
|
29
|
+
return await self.app(scope, receive, send)
|
35
30
|
|
36
|
-
|
37
|
-
|
38
|
-
return host
|
31
|
+
client_addr = scope.get("client")
|
32
|
+
client_host = client_addr[0] if client_addr else None
|
39
33
|
|
40
|
-
|
34
|
+
if client_host in self.trusted_hosts:
|
35
|
+
headers = dict(scope["headers"])
|
41
36
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
client_host = client_addr[0] if client_addr else None
|
47
|
-
|
48
|
-
if self.always_trust or client_host in self.trusted_hosts:
|
49
|
-
headers = dict(scope["headers"])
|
50
|
-
|
51
|
-
if b"x-forwarded-proto" in headers:
|
52
|
-
# Determine if the incoming request was http or https based on
|
53
|
-
# the X-Forwarded-Proto header.
|
54
|
-
x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip()
|
37
|
+
if b"x-forwarded-proto" in headers:
|
38
|
+
x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip()
|
39
|
+
|
40
|
+
if x_forwarded_proto in {"http", "https", "ws", "wss"}:
|
55
41
|
if scope["type"] == "websocket":
|
56
42
|
scope["scheme"] = x_forwarded_proto.replace("http", "ws")
|
57
43
|
else:
|
58
44
|
scope["scheme"] = x_forwarded_proto
|
59
45
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
46
|
+
if b"x-forwarded-for" in headers:
|
47
|
+
x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1")
|
48
|
+
host = self.trusted_hosts.get_trusted_client_host(x_forwarded_for)
|
49
|
+
|
50
|
+
if host:
|
51
|
+
# If the x-forwarded-for header is empty then host is an empty string.
|
52
|
+
# Only set the client if we actually got something usable.
|
53
|
+
# See: https://github.com/encode/uvicorn/issues/1068
|
54
|
+
|
55
|
+
# We've lost the connecting client's port information by now,
|
56
|
+
# so only include the host.
|
67
57
|
port = 0
|
68
|
-
scope["client"] = (host, port)
|
58
|
+
scope["client"] = (host, port)
|
69
59
|
|
70
60
|
return await self.app(scope, receive, send)
|
61
|
+
|
62
|
+
|
63
|
+
def _parse_raw_hosts(value: str) -> list[str]:
|
64
|
+
return [item.strip() for item in value.split(",")]
|
65
|
+
|
66
|
+
|
67
|
+
class _TrustedHosts:
|
68
|
+
"""Container for trusted hosts and networks"""
|
69
|
+
|
70
|
+
def __init__(self, trusted_hosts: list[str] | str) -> None:
|
71
|
+
self.always_trust: bool = trusted_hosts in ("*", ["*"])
|
72
|
+
|
73
|
+
self.trusted_literals: set[str] = set()
|
74
|
+
self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set()
|
75
|
+
self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set()
|
76
|
+
|
77
|
+
# Notes:
|
78
|
+
# - We separate hosts from literals as there are many ways to write
|
79
|
+
# an IPv6 Address so we need to compare by object.
|
80
|
+
# - We don't convert IP Address to single host networks (e.g. /32 / 128) as
|
81
|
+
# it more efficient to do an address lookup in a set than check for
|
82
|
+
# membership in each network.
|
83
|
+
# - We still allow literals as it might be possible that we receive a
|
84
|
+
# something that isn't an IP Address e.g. a unix socket.
|
85
|
+
|
86
|
+
if not self.always_trust:
|
87
|
+
if isinstance(trusted_hosts, str):
|
88
|
+
trusted_hosts = _parse_raw_hosts(trusted_hosts)
|
89
|
+
|
90
|
+
for host in trusted_hosts:
|
91
|
+
# Note: because we always convert invalid IP types to literals it
|
92
|
+
# is not possible for the user to know they provided a malformed IP
|
93
|
+
# type - this may lead to unexpected / difficult to debug behaviour.
|
94
|
+
|
95
|
+
if "/" in host:
|
96
|
+
# Looks like a network
|
97
|
+
try:
|
98
|
+
self.trusted_networks.add(ipaddress.ip_network(host))
|
99
|
+
except ValueError:
|
100
|
+
# Was not a valid IP Network
|
101
|
+
self.trusted_literals.add(host)
|
102
|
+
else:
|
103
|
+
try:
|
104
|
+
self.trusted_hosts.add(ipaddress.ip_address(host))
|
105
|
+
except ValueError:
|
106
|
+
# Was not a valid IP Address
|
107
|
+
self.trusted_literals.add(host)
|
108
|
+
|
109
|
+
def __contains__(self, host: str | None) -> bool:
|
110
|
+
if self.always_trust:
|
111
|
+
return True
|
112
|
+
|
113
|
+
if not host:
|
114
|
+
return False
|
115
|
+
|
116
|
+
try:
|
117
|
+
ip = ipaddress.ip_address(host)
|
118
|
+
if ip in self.trusted_hosts:
|
119
|
+
return True
|
120
|
+
return any(ip in net for net in self.trusted_networks)
|
121
|
+
|
122
|
+
except ValueError:
|
123
|
+
return host in self.trusted_literals
|
124
|
+
|
125
|
+
def get_trusted_client_host(self, x_forwarded_for: str) -> str:
|
126
|
+
"""Extract the client host from x_forwarded_for header
|
127
|
+
|
128
|
+
In general this is the first "untrusted" host in the forwarded for list.
|
129
|
+
"""
|
130
|
+
x_forwarded_for_hosts = _parse_raw_hosts(x_forwarded_for)
|
131
|
+
|
132
|
+
if self.always_trust:
|
133
|
+
return x_forwarded_for_hosts[0]
|
134
|
+
|
135
|
+
# Note: each proxy appends to the header list so check it in reverse order
|
136
|
+
for host in reversed(x_forwarded_for_hosts):
|
137
|
+
if host not in self:
|
138
|
+
return host
|
139
|
+
|
140
|
+
# All hosts are trusted meaning that the client was also a trusted proxy
|
141
|
+
# See https://github.com/encode/uvicorn/issues/1068#issuecomment-855371576
|
142
|
+
return x_forwarded_for_hosts[0]
|
@@ -224,9 +224,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
224
224
|
# itself (see https://github.com/encode/uvicorn/issues/920)
|
225
225
|
self.handshake_started_event.set()
|
226
226
|
|
227
|
-
async def ws_handler( # type: ignore[override]
|
228
|
-
self, protocol: WebSocketServerProtocol, path: str
|
229
|
-
) -> Any:
|
227
|
+
async def ws_handler(self, protocol: WebSocketServerProtocol, path: str) -> Any: # type: ignore[override]
|
230
228
|
"""
|
231
229
|
This is the main handler function for the 'websockets' implementation
|
232
230
|
to call into. We just wait for close then return, and instead allow
|
@@ -359,9 +357,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
359
357
|
msg = "Unexpected ASGI message '%s', after sending 'websocket.close' " "or response already completed."
|
360
358
|
raise RuntimeError(msg % message_type)
|
361
359
|
|
362
|
-
async def asgi_receive(
|
363
|
-
self,
|
364
|
-
) -> WebSocketDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent:
|
360
|
+
async def asgi_receive(self) -> WebSocketDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent:
|
365
361
|
if not self.connect_sent:
|
366
362
|
self.connect_sent = True
|
367
363
|
return {"type": "websocket.connect"}
|
@@ -378,11 +374,11 @@ class WebSocketProtocol(WebSocketServerProtocol):
|
|
378
374
|
|
379
375
|
try:
|
380
376
|
data = await self.recv()
|
381
|
-
except ConnectionClosed
|
377
|
+
except ConnectionClosed:
|
382
378
|
self.closed_event.set()
|
383
379
|
if self.ws_server.closing:
|
384
380
|
return {"type": "websocket.disconnect", "code": 1012}
|
385
|
-
return {"type": "websocket.disconnect", "code":
|
381
|
+
return {"type": "websocket.disconnect", "code": self.close_code or 1005, "reason": self.close_reason}
|
386
382
|
|
387
383
|
if isinstance(data, str):
|
388
384
|
return {"type": "websocket.receive", "text": data}
|
@@ -1,11 +1,11 @@
|
|
1
|
-
uvicorn/__init__.py,sha256=
|
1
|
+
uvicorn/__init__.py,sha256=vv20frJB2RygWYa40Tn-_aITeQZqylK1YdgUn-z3Fwk,147
|
2
2
|
uvicorn/__main__.py,sha256=DQizy6nKP0ywhPpnCHgmRDYIMfcqZKVEzNIWQZjqtVQ,62
|
3
3
|
uvicorn/_subprocess.py,sha256=HbfRnsCkXyg7xCWVAWWzXQTeWlvLKfTlIF5wevFBkR4,2766
|
4
4
|
uvicorn/_types.py,sha256=TcUzCyKNq90ZX2Hxa6ce0juF558zLO_AyBB1XijnD2Y,7814
|
5
|
-
uvicorn/config.py,sha256=
|
5
|
+
uvicorn/config.py,sha256=gpsukzuJFbXBRN5_qOnLGiENmGgKM0Fi-eZJ6GwM1dQ,20849
|
6
6
|
uvicorn/importer.py,sha256=nRt0QQ3qpi264-n_mR0l55C2ddM8nowTNzT1jsWaam8,1128
|
7
7
|
uvicorn/logging.py,sha256=sg4D9lHaW_kKQj_kmP-bolbChjKfhBuihktlWp8RjSI,4236
|
8
|
-
uvicorn/main.py,sha256=
|
8
|
+
uvicorn/main.py,sha256=iv6ptgDBnko0W-VkHs0e3I4UrF7_5sEZrntQNKJGFNY,16915
|
9
9
|
uvicorn/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
10
10
|
uvicorn/server.py,sha256=pIpMlW1WMWxarhM3wuZrffX0OcTEKoZXA82oY_M25d8,12879
|
11
11
|
uvicorn/workers.py,sha256=DukTKlrCyyvWVHbJWBJflIV2yUe-q6KaGdrEwLrNmyc,3893
|
@@ -19,7 +19,7 @@ uvicorn/loops/uvloop.py,sha256=K4QybYVxtK9C2emDhDPUCkBXR4XMT5Ofv9BPFPoX0ok,148
|
|
19
19
|
uvicorn/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
20
|
uvicorn/middleware/asgi2.py,sha256=YQrQNm3RehFts3mzk3k4yw8aD8Egtj0tRS3N45YkQa0,394
|
21
21
|
uvicorn/middleware/message_logger.py,sha256=IHEZUSnFNaMFUFdwtZO3AuFATnYcSor-gVtOjbCzt8M,2859
|
22
|
-
uvicorn/middleware/proxy_headers.py,sha256=
|
22
|
+
uvicorn/middleware/proxy_headers.py,sha256=f1VDAc-ipPHdNTuLNHwYCeDgYXoCL_VjD6hDTUXZT_U,5790
|
23
23
|
uvicorn/middleware/wsgi.py,sha256=TBeG4W_gEmWddbGfWyxdzJ0IDaWWkJZyF8eIp-1fv0U,7111
|
24
24
|
uvicorn/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
25
|
uvicorn/protocols/utils.py,sha256=rCjYLd4_uwPeZkbRXQ6beCfxyI_oYpvJCwz3jEGNOiE,1849
|
@@ -30,7 +30,7 @@ uvicorn/protocols/http/h11_impl.py,sha256=MuX72-pIyZGHDtZ75-1mveeTj6_ruL-306Ug7z
|
|
30
30
|
uvicorn/protocols/http/httptools_impl.py,sha256=TikbbIZRFG08KTClZER47ehM1Tu8koBfT6WGU5t5ACg,21491
|
31
31
|
uvicorn/protocols/websockets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
32
|
uvicorn/protocols/websockets/auto.py,sha256=kNP-h07ZzjA9dKRUd7MNO0J7xhRJ5xVBfit7wCbdB0A,574
|
33
|
-
uvicorn/protocols/websockets/websockets_impl.py,sha256=
|
33
|
+
uvicorn/protocols/websockets/websockets_impl.py,sha256=LV58OW3whQAd4iwbJl4R3iIod8myVYK3IhAn6F5VeZ4,15490
|
34
34
|
uvicorn/protocols/websockets/wsproto_impl.py,sha256=haJEXK82Ldu8_hz4NDxQ0KpPXa9vOi6pG6iDLoBDKjs,15341
|
35
35
|
uvicorn/supervisors/__init__.py,sha256=UVJYW3RVHMDSgUytToyAgGyd9NUQVqbNpVrQrvm4Tpc,700
|
36
36
|
uvicorn/supervisors/basereload.py,sha256=Hxezjgt_HXkOPVj-hJGH7uj0bZ3EhmwsmaOBc63ySoM,3831
|
@@ -38,8 +38,8 @@ uvicorn/supervisors/multiprocess.py,sha256=Opt0XvOUj1DIMXYwb4OlkJZxeh_RjweFnTmDP
|
|
38
38
|
uvicorn/supervisors/statreload.py,sha256=gc-HUB44f811PvxD_ZIEQYenM7mWmhQQjYg7KKQ1c5o,1542
|
39
39
|
uvicorn/supervisors/watchfilesreload.py,sha256=41FGNMXPKrKvPr-5O8yRWg43l6OCBtapt39M-gpdk0E,3010
|
40
40
|
uvicorn/supervisors/watchgodreload.py,sha256=kd-gOvp14ArTNIc206Nt5CEjZZ4NP2UmMVYE7571yRQ,5486
|
41
|
-
uvicorn-0.
|
42
|
-
uvicorn-0.
|
43
|
-
uvicorn-0.
|
44
|
-
uvicorn-0.
|
45
|
-
uvicorn-0.
|
41
|
+
uvicorn-0.31.1.dist-info/METADATA,sha256=ClTG1r-p8xfowikO16LdWLojw-qLIdv4VC9UsPP0KjE,6569
|
42
|
+
uvicorn-0.31.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
43
|
+
uvicorn-0.31.1.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
|
44
|
+
uvicorn-0.31.1.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
|
45
|
+
uvicorn-0.31.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|