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 CHANGED
@@ -1,5 +1,5 @@
1
1
  from uvicorn.config import Config
2
2
  from uvicorn.main import Server, main, run
3
3
 
4
- __version__ = "0.30.6"
4
+ __version__ = "0.31.1"
5
5
  __all__ = ["main", "run", "Config", "Server"]
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 IPs to trust with proxy headers. Defaults to"
244
- " the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'.",
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
- Modifies the `client` and `scheme` information so that they reference
7
- the connecting client, rather that the connecting proxy.
3
+ import ipaddress
8
4
 
9
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies
10
- """
5
+ from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope
11
6
 
12
- from __future__ import annotations
13
7
 
14
- from typing import Union, cast
8
+ class ProxyHeadersMiddleware:
9
+ """Middleware for handling known proxy headers
15
10
 
16
- from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, HTTPScope, Scope, WebSocketScope
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
- class ProxyHeadersMiddleware:
20
- def __init__(
21
- self,
22
- app: ASGI3Application,
23
- trusted_hosts: list[str] | str = "127.0.0.1",
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
- if isinstance(trusted_hosts, str):
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 get_trusted_client_host(self, x_forwarded_for_hosts: list[str]) -> str | None:
33
- if self.always_trust:
34
- return x_forwarded_for_hosts[0]
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
- for host in reversed(x_forwarded_for_hosts):
37
- if host not in self.trusted_hosts:
38
- return host
31
+ client_addr = scope.get("client")
32
+ client_host = client_addr[0] if client_addr else None
39
33
 
40
- return None # pragma: full coverage
34
+ if client_host in self.trusted_hosts:
35
+ headers = dict(scope["headers"])
41
36
 
42
- async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
43
- if scope["type"] in ("http", "websocket"):
44
- scope = cast(Union["HTTPScope", "WebSocketScope"], scope)
45
- client_addr: tuple[str, int] | None = scope.get("client")
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
- if b"x-forwarded-for" in headers:
61
- # Determine the client address from the last trusted IP in the
62
- # X-Forwarded-For header. We've lost the connecting client's port
63
- # information by now, so only include the host.
64
- x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1")
65
- x_forwarded_for_hosts = [item.strip() for item in x_forwarded_for.split(",")]
66
- host = self.get_trusted_client_host(x_forwarded_for_hosts)
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) # type: ignore[arg-type]
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 as exc:
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": exc.code, "reason": exc.reason}
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.30.6
3
+ Version: 0.31.1
4
4
  Summary: The lightning-fast ASGI server.
5
5
  Project-URL: Changelog, https://github.com/encode/uvicorn/blob/master/CHANGELOG.md
6
6
  Project-URL: Funding, https://github.com/sponsors/encode
@@ -1,11 +1,11 @@
1
- uvicorn/__init__.py,sha256=o3l1sMdW81iZNyIR--S-G0OX1ua9vRy24ml-4PSD9H4,147
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=4PZiIBMV8Bu8pNq63-P3pv5ynyEGz-K0aVoC98Y5hrQ,20830
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=jmZOCNq5frbWmlflJpGJ1wpqPxlXTDxcN8bGm2TQfSo,16783
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=McSfCWvhMEFOTkUbQWgnt9QCBIAY9RfFSaEZboBbAYg,3065
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=59SLT1Q2sXnpbfxdk5e2SDTJPjrxOvqsYQOHxxCjCP4,15504
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.30.6.dist-info/METADATA,sha256=8mHWCwo1g631l-1XL1Km8_W6ik_qHHawLJfb792sKF4,6569
42
- uvicorn-0.30.6.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
43
- uvicorn-0.30.6.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
44
- uvicorn-0.30.6.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
45
- uvicorn-0.30.6.dist-info/RECORD,,
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,,