uvicorn 0.30.6__py3-none-any.whl → 0.31.0__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/main.py +4 -2
- uvicorn/middleware/proxy_headers.py +122 -50
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.0.dist-info}/METADATA +1 -1
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.0.dist-info}/RECORD +8 -8
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.0.dist-info}/WHEEL +0 -0
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.0.dist-info}/entry_points.txt +0 -0
- {uvicorn-0.30.6.dist-info → uvicorn-0.31.0.dist-info}/licenses/LICENSE.md +0 -0
uvicorn/__init__.py
CHANGED
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",
|
@@ -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 == "*"
|
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]
|
@@ -1,11 +1,11 @@
|
|
1
|
-
uvicorn/__init__.py,sha256=
|
1
|
+
uvicorn/__init__.py,sha256=C-q5FbOk-czeRzwHSldRMGQJKWQE5uLkJc9U3BvMCb8,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
5
|
uvicorn/config.py,sha256=4PZiIBMV8Bu8pNq63-P3pv5ynyEGz-K0aVoC98Y5hrQ,20830
|
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=zWgYECz0qZh0kj0Y-IoF0AthvohKz9hmzqMyDEOwa80,16896
|
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=fUMuYPOA23IJ3zQQBv5GOV1g8yu6hA5RpXhOXgnFu7Y,5781
|
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
|
@@ -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.0.dist-info/METADATA,sha256=VKk1dOX3pwKhtJjjDXFzq6teX5AMs_jvlSofsXUJUrQ,6569
|
42
|
+
uvicorn-0.31.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
43
|
+
uvicorn-0.31.0.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
|
44
|
+
uvicorn-0.31.0.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
|
45
|
+
uvicorn-0.31.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|