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 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.5"
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]
@@ -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, upgrade: bytes | None) -> bool:
145
- if upgrade == b"websocket" and self.ws_protocol_class is not None:
146
- return True
147
- if self.config.ws == "auto":
148
- msg = "Unsupported upgrade request."
149
- self.logger.warning(msg)
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(upgrade)
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
- upgrade = self._get_upgrade()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uvicorn
3
- Version: 0.30.5
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=aSoUZbVO0X1F9pIylSyTOjDCsq4sO-CYKjka_R5eP0Y,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,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=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
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=37iA--AYCidYBFlcwE8vFEuLXBR_eiFT_inSp548dN8,20541
30
- uvicorn/protocols/http/httptools_impl.py,sha256=IFxanBZZDlTePYKJ4qV3-L8iVSfxT8wh93FePcroAVY,21475
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.30.5.dist-info/METADATA,sha256=k_D1EOmTRZCMibKvZHu_XNcxIpSE65BQsBd0JjUf1DU,6569
42
- uvicorn-0.30.5.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
43
- uvicorn-0.30.5.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
44
- uvicorn-0.30.5.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
45
- uvicorn-0.30.5.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,,