Files
kefu/wechat_rpa/.venv/Lib/site-packages/websockets/http11.py
T
2026-09-21 10:34:06 +08:00

494 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import dataclasses
import os
import re
import sys
import warnings
from collections.abc import Generator
from typing import Callable
from .datastructures import Headers
from .exceptions import (
HeaderLineTooLong,
RequestLineTooLong,
SecurityError,
StatusLineTooLong,
TooManyHeaders,
)
from .version import version as websockets_version
__all__ = [
"SERVER",
"USER_AGENT",
"Request",
"Response",
]
PYTHON_VERSION = "{}.{}".format(*sys.version_info)
# User-Agent header for HTTP requests.
USER_AGENT = os.environ.get(
"WEBSOCKETS_USER_AGENT",
f"Python/{PYTHON_VERSION} websockets/{websockets_version}",
)
# Server header for HTTP responses.
SERVER = os.environ.get(
"WEBSOCKETS_SERVER",
f"Python/{PYTHON_VERSION} websockets/{websockets_version}",
)
# Maximum total size of headers is around 128 * 8 KiB = 1 MiB.
MAX_NUM_HEADERS = int(os.environ.get("WEBSOCKETS_MAX_NUM_HEADERS", "128"))
# Limit request line and header lines. 8KiB is the most common default
# configuration of popular HTTP servers.
MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", "8192"))
# Support for HTTP response bodies is intended to read an error message
# returned by a server. It isn't designed to perform large file transfers.
MAX_BODY_SIZE = int(os.environ.get("WEBSOCKETS_MAX_BODY_SIZE", "1_048_576")) # 1 MiB
def d(value: bytes | bytearray) -> str:
"""
Decode a bytestring for interpolating into an error message.
"""
return value.decode(errors="backslashreplace")
# See https://datatracker.ietf.org/doc/html/rfc7230#appendix-B.
# Regex for validating header names.
_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+")
# Regex for validating header values.
# We don't attempt to support obsolete line folding.
# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff).
# The ABNF is complicated because it attempts to express that optional
# whitespace is ignored. We strip whitespace and don't revalidate that.
# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*")
@dataclasses.dataclass
class Request:
"""
WebSocket handshake request.
``method`` and ``path`` must contain only ASCII characters. ``headers``
should contain only ASCII characters; however, non-ASCII header values are
tolerated and encoded as ISO-8859-1.
Attributes:
path: Request path, including optional query.
headers: Request headers.
method: Request method; WebSocket handshake requests use GET.
protocol: Request protocol; WebSocket handshake requests use HTTP/1.1.
"""
path: str
headers: Headers
# method and protocol have a default value, so they're declared after path
# and headers which don't.
method: str = "GET"
protocol: str = "HTTP/1.1"
# body isn't useful is the context of this library.
_exception: Exception | None = None
@property
def exception(self) -> Exception | None: # pragma: no cover
warnings.warn( # deprecated in 10.3 - 2022-04-17
"Request.exception is deprecated; use ServerProtocol.handshake_exc instead",
DeprecationWarning,
)
return self._exception
@classmethod
def parse(
cls,
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
) -> Generator[None, None, Request]:
"""
Parse a WebSocket handshake request.
This is a generator-based coroutine.
The request method and path must contain only ASCII characters. The
request path isn't URL-decoded or validated in any way. Request headers
should contain only ASCII characters; however, non-ASCII header values
are tolerated and decoded with ISO-8859-1.
:meth:`parse` doesn't read the request body because WebSocket handshake
requests don't have one. If the request contains a body, it may be read
from the data stream after :meth:`parse` returns.
Args:
read_line: Generator-based coroutine that reads a LF-terminated
line or raises an exception if there isn't enough data
Raises:
EOFError: If the connection is closed without a full HTTP request.
RequestLineTooLong: If the request line is too long.
HeaderLineTooLong: If a header line is too long.
TooManyHeaders: If there are too many headers.
UnicodeDecodeError: If the request method or path isn't ASCII.
ValueError: If the request isn't well formatted.
"""
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
# Parsing is simple because a fixed value is expected for the version
# and because path isn't checked. Since WebSocket libraries generally
# implement HTTP/1.1 strictly, there's little need for lenient parsing.
try:
request_line = yield from parse_line(read_line, RequestLineTooLong)
except EOFError as exc:
raise EOFError("connection closed while reading HTTP request line") from exc
try:
raw_method, raw_path, raw_protocol = request_line.split(b" ", 2)
except ValueError: # not enough values to unpack (expected 3, got 1-2)
raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None
if raw_protocol not in [b"HTTP/1.1", b"HTTP/1.0"]:
raise ValueError(
f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "
f"{d(request_line)}"
)
method = raw_method.decode("ascii")
protocol = raw_protocol.decode("ascii")
# RFC 9110 defers the definition of URIs to RFC 3986, which allows only
# a subset of ASCII. Non-ASCII IRIs must be UTF-8 then percent-encoded.
path = raw_path.decode("ascii")
headers = yield from parse_headers(read_line)
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
if "Transfer-Encoding" in headers:
raise NotImplementedError("transfer codings aren't supported")
if "Content-Length" in headers:
# Some devices send a Content-Length header with a value of 0.
# This raises ValueError if Content-Length isn't an integer too.
if int(headers["Content-Length"]) != 0:
raise ValueError("unsupported request body")
return cls(path, headers, method, protocol)
def serialize(self) -> bytes:
"""
Serialize a WebSocket handshake request.
"""
# Methods are hardcoded and always ASCII. Non-ASCII paths are converted
# from URI to IRI and percent-encoded. Enforce ASCII as a safety net.
request_line = f"{self.method} {self.path} {self.protocol}\r\n"
request = request_line.encode("ascii")
request += self.headers.serialize()
return request
@dataclasses.dataclass
class Response:
"""
WebSocket handshake response.
``reason_phrase`` and ``headers`` should contain only ASCII characters;
however, non-ASCII reason phrases and header values are tolerated and
encoded as ISO-8859-1.
Attributes:
status_code: Response code.
reason_phrase: Response reason.
headers: Response headers.
body: Response body.
"""
status_code: int
reason_phrase: str
headers: Headers
body: bytes | bytearray = b""
_exception: Exception | None = None
@property
def exception(self) -> Exception | None: # pragma: no cover
warnings.warn( # deprecated in 10.3 - 2022-04-17
"Response.exception is deprecated; "
"use ClientProtocol.handshake_exc instead",
DeprecationWarning,
)
return self._exception
@classmethod
def parse(
cls,
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
read_to_eof: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
proxy: bool = False,
) -> Generator[None, None, Response]:
"""
Parse a WebSocket handshake response.
This is a generator-based coroutine.
The reason phrase and headers should contain only ASCII characters;
however, non-ASCII reason phrases and header values are tolerated and
decoded as ISO-8859-1.
Args:
read_line: Generator-based coroutine that reads a LF-terminated
line or raises an exception if there isn't enough data.
read_exact: Generator-based coroutine that reads the requested
bytes or raises an exception if there isn't enough data.
read_to_eof: Generator-based coroutine that reads until the end
of the stream.
Raises:
EOFError: If the connection is closed without a full HTTP response.
StatusLineTooLong: If the status line is too long.
HeaderLineTooLong: If a header line is too long.
TooManyHeaders: If there are too many headers.
SecurityError: If the response body exceeds a security limit.
LookupError: If the response isn't well formatted.
ValueError: If the response isn't well formatted.
"""
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
try:
status_line = yield from parse_line(read_line, StatusLineTooLong)
except EOFError as exc:
raise EOFError("connection closed while reading HTTP status line") from exc
try:
protocol, raw_status_code, raw_reason = status_line.split(b" ", 2)
except ValueError: # not enough values to unpack (expected 3, got 1-2)
raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None
if proxy: # some proxies still use HTTP/1.0
if protocol not in [b"HTTP/1.1", b"HTTP/1.0"]:
raise ValueError(
f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "
f"{d(status_line)}"
)
else:
if protocol != b"HTTP/1.1":
raise ValueError(
f"unsupported protocol; expected HTTP/1.1: {d(status_line)}"
)
try:
status_code = int(raw_status_code)
except ValueError: # invalid literal for int() with base 10
raise ValueError(
f"invalid status code; expected integer; got {d(raw_status_code)}"
) from None
if not 100 <= status_code < 600:
raise ValueError(
f"invalid status code; expected 100–599; got {d(raw_status_code)}"
)
if not _value_re.fullmatch(raw_reason):
raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}")
# RFC 2616 implies ISO-8859-1. It's easy to reverse and cannot crash.
# Non-ASCII never worked reliably and the reason isn't useful anyway.
reason = raw_reason.decode("iso-8859-1")
headers = yield from parse_headers(read_line)
body: bytes | bytearray
if proxy:
body = b""
else:
body = yield from read_body(
status_code, headers, read_line, read_exact, read_to_eof
)
return cls(status_code, reason, headers, body)
def serialize(self) -> bytes:
"""
Serialize a WebSocket handshake response.
"""
# Encode the reason phrase as ISO-8859-1 to round-trip cleanly.
status_line = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n"
response = status_line.encode("iso-8859-1")
response += self.headers.serialize()
response += self.body
return response
def parse_line(
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
too_long_exc_type: type[Exception] = SecurityError,
) -> Generator[None, None, bytes | bytearray]:
"""
Parse a single line.
CRLF is stripped from the return value.
Args:
read_line: Generator-based coroutine that reads a LF-terminated line
or raises an exception if there isn't enough data.
too_long_exc_type: exception to raise if the line is too long;
defaults to :exc:`SecurityError`.
Raises:
EOFError: If the connection is closed without a CRLF.
SecurityError: If the response exceeds a security limit.
"""
line = yield from read_line(MAX_LINE_LENGTH, too_long_exc_type)
# Not mandatory but safe - https://datatracker.ietf.org/doc/html/rfc7230#section-3.5
if not line.endswith(b"\r\n"):
raise EOFError("line without CRLF")
return line[:-2]
def parse_headers(
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
) -> Generator[None, None, Headers]:
"""
Parse HTTP headers.
Headers should contain only ASCII characters; however, non-ASCII values are
tolerated and decoded as ISO-8859-1.
Args:
read_line: Generator-based coroutine that reads a LF-terminated line
or raises an exception if there isn't enough data.
Raises:
EOFError: If the connection is closed without complete headers.
HeaderLineTooLong: If a header line is too long.
TooManyHeaders: If there are too many headers.
ValueError: If the request isn't well formatted.
"""
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
# We don't attempt to support obsolete line folding.
headers = Headers()
for _ in range(MAX_NUM_HEADERS + 1):
try:
line = yield from parse_line(read_line, HeaderLineTooLong)
except EOFError as exc:
raise EOFError("connection closed while reading HTTP headers") from exc
if line == b"":
break
try:
raw_name, raw_value = line.split(b":", 1)
except ValueError: # not enough values to unpack (expected 2, got 1)
raise ValueError(f"invalid HTTP header line: {d(line)}") from None
if not _token_re.fullmatch(raw_name):
raise ValueError(f"invalid HTTP header name: {d(raw_name)}")
raw_value = raw_value.strip(b" \t")
if not _value_re.fullmatch(raw_value):
raise ValueError(f"invalid HTTP header value: {d(raw_value)}")
name = raw_name.decode("ascii") # guaranteed to be ASCII at this point
# Headers should be ASCII. Section 5.5 of RFC 9110 says: "Historically,
# HTTP allowed field content with text in the ISO-8859-1 charset." and
# "A recipient SHOULD treat other allowed octets in field content (i.e.,
# obs-text) as opaque data." ISO-8859-1 is an opaque representation of
# arbitrary binary data in a str object and it is easy to reverse.
value = raw_value.decode("iso-8859-1")
# Since we just validated raw_value, we don't need to revalidate it.
headers.set_insecure(name, value)
else:
raise TooManyHeaders(f"expected no more than {MAX_NUM_HEADERS} headers")
return headers
def read_body(
status_code: int,
headers: Headers,
read_line: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
read_to_eof: Callable[
[int, type[Exception]], Generator[None, None, bytes | bytearray]
],
) -> Generator[None, None, bytes | bytearray]:
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
# Since websockets only does GET requests (no HEAD, no CONNECT), all
# responses except 1xx, 204, and 304 include a message body.
if 100 <= status_code < 200 or status_code == 204 or status_code == 304:
return b""
# MultipleValuesError is sufficiently unlikely that we don't attempt to
# handle it when accessing headers. Instead we document that its parent
# class, LookupError, may be raised.
# Conversions from str to int are protected by sys.set_int_max_str_digits..
elif (coding := headers.get("Transfer-Encoding")) is not None:
if coding != "chunked":
raise NotImplementedError(f"transfer coding {coding} isn't supported")
body = b""
while True:
chunk_size_line = yield from parse_line(read_line, SecurityError)
raw_chunk_size = chunk_size_line.split(b";", 1)[0]
# Set a lower limit than default_max_str_digits; 1 EB is plenty.
if len(raw_chunk_size) > 15:
str_chunk_size = raw_chunk_size.decode(errors="backslashreplace")
raise SecurityError(f"chunk too large: 0x{str_chunk_size} bytes")
chunk_size = int(raw_chunk_size, 16)
if chunk_size == 0:
break
if len(body) + chunk_size > MAX_BODY_SIZE:
raise SecurityError(
f"chunk too large: {chunk_size} bytes after {len(body)} bytes"
)
body += yield from read_exact(chunk_size)
if (yield from read_exact(2)) != b"\r\n":
raise ValueError("chunk without CRLF")
# Read the trailer.
yield from parse_headers(read_line)
return body
elif (raw_content_length := headers.get("Content-Length")) is not None:
# Set a lower limit than default_max_str_digits; 1 EiB is plenty.
if len(raw_content_length) > 18:
raise SecurityError(f"body too large: {raw_content_length} bytes")
content_length = int(raw_content_length)
if content_length > MAX_BODY_SIZE:
raise SecurityError(f"body too large: {content_length} bytes")
return (yield from read_exact(content_length))
else:
return (yield from read_to_eof(MAX_BODY_SIZE, SecurityError))