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 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.0"
5
5
  __all__ = ["main", "run", "Config", "Server"]
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",
@@ -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 == "*"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.30.6
3
+ Version: 0.31.0
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=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=jmZOCNq5frbWmlflJpGJ1wpqPxlXTDxcN8bGm2TQfSo,16783
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=McSfCWvhMEFOTkUbQWgnt9QCBIAY9RfFSaEZboBbAYg,3065
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.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.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,,