Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websockets/http11.py: 75%
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]*")
84@dataclasses.dataclass
85class Request:
86 """
87 WebSocket handshake request.
89 ``method`` and ``path`` must contain only ASCII characters. ``headers``
90 should contain only ASCII characters; however, non-ASCII header values are
91 tolerated and encoded as ISO-8859-1.
93 Attributes:
94 path: Request path, including optional query.
95 headers: Request headers.
96 method: Request method; WebSocket handshake requests use GET.
97 protocol: Request protocol; WebSocket handshake requests use HTTP/1.1.
98 """
100 path: str
101 headers: Headers
102 # method and protocol have a default value, so they're declared after path
103 # and headers which don't.
104 method: str = "GET"
105 protocol: str = "HTTP/1.1"
106 # body isn't useful is the context of this library.
108 _exception: Exception | None = None
110 @property
111 def exception(self) -> Exception | None: # pragma: no cover
112 warnings.warn( # deprecated in 10.3 - 2022-04-17
113 "Request.exception is deprecated; use ServerProtocol.handshake_exc instead",
114 DeprecationWarning,
115 )
116 return self._exception
118 @classmethod
119 def parse(
120 cls,
121 read_line: Callable[
122 [int, type[Exception]], Generator[None, None, bytes | bytearray]
123 ],
124 ) -> Generator[None, None, Request]:
125 """
126 Parse a WebSocket handshake request.
128 This is a generator-based coroutine.
130 The request method and path must contain only ASCII characters. The
131 request path isn't URL-decoded or validated in any way. Request headers
132 should contain only ASCII characters; however, non-ASCII header values
133 are tolerated and decoded with ISO-8859-1.
135 :meth:`parse` doesn't read the request body because WebSocket handshake
136 requests don't have one. If the request contains a body, it may be read
137 from the data stream after :meth:`parse` returns.
139 Args:
140 read_line: Generator-based coroutine that reads a LF-terminated
141 line or raises an exception if there isn't enough data
143 Raises:
144 EOFError: If the connection is closed without a full HTTP request.
145 RequestLineTooLong: If the request line is too long.
146 HeaderLineTooLong: If a header line is too long.
147 TooManyHeaders: If there are too many headers.
148 UnicodeDecodeError: If the request method or path isn't ASCII.
149 ValueError: If the request isn't well formatted.
151 """
152 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
154 # Parsing is simple because a fixed value is expected for the version
155 # and because path isn't checked. Since WebSocket libraries generally
156 # implement HTTP/1.1 strictly, there's little need for lenient parsing.
158 try:
159 request_line = yield from parse_line(read_line, RequestLineTooLong)
160 except EOFError as exc:
161 raise EOFError("connection closed while reading HTTP request line") from exc
163 try:
164 raw_method, raw_path, raw_protocol = request_line.split(b" ", 2)
165 except ValueError: # not enough values to unpack (expected 3, got 1-2)
166 raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None
167 if raw_protocol not in [b"HTTP/1.1", b"HTTP/1.0"]:
168 raise ValueError(
169 f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "
170 f"{d(request_line)}"
171 )
172 method = raw_method.decode("ascii")
173 protocol = raw_protocol.decode("ascii")
175 # RFC 9110 defers the definition of URIs to RFC 3986, which allows only
176 # a subset of ASCII. Non-ASCII IRIs must be UTF-8 then percent-encoded.
177 path = raw_path.decode("ascii")
179 headers = yield from parse_headers(read_line)
181 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
183 if "Transfer-Encoding" in headers:
184 raise NotImplementedError("transfer codings aren't supported")
186 if "Content-Length" in headers:
187 # Some devices send a Content-Length header with a value of 0.
188 # This raises ValueError if Content-Length isn't an integer too.
189 if int(headers["Content-Length"]) != 0:
190 raise ValueError("unsupported request body")
192 return cls(path, headers, method, protocol)
194 def serialize(self) -> bytes:
195 """
196 Serialize a WebSocket handshake request.
198 """
199 # Methods are hardcoded and always ASCII. Non-ASCII paths are converted
200 # from URI to IRI and percent-encoded. Enforce ASCII as a safety net.
201 request_line = f"{self.method} {self.path} {self.protocol}\r\n"
202 request = request_line.encode("ascii")
203 request += self.headers.serialize()
204 return request
207@dataclasses.dataclass
208class Response:
209 """
210 WebSocket handshake response.
212 ``reason_phrase`` and ``headers`` should contain only ASCII characters;
213 however, non-ASCII reason phrases and header values are tolerated and
214 encoded as ISO-8859-1.
216 Attributes:
217 status_code: Response code.
218 reason_phrase: Response reason.
219 headers: Response headers.
220 body: Response body.
222 """
224 status_code: int
225 reason_phrase: str
226 headers: Headers
227 body: bytes | bytearray = b""
229 _exception: Exception | None = None
231 @property
232 def exception(self) -> Exception | None: # pragma: no cover
233 warnings.warn( # deprecated in 10.3 - 2022-04-17
234 "Response.exception is deprecated; "
235 "use ClientProtocol.handshake_exc instead",
236 DeprecationWarning,
237 )
238 return self._exception
240 @classmethod
241 def parse(
242 cls,
243 read_line: Callable[
244 [int, type[Exception]], Generator[None, None, bytes | bytearray]
245 ],
246 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
247 read_to_eof: Callable[
248 [int, type[Exception]], Generator[None, None, bytes | bytearray]
249 ],
250 proxy: bool = False,
251 ) -> Generator[None, None, Response]:
252 """
253 Parse a WebSocket handshake response.
255 This is a generator-based coroutine.
257 The reason phrase and headers should contain only ASCII characters;
258 however, non-ASCII reason phrases and header values are tolerated and
259 decoded as ISO-8859-1.
261 Args:
262 read_line: Generator-based coroutine that reads a LF-terminated
263 line or raises an exception if there isn't enough data.
264 read_exact: Generator-based coroutine that reads the requested
265 bytes or raises an exception if there isn't enough data.
266 read_to_eof: Generator-based coroutine that reads until the end
267 of the stream.
269 Raises:
270 EOFError: If the connection is closed without a full HTTP response.
271 StatusLineTooLong: If the status line is too long.
272 HeaderLineTooLong: If a header line is too long.
273 TooManyHeaders: If there are too many headers.
274 SecurityError: If the response body exceeds a security limit.
275 LookupError: If the response isn't well formatted.
276 ValueError: If the response isn't well formatted.
278 """
279 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
281 try:
282 status_line = yield from parse_line(read_line, StatusLineTooLong)
283 except EOFError as exc:
284 raise EOFError("connection closed while reading HTTP status line") from exc
286 try:
287 protocol, raw_status_code, raw_reason = status_line.split(b" ", 2)
288 except ValueError: # not enough values to unpack (expected 3, got 1-2)
289 raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None
290 if proxy: # some proxies still use HTTP/1.0
291 if protocol not in [b"HTTP/1.1", b"HTTP/1.0"]:
292 raise ValueError(
293 f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "
294 f"{d(status_line)}"
295 )
296 else:
297 if protocol != b"HTTP/1.1":
298 raise ValueError(
299 f"unsupported protocol; expected HTTP/1.1: {d(status_line)}"
300 )
301 try:
302 status_code = int(raw_status_code)
303 except ValueError: # invalid literal for int() with base 10
304 raise ValueError(
305 f"invalid status code; expected integer; got {d(raw_status_code)}"
306 ) from None
307 if not 100 <= status_code < 600:
308 raise ValueError(
309 f"invalid status code; expected 100–599; got {d(raw_status_code)}"
310 )
311 if not _value_re.fullmatch(raw_reason):
312 raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}")
314 # RFC 2616 implies ISO-8859-1. It's easy to reverse and cannot crash.
315 # Non-ASCII never worked reliably and the reason isn't useful anyway.
316 reason = raw_reason.decode("iso-8859-1")
318 headers = yield from parse_headers(read_line)
320 body: bytes | bytearray
321 if proxy:
322 body = b""
323 else:
324 body = yield from read_body(
325 status_code, headers, read_line, read_exact, read_to_eof
326 )
328 return cls(status_code, reason, headers, body)
330 def serialize(self) -> bytes:
331 """
332 Serialize a WebSocket handshake response.
334 """
335 # Encode the reason phrase as ISO-8859-1 to round-trip cleanly.
336 status_line = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n"
337 response = status_line.encode("iso-8859-1")
338 response += self.headers.serialize()
339 response += self.body
340 return response
343def parse_line(
344 read_line: Callable[
345 [int, type[Exception]], Generator[None, None, bytes | bytearray]
346 ],
347 too_long_exc_type: type[Exception] = SecurityError,
348) -> Generator[None, None, bytes | bytearray]:
349 """
350 Parse a single line.
352 CRLF is stripped from the return value.
354 Args:
355 read_line: Generator-based coroutine that reads a LF-terminated line
356 or raises an exception if there isn't enough data.
357 too_long_exc_type: exception to raise if the line is too long;
358 defaults to :exc:`SecurityError`.
360 Raises:
361 EOFError: If the connection is closed without a CRLF.
362 SecurityError: If the response exceeds a security limit.
364 """
365 line = yield from read_line(MAX_LINE_LENGTH, too_long_exc_type)
366 # Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5
367 if not line.endswith(b"\r\n"):
368 raise EOFError("line without CRLF")
369 return line[:-2]
372def parse_headers(
373 read_line: Callable[
374 [int, type[Exception]], Generator[None, None, bytes | bytearray]
375 ],
376) -> Generator[None, None, Headers]:
377 """
378 Parse HTTP headers.
380 Headers should contain only ASCII characters; however, non-ASCII values are
381 tolerated and decoded as ISO-8859-1.
383 Args:
384 read_line: Generator-based coroutine that reads a LF-terminated line
385 or raises an exception if there isn't enough data.
387 Raises:
388 EOFError: If the connection is closed without complete headers.
389 HeaderLineTooLong: If a header line is too long.
390 TooManyHeaders: If there are too many headers.
391 ValueError: If the request isn't well formatted.
393 """
394 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
396 # We don't attempt to support obsolete line folding.
398 headers = Headers()
399 for _ in range(MAX_NUM_HEADERS + 1):
400 try:
401 line = yield from parse_line(read_line, HeaderLineTooLong)
402 except EOFError as exc:
403 raise EOFError("connection closed while reading HTTP headers") from exc
404 if line == b"":
405 break
407 try:
408 raw_name, raw_value = line.split(b":", 1)
409 except ValueError: # not enough values to unpack (expected 2, got 1)
410 raise ValueError(f"invalid HTTP header line: {d(line)}") from None
411 if not _token_re.fullmatch(raw_name):
412 raise ValueError(f"invalid HTTP header name: {d(raw_name)}")
413 raw_value = raw_value.strip(b" \t")
414 if not _value_re.fullmatch(raw_value):
415 raise ValueError(f"invalid HTTP header value: {d(raw_value)}")
417 name = raw_name.decode("ascii") # guaranteed to be ASCII at this point
418 # Headers should be ASCII. Section 5.5 of RFC 9110 says: "Historically,
419 # HTTP allowed field content with text in the ISO-8859-1 charset." and
420 # "A recipient SHOULD treat other allowed octets in field content (i.e.,
421 # obs-text) as opaque data." ISO-8859-1 is an opaque representation of
422 # arbitrary binary data in a str object and it is easy to reverse.
423 value = raw_value.decode("iso-8859-1")
425 # Since we just validated raw_value, we don't need to revalidate it.
426 headers.set_insecure(name, value)
428 else:
429 raise TooManyHeaders(f"expected no more than {MAX_NUM_HEADERS} headers")
431 return headers
434def read_body(
435 status_code: int,
436 headers: Headers,
437 read_line: Callable[
438 [int, type[Exception]], Generator[None, None, bytes | bytearray]
439 ],
440 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
441 read_to_eof: Callable[
442 [int, type[Exception]], Generator[None, None, bytes | bytearray]
443 ],
444) -> Generator[None, None, bytes | bytearray]:
445 # https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
447 # Since websockets only does GET requests (no HEAD, no CONNECT), all
448 # responses except 1xx, 204, and 304 include a message body.
449 if 100 <= status_code < 200 or status_code == 204 or status_code == 304:
450 return b""
452 # MultipleValuesError is sufficiently unlikely that we don't attempt to
453 # handle it when accessing headers. Instead we document that its parent
454 # class, LookupError, may be raised.
455 # Conversions from str to int are protected by sys.set_int_max_str_digits..
457 elif (coding := headers.get("Transfer-Encoding")) is not None:
458 if coding != "chunked":
459 raise NotImplementedError(f"transfer coding {coding} isn't supported")
461 body = b""
462 while True:
463 chunk_size_line = yield from parse_line(read_line, SecurityError)
464 raw_chunk_size = chunk_size_line.split(b";", 1)[0]
465 # Set a lower limit than default_max_str_digits; 1 EB is plenty.
466 if len(raw_chunk_size) > 15:
467 str_chunk_size = raw_chunk_size.decode(errors="backslashreplace")
468 raise SecurityError(f"chunk too large: 0x{str_chunk_size} bytes")
469 chunk_size = int(raw_chunk_size, 16)
470 if chunk_size == 0:
471 break
472 if len(body) + chunk_size > MAX_BODY_SIZE:
473 raise SecurityError(
474 f"chunk too large: {chunk_size} bytes after {len(body)} bytes"
475 )
476 body += yield from read_exact(chunk_size)
477 if (yield from read_exact(2)) != b"\r\n":
478 raise ValueError("chunk without CRLF")
479 # Read the trailer.
480 yield from parse_headers(read_line)
481 return body
483 elif (raw_content_length := headers.get("Content-Length")) is not None:
484 # Set a lower limit than default_max_str_digits; 1 EiB is plenty.
485 if len(raw_content_length) > 18:
486 raise SecurityError(f"body too large: {raw_content_length} bytes")
487 content_length = int(raw_content_length)
488 if content_length > MAX_BODY_SIZE:
489 raise SecurityError(f"body too large: {content_length} bytes")
490 return (yield from read_exact(content_length))
492 else:
493 return (yield from read_to_eof(MAX_BODY_SIZE, SecurityError))