Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websockets/http11.py: 88%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import dataclasses
4import os
5import re
6import sys
7import warnings
8from collections.abc import Generator
9from typing import Callable
11from .datastructures import Headers
12from .exceptions import (
13 HeaderLineTooLong,
14 RequestLineTooLong,
15 SecurityError,
16 StatusLineTooLong,
17 TooManyHeaders,
18)
19from .version import version as websockets_version
22__all__ = [
23 "SERVER",
24 "USER_AGENT",
25 "Request",
26 "Response",
27]
30PYTHON_VERSION = "{}.{}".format(*sys.version_info)
32# User-Agent header for HTTP requests.
33USER_AGENT = os.environ.get(
34 "WEBSOCKETS_USER_AGENT",
35 f"Python/{PYTHON_VERSION} websockets/{websockets_version}",
36)
38# Server header for HTTP responses.
39SERVER = os.environ.get(
40 "WEBSOCKETS_SERVER",
41 f"Python/{PYTHON_VERSION} websockets/{websockets_version}",
42)
44# Maximum total size of headers is around 128 * 8 KiB = 1 MiB.
45MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128"))
47# Limit request line and header lines. 8KiB is the most common default
48# configuration of popular HTTP servers.
49MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192"))
51# Support for HTTP response bodies is intended to read an error message
52# returned by a server. It isn't designed to perform large file transfers.
53MAX_BODY_SIZE = int(os.environ.get("WEBSOCKETS_MAX_BODY_SIZE", "1_048_576")) # 1 MiB
56def d(value: bytes | bytearray) -> str:
57 """
58 Decode a bytestring for interpolating into an error message.
60 """
61 return value.decode(errors="backslashreplace")
64# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B.
66# Regex for validating header names.
68_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
70# Regex for validating header values.
72# We don't attempt to support obsolete line folding.
74# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff).
76# The ABNF is complicated because it attempts to express that optional
77# whitespace is ignored. We strip whitespace and don't revalidate that.
79# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
81_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*")
83# Regexes for validating status codes and numeric header values.
84# Set a lower limit than default_max_str_digits; 1 EB is plenty.
86# Content-Length = 1*DIGIT
87# See https://datatracker.ietf.org/doc/html/rfc9110#section-8.6
89_digits_re = re.compile(rb"[0-9]{1,18}")
91# chunk-size = 1*HEXDIG
92# See https://datatracker.ietf.org/doc/html/rfc9112#section-7.1
94_hexdigits_re = re.compile(rb"[0-9a-fA-F]{1,15}")
97@dataclasses.dataclass
98class Request:
99 """
100 WebSocket handshake request.
102 ``method`` and ``path`` must contain only ASCII characters. ``headers``
103 should contain only ASCII characters; however, non-ASCII header values are
104 tolerated and encoded as ISO-8859-1.
106 Attributes:
107 path: Request path, including optional query.
108 headers: Request headers.
109 method: Request method; WebSocket handshake requests use GET.
110 protocol: Request protocol; WebSocket handshake requests use HTTP/1.1.
111 """
113 path: str
114 headers: Headers
115 # method and protocol have a default value, so they're declared after path
116 # and headers which don't.
117 method: str = "GET"
118 protocol: str = "HTTP/1.1"
119 # body isn't useful is the context of this library.
121 _exception: Exception | None = None
123 @property
124 def exception(self) -> Exception | None: # pragma: no cover
125 warnings.warn( # deprecated in 10.3 - 2022-04-17
126 "Request.exception is deprecated; use ServerProtocol.handshake_exc instead",
127 DeprecationWarning,
128 )
129 return self._exception
131 @classmethod
132 def parse(
133 cls,
134 read_line: Callable[
135 [int, type[Exception]], Generator[None, None, bytes | bytearray]
136 ],
137 ) -> Generator[None, None, Request]:
138 """
139 Parse a WebSocket handshake request.
141 This is a generator-based coroutine.
143 The request method and path must contain only ASCII characters. The
144 request path isn't URL-decoded or validated in any way. Request headers
145 should contain only ASCII characters; however, non-ASCII header values
146 are tolerated and decoded with ISO-8859-1.
148 :meth:`parse` doesn't read the request body because WebSocket handshake
149 requests don't have one. If the request contains a body, it may be read
150 from the data stream after :meth:`parse` returns.
152 Args:
153 read_line: Generator-based coroutine that reads a LF-terminated
154 line or raises an exception if there isn't enough data
156 Raises:
157 EOFError: If the connection is closed without a full HTTP request.
158 RequestLineTooLong: If the request line is too long.
159 HeaderLineTooLong: If a header line is too long.
160 TooManyHeaders: If there are too many headers.
161 UnicodeDecodeError: If the request method or path isn't ASCII.
162 ValueError: If the request isn't well formatted.
163 NotImplementedError: If the request uses Transfer-Encoding.
165 """
166 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
168 # Parsing is simple because a fixed value is expected for the version
169 # and because path isn't checked. Since WebSocket libraries generally
170 # implement HTTP/1.1 strictly, there's little need for lenient parsing.
172 try:
173 request_line = yield from parse_line(read_line, RequestLineTooLong)
174 except EOFError as exc:
175 raise EOFError("connection closed while reading HTTP request line") from exc
177 try:
178 raw_method, raw_path, raw_protocol = request_line.split(b" ", 2)
179 except ValueError: # not enough values to unpack (expected 3, got 1-2)
180 raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None
181 if raw_protocol not in [b"HTTP/1.1", b"HTTP/1.0"]:
182 raise ValueError(
183 f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "
184 f"{d(request_line)}"
185 )
186 method = raw_method.decode("ascii")
187 protocol = raw_protocol.decode("ascii")
189 # RFC 9110 defers the definition of URIs to RFC 3986, which allows only
190 # a subset of ASCII. Non-ASCII IRIs must be UTF-8 then percent-encoded.
191 path = raw_path.decode("ascii")
193 headers = yield from parse_headers(read_line)
195 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
197 if "Transfer-Encoding" in headers:
198 raise NotImplementedError("transfer codings aren't supported")
200 # Some devices send a Content-Length header with a value of 0.
201 # Reject any other value. For technical correctness, accept
202 # multiple Content-Length headers, all with a value of 0.
203 if not set(headers.get_all("Content-Length")) <= {"0"}:
204 raise ValueError("unsupported request body")
206 return cls(path, headers, method, protocol)
208 def serialize(self) -> bytes:
209 """
210 Serialize a WebSocket handshake request.
212 """
213 # Methods are hardcoded and always ASCII. Non-ASCII paths are converted
214 # from URI to IRI and percent-encoded. Enforce ASCII as a safety net.
215 request_line = f"{self.method} {self.path} {self.protocol}\r\n"
216 request = request_line.encode("ascii")
217 request += self.headers.serialize()
218 return request
221@dataclasses.dataclass
222class Response:
223 """
224 WebSocket handshake response.
226 ``reason_phrase`` and ``headers`` should contain only ASCII characters;
227 however, non-ASCII reason phrases and header values are tolerated and
228 encoded as ISO-8859-1.
230 Attributes:
231 status_code: Response code.
232 reason_phrase: Response reason.
233 headers: Response headers.
234 body: Response body.
236 """
238 status_code: int
239 reason_phrase: str
240 headers: Headers
241 body: bytes | bytearray = b""
243 _exception: Exception | None = None
245 @property
246 def exception(self) -> Exception | None: # pragma: no cover
247 warnings.warn( # deprecated in 10.3 - 2022-04-17
248 "Response.exception is deprecated; "
249 "use ClientProtocol.handshake_exc instead",
250 DeprecationWarning,
251 )
252 return self._exception
254 @classmethod
255 def parse(
256 cls,
257 read_line: Callable[
258 [int, type[Exception]], Generator[None, None, bytes | bytearray]
259 ],
260 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
261 read_to_eof: Callable[
262 [int, type[Exception]], Generator[None, None, bytes | bytearray]
263 ],
264 proxy: bool = False,
265 ) -> Generator[None, None, Response]:
266 """
267 Parse a WebSocket handshake response.
269 This is a generator-based coroutine.
271 The reason phrase and headers should contain only ASCII characters;
272 however, non-ASCII reason phrases and header values are tolerated and
273 decoded as ISO-8859-1.
275 Args:
276 read_line: Generator-based coroutine that reads a LF-terminated
277 line or raises an exception if there isn't enough data.
278 read_exact: Generator-based coroutine that reads the requested
279 bytes or raises an exception if there isn't enough data.
280 read_to_eof: Generator-based coroutine that reads until the end
281 of the stream.
283 Raises:
284 EOFError: If the connection is closed without a full HTTP response.
285 StatusLineTooLong: If the status line is too long.
286 HeaderLineTooLong: If a header line is too long.
287 TooManyHeaders: If there are too many headers.
288 SecurityError: If the response body exceeds a security limit.
289 LookupError: If the response isn't well formatted.
290 ValueError: If the response isn't well formatted.
291 NotImplementedError: If the response uses Transfer-Encoding, and
292 it isn't "chunked".
294 """
295 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
297 try:
298 status_line = yield from parse_line(read_line, StatusLineTooLong)
299 except EOFError as exc:
300 raise EOFError("connection closed while reading HTTP status line") from exc
302 try:
303 protocol, raw_status_code, raw_reason = status_line.split(b" ", 2)
304 except ValueError: # not enough values to unpack (expected 3, got 1-2)
305 raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None
306 if proxy: # some proxies still use HTTP/1.0
307 if protocol not in [b"HTTP/1.1", b"HTTP/1.0"]:
308 raise ValueError(
309 f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "
310 f"{d(status_line)}"
311 )
312 else:
313 if protocol != b"HTTP/1.1":
314 raise ValueError(
315 f"unsupported protocol; expected HTTP/1.1: {d(status_line)}"
316 )
317 if not _digits_re.fullmatch(raw_status_code):
318 raise ValueError(
319 f"invalid status code; expected integer; got {d(raw_status_code)}"
320 )
321 status_code = int(raw_status_code)
322 if not 100 <= status_code < 600:
323 raise ValueError(
324 f"invalid status code; expected 100–599; got {d(raw_status_code)}"
325 )
326 if not _value_re.fullmatch(raw_reason):
327 raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}")
329 # RFC 2616 implies ISO-8859-1. It's easy to reverse and cannot crash.
330 # Non-ASCII never worked reliably and the reason isn't useful anyway.
331 reason = raw_reason.decode("iso-8859-1")
333 headers = yield from parse_headers(read_line)
335 body: bytes | bytearray
336 if proxy:
337 body = b""
338 else:
339 body = yield from read_body(
340 status_code, headers, read_line, read_exact, read_to_eof
341 )
343 return cls(status_code, reason, headers, body)
345 def serialize(self) -> bytes:
346 """
347 Serialize a WebSocket handshake response.
349 """
350 # Encode the reason phrase as ISO-8859-1 to round-trip cleanly.
351 status_line = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n"
352 response = status_line.encode("iso-8859-1")
353 response += self.headers.serialize()
354 response += self.body
355 return response
358def parse_line(
359 read_line: Callable[
360 [int, type[Exception]], Generator[None, None, bytes | bytearray]
361 ],
362 too_long_exc_type: type[Exception] = SecurityError,
363) -> Generator[None, None, bytes | bytearray]:
364 """
365 Parse a single line.
367 CRLF is stripped from the return value.
369 Args:
370 read_line: Generator-based coroutine that reads a LF-terminated line
371 or raises an exception if there isn't enough data.
372 too_long_exc_type: exception to raise if the line is too long;
373 defaults to :exc:`SecurityError`.
375 Raises:
376 EOFError: If the connection is closed without a CRLF.
377 SecurityError: If the response exceeds a security limit.
379 """
380 line = yield from read_line(MAX_LINE_LENGTH, too_long_exc_type)
381 # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5
382 if not line.endswith(b"\r\n"):
383 raise EOFError("line without CRLF")
384 return line[:-2]
387def parse_headers(
388 read_line: Callable[
389 [int, type[Exception]], Generator[None, None, bytes | bytearray]
390 ],
391) -> Generator[None, None, Headers]:
392 """
393 Parse HTTP headers.
395 Headers should contain only ASCII characters; however, non-ASCII values are
396 tolerated and decoded as ISO-8859-1.
398 Args:
399 read_line: Generator-based coroutine that reads a LF-terminated line
400 or raises an exception if there isn't enough data.
402 Raises:
403 EOFError: If the connection is closed without complete headers.
404 HeaderLineTooLong: If a header line is too long.
405 TooManyHeaders: If there are too many headers.
406 ValueError: If the request isn't well formatted.
408 """
409 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
411 # We don't attempt to support obsolete line folding.
413 headers = Headers()
414 for _ in range(MAX_NUM_HEADERS + 1):
415 try:
416 line = yield from parse_line(read_line, HeaderLineTooLong)
417 except EOFError as exc:
418 raise EOFError("connection closed while reading HTTP headers") from exc
419 if line == b"":
420 break
422 try:
423 raw_name, raw_value = line.split(b":", 1)
424 except ValueError: # not enough values to unpack (expected 2, got 1)
425 raise ValueError(f"invalid HTTP header line: {d(line)}") from None
426 if not _token_re.fullmatch(raw_name):
427 raise ValueError(f"invalid HTTP header name: {d(raw_name)}")
428 raw_value = raw_value.strip(b" \t")
429 if not _value_re.fullmatch(raw_value):
430 raise ValueError(f"invalid HTTP header value: {d(raw_value)}")
432 name = raw_name.decode("ascii") # guaranteed to be ASCII at this point
433 # Headers should be ASCII. Section 5.5 of RFC 9110 says: "Historically,
434 # HTTP allowed field content with text in the ISO-8859-1 charset." and
435 # "A recipient SHOULD treat other allowed octets in field content (i.e.,
436 # obs-text) as opaque data." ISO-8859-1 is an opaque representation of
437 # arbitrary binary data in a str object and it is easy to reverse.
438 value = raw_value.decode("iso-8859-1")
440 # Since we just validated raw_value, we don't need to revalidate it.
441 headers.set_insecure(name, value)
443 else:
444 raise TooManyHeaders(f"expected no more than {MAX_NUM_HEADERS} headers")
446 return headers
449def read_body(
450 status_code: int,
451 headers: Headers,
452 read_line: Callable[
453 [int, type[Exception]], Generator[None, None, bytes | bytearray]
454 ],
455 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
456 read_to_eof: Callable[
457 [int, type[Exception]], Generator[None, None, bytes | bytearray]
458 ],
459) -> Generator[None, None, bytes | bytearray]:
460 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
462 # Since websockets only does GET requests (no HEAD, no CONNECT), all
463 # responses except 1xx, 204, and 304 include a message body.
464 if 100 <= status_code < 200 or status_code == 204 or status_code == 304:
465 return b""
467 # MultipleValuesError is sufficiently unlikely that we don't attempt to
468 # handle it when accessing headers. Instead we document that its parent
469 # class, LookupError, may be raised.
470 # Conversions from str to int are protected by sys.set_int_max_str_digits.
472 elif (coding := headers.get("Transfer-Encoding")) is not None:
473 if coding != "chunked":
474 raise NotImplementedError(f"transfer coding {coding} isn't supported")
476 body = b""
477 while True:
478 chunk_size_line = yield from parse_line(read_line, SecurityError)
479 raw_chunk_size = chunk_size_line.split(b";", 1)[0]
480 if not _hexdigits_re.fullmatch(raw_chunk_size):
481 raise SecurityError(f"invalid chunk size: {d(raw_chunk_size)}")
482 chunk_size = int(raw_chunk_size, 16)
483 if chunk_size == 0:
484 break
485 if len(body) + chunk_size > MAX_BODY_SIZE:
486 raise SecurityError(
487 f"chunk too large: {chunk_size} bytes after {len(body)} bytes"
488 )
489 body += yield from read_exact(chunk_size)
490 if (yield from read_exact(2)) != b"\r\n":
491 raise ValueError("chunk without CRLF")
492 # Read the trailer.
493 yield from parse_headers(read_line)
494 return body
496 elif (raw_content_length := headers.get("Content-Length")) is not None:
497 if not _digits_re.fullmatch(raw_content_length.encode("iso-8859-1")):
498 raise SecurityError(f"invalid content length: {raw_content_length}")
499 content_length = int(raw_content_length)
500 if content_length > MAX_BODY_SIZE:
501 raise SecurityError(f"body too large: {content_length} bytes")
502 return (yield from read_exact(content_length))
504 else:
505 return (yield from read_to_eof(MAX_BODY_SIZE, SecurityError))