uvicorn 0.30.5__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/protocols/http/h11_impl.py +16 -8
- uvicorn/protocols/http/httptools_impl.py +12 -10
- {uvicorn-0.30.5.dist-info → uvicorn-0.31.0.dist-info}/METADATA +1 -1
- {uvicorn-0.30.5.dist-info → uvicorn-0.31.0.dist-info}/RECORD +10 -10
- {uvicorn-0.30.5.dist-info → uvicorn-0.31.0.dist-info}/WHEEL +0 -0
- {uvicorn-0.30.5.dist-info → uvicorn-0.31.0.dist-info}/entry_points.txt +0 -0
- {uvicorn-0.30.5.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]
|
@@ -147,14 +147,24 @@ class H11Protocol(asyncio.Protocol):
|
|
147
147
|
|
148
148
|
def _should_upgrade_to_ws(self) -> bool:
|
149
149
|
if self.ws_protocol_class is None:
|
150
|
-
if self.config.ws == "auto":
|
151
|
-
msg = "Unsupported upgrade request."
|
152
|
-
self.logger.warning(msg)
|
153
|
-
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
154
|
-
self.logger.warning(msg)
|
155
150
|
return False
|
156
151
|
return True
|
157
152
|
|
153
|
+
def _unsupported_upgrade_warning(self) -> None:
|
154
|
+
msg = "Unsupported upgrade request."
|
155
|
+
self.logger.warning(msg)
|
156
|
+
if not self._should_upgrade_to_ws():
|
157
|
+
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
158
|
+
self.logger.warning(msg)
|
159
|
+
|
160
|
+
def _should_upgrade(self) -> bool:
|
161
|
+
upgrade = self._get_upgrade()
|
162
|
+
if upgrade == b"websocket" and self._should_upgrade_to_ws():
|
163
|
+
return True
|
164
|
+
if upgrade is not None:
|
165
|
+
self._unsupported_upgrade_warning()
|
166
|
+
return False
|
167
|
+
|
158
168
|
def data_received(self, data: bytes) -> None:
|
159
169
|
self._unset_keepalive_if_required()
|
160
170
|
|
@@ -206,9 +216,7 @@ class H11Protocol(asyncio.Protocol):
|
|
206
216
|
"headers": self.headers,
|
207
217
|
"state": self.app_state.copy(),
|
208
218
|
}
|
209
|
-
|
210
|
-
upgrade = self._get_upgrade()
|
211
|
-
if upgrade == b"websocket" and self._should_upgrade_to_ws():
|
219
|
+
if self._should_upgrade():
|
212
220
|
self.handle_websocket_upgrade(event)
|
213
221
|
return
|
214
222
|
|
@@ -141,19 +141,20 @@ class HttpToolsProtocol(asyncio.Protocol):
|
|
141
141
|
return upgrade
|
142
142
|
return None # pragma: full coverage
|
143
143
|
|
144
|
-
def _should_upgrade_to_ws(self
|
145
|
-
if
|
146
|
-
return
|
147
|
-
|
148
|
-
|
149
|
-
|
144
|
+
def _should_upgrade_to_ws(self) -> bool:
|
145
|
+
if self.ws_protocol_class is None:
|
146
|
+
return False
|
147
|
+
return True
|
148
|
+
|
149
|
+
def _unsupported_upgrade_warning(self) -> None:
|
150
|
+
self.logger.warning("Unsupported upgrade request.")
|
151
|
+
if not self._should_upgrade_to_ws():
|
150
152
|
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
151
153
|
self.logger.warning(msg)
|
152
|
-
return False
|
153
154
|
|
154
155
|
def _should_upgrade(self) -> bool:
|
155
156
|
upgrade = self._get_upgrade()
|
156
|
-
return self._should_upgrade_to_ws(
|
157
|
+
return upgrade == b"websocket" and self._should_upgrade_to_ws()
|
157
158
|
|
158
159
|
def data_received(self, data: bytes) -> None:
|
159
160
|
self._unset_keepalive_if_required()
|
@@ -166,9 +167,10 @@ class HttpToolsProtocol(asyncio.Protocol):
|
|
166
167
|
self.send_400_response(msg)
|
167
168
|
return
|
168
169
|
except httptools.HttpParserUpgrade:
|
169
|
-
|
170
|
-
if self._should_upgrade_to_ws(upgrade):
|
170
|
+
if self._should_upgrade():
|
171
171
|
self.handle_websocket_upgrade()
|
172
|
+
else:
|
173
|
+
self._unsupported_upgrade_warning()
|
172
174
|
|
173
175
|
def handle_websocket_upgrade(self) -> None:
|
174
176
|
if self.logger.level <= TRACE_LOG_LEVEL:
|
@@ -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,15 +19,15 @@ 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
|
26
26
|
uvicorn/protocols/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
27
|
uvicorn/protocols/http/auto.py,sha256=YfXGyzWTaaE2p_jkTPWrJCXsxEaQnC3NK0-G7Wgmnls,403
|
28
28
|
uvicorn/protocols/http/flow_control.py,sha256=050WVg31EvPOkHwynCoMP1zXFl_vO3U4durlc5vyp4U,1701
|
29
|
-
uvicorn/protocols/http/h11_impl.py,sha256=
|
30
|
-
uvicorn/protocols/http/httptools_impl.py,sha256=
|
29
|
+
uvicorn/protocols/http/h11_impl.py,sha256=MuX72-pIyZGHDtZ75-1mveeTj6_ruL-306Ug7z0yV8w,20765
|
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
33
|
uvicorn/protocols/websockets/websockets_impl.py,sha256=59SLT1Q2sXnpbfxdk5e2SDTJPjrxOvqsYQOHxxCjCP4,15504
|
@@ -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
|