Загрузить файлы в «venv/Lib/site-packages/websockets»
This commit is contained in:
20
venv/Lib/site-packages/websockets/http.py
Normal file
20
venv/Lib/site-packages/websockets/http.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from .datastructures import Headers, MultipleValuesError # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
# Suppress redundant DeprecationWarning raised by websockets.legacy.
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
from .legacy.http import read_request, read_response # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
warnings.warn( # deprecated in 9.0 - 2021-09-01
|
||||||
|
"Headers and MultipleValuesError were moved "
|
||||||
|
"from websockets.http to websockets.datastructures"
|
||||||
|
"and read_request and read_response were moved "
|
||||||
|
"from websockets.http to websockets.legacy.http",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
438
venv/Lib/site-packages/websockets/http11.py
Normal file
438
venv/Lib/site-packages/websockets/http11.py
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
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 SecurityError
|
||||||
|
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.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
path: Request path, including optional query.
|
||||||
|
headers: Request headers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
headers: Headers
|
||||||
|
# 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], Generator[None, None, bytes | bytearray]],
|
||||||
|
) -> Generator[None, None, Request]:
|
||||||
|
"""
|
||||||
|
Parse a WebSocket handshake request.
|
||||||
|
|
||||||
|
This is a generator-based coroutine.
|
||||||
|
|
||||||
|
The request path isn't URL-decoded or validated in any way.
|
||||||
|
|
||||||
|
The request path and headers are expected to contain only ASCII
|
||||||
|
characters. Other characters are represented with surrogate escapes.
|
||||||
|
|
||||||
|
:meth:`parse` doesn't attempt to 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.
|
||||||
|
SecurityError: If the request exceeds a security limit.
|
||||||
|
ValueError: If the request isn't well formatted.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1
|
||||||
|
|
||||||
|
# Parsing is simple because fixed values are expected for method and
|
||||||
|
# version and because path isn't checked. Since WebSocket software tends
|
||||||
|
# to implement HTTP/1.1 strictly, there's little need for lenient parsing.
|
||||||
|
|
||||||
|
try:
|
||||||
|
request_line = yield from parse_line(read_line)
|
||||||
|
except EOFError as exc:
|
||||||
|
raise EOFError("connection closed while reading HTTP request line") from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
method, raw_path, 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 protocol != b"HTTP/1.1":
|
||||||
|
raise ValueError(
|
||||||
|
f"unsupported protocol; expected HTTP/1.1: {d(request_line)}"
|
||||||
|
)
|
||||||
|
if method != b"GET":
|
||||||
|
raise ValueError(f"unsupported HTTP method; expected GET; got {d(method)}")
|
||||||
|
path = raw_path.decode("ascii", "surrogateescape")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def serialize(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Serialize a WebSocket handshake request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Since the request line and headers only contain ASCII characters,
|
||||||
|
# we can keep this simple.
|
||||||
|
request = f"GET {self.path} HTTP/1.1\r\n".encode()
|
||||||
|
request += self.headers.serialize()
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Response:
|
||||||
|
"""
|
||||||
|
WebSocket handshake response.
|
||||||
|
|
||||||
|
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], Generator[None, None, bytes | bytearray]],
|
||||||
|
read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
|
||||||
|
read_to_eof: Callable[[int], 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 are expected to contain only ASCII
|
||||||
|
characters. Other characters are represented with surrogate escapes.
|
||||||
|
|
||||||
|
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.
|
||||||
|
SecurityError: If the response 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)
|
||||||
|
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)}")
|
||||||
|
reason = raw_reason.decode("ascii", "surrogateescape")
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Since the status line and headers only contain ASCII characters,
|
||||||
|
# we can keep this simple.
|
||||||
|
response = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n".encode()
|
||||||
|
response += self.headers.serialize()
|
||||||
|
response += self.body
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def parse_line(
|
||||||
|
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
|
||||||
|
) -> 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.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
EOFError: If the connection is closed without a CRLF.
|
||||||
|
SecurityError: If the response exceeds a security limit.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
line = yield from read_line(MAX_LINE_LENGTH)
|
||||||
|
except RuntimeError:
|
||||||
|
raise SecurityError("line too long")
|
||||||
|
# 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], Generator[None, None, bytes | bytearray]],
|
||||||
|
) -> Generator[None, None, Headers]:
|
||||||
|
"""
|
||||||
|
Parse HTTP headers.
|
||||||
|
|
||||||
|
Non-ASCII characters are represented with surrogate escapes.
|
||||||
|
|
||||||
|
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.
|
||||||
|
SecurityError: If the request exceeds a security limit.
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
value = raw_value.decode("ascii", "surrogateescape")
|
||||||
|
headers[name] = value
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise SecurityError("too many HTTP headers")
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
def read_body(
|
||||||
|
status_code: int,
|
||||||
|
headers: Headers,
|
||||||
|
read_line: Callable[[int], Generator[None, None, bytes | bytearray]],
|
||||||
|
read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
|
||||||
|
read_to_eof: Callable[[int], 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)
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
return (yield from read_to_eof(MAX_BODY_SIZE))
|
||||||
|
except RuntimeError:
|
||||||
|
raise SecurityError(f"body too large: over {MAX_BODY_SIZE} bytes")
|
||||||
100
venv/Lib/site-packages/websockets/imports.py
Normal file
100
venv/Lib/site-packages/websockets/imports.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["lazy_import"]
|
||||||
|
|
||||||
|
|
||||||
|
def import_name(name: str, source: str, namespace: dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Import ``name`` from ``source`` in ``namespace``.
|
||||||
|
|
||||||
|
There are two use cases:
|
||||||
|
|
||||||
|
- ``name`` is an object defined in ``source``;
|
||||||
|
- ``name`` is a submodule of ``source``.
|
||||||
|
|
||||||
|
Neither :func:`__import__` nor :func:`~importlib.import_module` does
|
||||||
|
exactly this. :func:`__import__` is closer to the intended behavior.
|
||||||
|
|
||||||
|
"""
|
||||||
|
level = 0
|
||||||
|
while source[level] == ".":
|
||||||
|
level += 1
|
||||||
|
assert level < len(source), "importing from parent isn't supported"
|
||||||
|
module = __import__(source[level:], namespace, None, [name], level)
|
||||||
|
return getattr(module, name)
|
||||||
|
|
||||||
|
|
||||||
|
def lazy_import(
|
||||||
|
namespace: dict[str, Any],
|
||||||
|
aliases: dict[str, str] | None = None,
|
||||||
|
deprecated_aliases: dict[str, str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Provide lazy, module-level imports.
|
||||||
|
|
||||||
|
Typical use::
|
||||||
|
|
||||||
|
__getattr__, __dir__ = lazy_import(
|
||||||
|
globals(),
|
||||||
|
aliases={
|
||||||
|
"<name>": "<source module>",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
deprecated_aliases={
|
||||||
|
...,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
This function defines ``__getattr__`` and ``__dir__`` per :pep:`562`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if aliases is None:
|
||||||
|
aliases = {}
|
||||||
|
if deprecated_aliases is None:
|
||||||
|
deprecated_aliases = {}
|
||||||
|
|
||||||
|
namespace_set = set(namespace)
|
||||||
|
aliases_set = set(aliases)
|
||||||
|
deprecated_aliases_set = set(deprecated_aliases)
|
||||||
|
|
||||||
|
assert not namespace_set & aliases_set, "namespace conflict"
|
||||||
|
assert not namespace_set & deprecated_aliases_set, "namespace conflict"
|
||||||
|
assert not aliases_set & deprecated_aliases_set, "namespace conflict"
|
||||||
|
|
||||||
|
package = namespace["__name__"]
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
assert aliases is not None # mypy cannot figure this out
|
||||||
|
try:
|
||||||
|
source = aliases[name]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return import_name(name, source, namespace)
|
||||||
|
|
||||||
|
assert deprecated_aliases is not None # mypy cannot figure this out
|
||||||
|
try:
|
||||||
|
source = deprecated_aliases[name]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
f"{package}.{name} is deprecated",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
return import_name(name, source, namespace)
|
||||||
|
|
||||||
|
raise AttributeError(f"module {package!r} has no attribute {name!r}")
|
||||||
|
|
||||||
|
namespace["__getattr__"] = __getattr__
|
||||||
|
|
||||||
|
def __dir__() -> Iterable[str]:
|
||||||
|
return sorted(namespace_set | aliases_set | deprecated_aliases_set)
|
||||||
|
|
||||||
|
namespace["__dir__"] = __dir__
|
||||||
768
venv/Lib/site-packages/websockets/protocol.py
Normal file
768
venv/Lib/site-packages/websockets/protocol.py
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
from .exceptions import (
|
||||||
|
ConnectionClosed,
|
||||||
|
ConnectionClosedError,
|
||||||
|
ConnectionClosedOK,
|
||||||
|
InvalidState,
|
||||||
|
PayloadTooBig,
|
||||||
|
ProtocolError,
|
||||||
|
)
|
||||||
|
from .extensions import Extension
|
||||||
|
from .frames import (
|
||||||
|
OK_CLOSE_CODES,
|
||||||
|
OP_BINARY,
|
||||||
|
OP_CLOSE,
|
||||||
|
OP_CONT,
|
||||||
|
OP_PING,
|
||||||
|
OP_PONG,
|
||||||
|
OP_TEXT,
|
||||||
|
Close,
|
||||||
|
CloseCode,
|
||||||
|
Frame,
|
||||||
|
)
|
||||||
|
from .http11 import Request, Response
|
||||||
|
from .streams import StreamReader
|
||||||
|
from .typing import BytesLike, LoggerLike, Origin, Subprotocol
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Protocol",
|
||||||
|
"Side",
|
||||||
|
"State",
|
||||||
|
"SEND_EOF",
|
||||||
|
]
|
||||||
|
|
||||||
|
Event = Request | Response | Frame
|
||||||
|
"""Events that :meth:`~Protocol.events_received` may return."""
|
||||||
|
|
||||||
|
|
||||||
|
class Side(enum.IntEnum):
|
||||||
|
"""A WebSocket connection is either a server or a client."""
|
||||||
|
|
||||||
|
SERVER, CLIENT = range(2)
|
||||||
|
|
||||||
|
|
||||||
|
SERVER = Side.SERVER
|
||||||
|
CLIENT = Side.CLIENT
|
||||||
|
|
||||||
|
|
||||||
|
class State(enum.IntEnum):
|
||||||
|
"""A WebSocket connection is in one of these four states."""
|
||||||
|
|
||||||
|
CONNECTING, OPEN, CLOSING, CLOSED = range(4)
|
||||||
|
|
||||||
|
|
||||||
|
CONNECTING = State.CONNECTING
|
||||||
|
OPEN = State.OPEN
|
||||||
|
CLOSING = State.CLOSING
|
||||||
|
CLOSED = State.CLOSED
|
||||||
|
|
||||||
|
|
||||||
|
SEND_EOF = b""
|
||||||
|
"""Sentinel signaling that the TCP connection must be half-closed."""
|
||||||
|
|
||||||
|
|
||||||
|
class Protocol:
|
||||||
|
"""
|
||||||
|
Sans-I/O implementation of a WebSocket connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`.
|
||||||
|
state: Initial state of the WebSocket connection.
|
||||||
|
max_size: Maximum size of incoming messages in bytes.
|
||||||
|
:obj:`None` disables the limit. You may pass a ``(max_message_size,
|
||||||
|
max_fragment_size)`` tuple to set different limits for messages and
|
||||||
|
fragments when you expect long messages sent in short fragments.
|
||||||
|
logger: Logger for this connection; depending on ``side``,
|
||||||
|
defaults to ``logging.getLogger("websockets.client")``
|
||||||
|
or ``logging.getLogger("websockets.server")``;
|
||||||
|
see the :doc:`logging guide <../../topics/logging>` for details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
side: Side,
|
||||||
|
*,
|
||||||
|
state: State = OPEN,
|
||||||
|
max_size: tuple[int | None, int | None] | int | None = 2**20,
|
||||||
|
logger: LoggerLike | None = None,
|
||||||
|
) -> None:
|
||||||
|
# Unique identifier. For logs.
|
||||||
|
self.id: uuid.UUID = uuid.uuid4()
|
||||||
|
"""Unique identifier of the connection. Useful in logs."""
|
||||||
|
|
||||||
|
# Logger or LoggerAdapter for this connection.
|
||||||
|
if logger is None:
|
||||||
|
logger = logging.getLogger(f"websockets.{side.name.lower()}")
|
||||||
|
self.logger: LoggerLike = logger
|
||||||
|
"""Logger for this connection."""
|
||||||
|
|
||||||
|
# Track if DEBUG is enabled. Shortcut logging calls if it isn't.
|
||||||
|
self.debug = logger.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
|
# Connection side. CLIENT or SERVER.
|
||||||
|
self.side = side
|
||||||
|
|
||||||
|
# Connection state. Initially OPEN because subclasses handle CONNECTING.
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
# Maximum size of incoming messages in bytes.
|
||||||
|
if isinstance(max_size, int) or max_size is None:
|
||||||
|
self.max_message_size, self.max_fragment_size = max_size, None
|
||||||
|
else:
|
||||||
|
self.max_message_size, self.max_fragment_size = max_size
|
||||||
|
|
||||||
|
# Current size of incoming message in bytes. Only set while reading a
|
||||||
|
# fragmented message i.e. a data frames with the FIN bit not set.
|
||||||
|
self.current_size: int | None = None
|
||||||
|
|
||||||
|
# True while sending a fragmented message i.e. a data frames with the
|
||||||
|
# FIN bit not set.
|
||||||
|
self.expect_continuation_frame = False
|
||||||
|
|
||||||
|
# WebSocket protocol parameters.
|
||||||
|
self.origin: Origin | None = None
|
||||||
|
self.extensions: list[Extension] = []
|
||||||
|
self.subprotocol: Subprotocol | None = None
|
||||||
|
|
||||||
|
# Close code and reason, set when a close frame is sent or received.
|
||||||
|
self.close_rcvd: Close | None = None
|
||||||
|
self.close_sent: Close | None = None
|
||||||
|
self.close_rcvd_then_sent: bool | None = None
|
||||||
|
|
||||||
|
# Track if an exception happened during the handshake.
|
||||||
|
self.handshake_exc: Exception | None = None
|
||||||
|
"""
|
||||||
|
Exception to raise if the opening handshake failed.
|
||||||
|
|
||||||
|
:obj:`None` if the opening handshake succeeded.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Track if send_eof() was called.
|
||||||
|
self.eof_sent = False
|
||||||
|
|
||||||
|
# Parser state.
|
||||||
|
self.reader = StreamReader()
|
||||||
|
self.events: list[Event] = []
|
||||||
|
self.writes: list[bytes] = []
|
||||||
|
self.parser = self.parse()
|
||||||
|
next(self.parser) # start coroutine
|
||||||
|
self.parser_exc: Exception | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> State:
|
||||||
|
"""
|
||||||
|
State of the WebSocket connection.
|
||||||
|
|
||||||
|
Defined in 4.1_, 4.2_, 7.1.3_, and 7.1.4_ of :rfc:`6455`.
|
||||||
|
|
||||||
|
.. _4.1: https://datatracker.ietf.org/doc/html/rfc6455#section-4.1
|
||||||
|
.. _4.2: https://datatracker.ietf.org/doc/html/rfc6455#section-4.2
|
||||||
|
.. _7.1.3: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.3
|
||||||
|
.. _7.1.4: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, state: State) -> None:
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("= connection is %s", state.name)
|
||||||
|
self._state = state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_code(self) -> int | None:
|
||||||
|
"""
|
||||||
|
WebSocket close code received from the remote endpoint.
|
||||||
|
|
||||||
|
Defined in 7.1.5_ of :rfc:`6455`.
|
||||||
|
|
||||||
|
.. _7.1.5: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
|
||||||
|
|
||||||
|
:obj:`None` if the connection isn't closed yet.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.state is not CLOSED:
|
||||||
|
return None
|
||||||
|
elif self.close_rcvd is None:
|
||||||
|
return CloseCode.ABNORMAL_CLOSURE
|
||||||
|
else:
|
||||||
|
return self.close_rcvd.code
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_reason(self) -> str | None:
|
||||||
|
"""
|
||||||
|
WebSocket close reason received from the remote endpoint.
|
||||||
|
|
||||||
|
Defined in 7.1.6_ of :rfc:`6455`.
|
||||||
|
|
||||||
|
.. _7.1.6: https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6
|
||||||
|
|
||||||
|
:obj:`None` if the connection isn't closed yet.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.state is not CLOSED:
|
||||||
|
return None
|
||||||
|
elif self.close_rcvd is None:
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
return self.close_rcvd.reason
|
||||||
|
|
||||||
|
@property
|
||||||
|
def close_exc(self) -> ConnectionClosed:
|
||||||
|
"""
|
||||||
|
Exception to raise when trying to interact with a closed connection.
|
||||||
|
|
||||||
|
Don't raise this exception while the connection :attr:`state`
|
||||||
|
is :attr:`~websockets.protocol.State.CLOSING`; wait until
|
||||||
|
it's :attr:`~websockets.protocol.State.CLOSED`.
|
||||||
|
|
||||||
|
Indeed, the exception includes the close code and reason, which are
|
||||||
|
known only once the connection is closed.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If the connection isn't closed yet.
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert self.state is CLOSED, "connection isn't closed yet"
|
||||||
|
exc_type: type[ConnectionClosed]
|
||||||
|
if (
|
||||||
|
self.close_rcvd is not None
|
||||||
|
and self.close_sent is not None
|
||||||
|
and self.close_rcvd.code in OK_CLOSE_CODES
|
||||||
|
and self.close_sent.code in OK_CLOSE_CODES
|
||||||
|
):
|
||||||
|
exc_type = ConnectionClosedOK
|
||||||
|
else:
|
||||||
|
exc_type = ConnectionClosedError
|
||||||
|
exc: ConnectionClosed = exc_type(
|
||||||
|
self.close_rcvd,
|
||||||
|
self.close_sent,
|
||||||
|
self.close_rcvd_then_sent,
|
||||||
|
)
|
||||||
|
# Chain to the exception raised in the parser, if any.
|
||||||
|
exc.__cause__ = self.parser_exc
|
||||||
|
return exc
|
||||||
|
|
||||||
|
# Public methods for receiving data.
|
||||||
|
|
||||||
|
def receive_data(self, data: bytes | bytearray) -> None:
|
||||||
|
"""
|
||||||
|
Receive data from the network.
|
||||||
|
|
||||||
|
After calling this method:
|
||||||
|
|
||||||
|
- You must call :meth:`data_to_send` and send this data to the network.
|
||||||
|
- You should call :meth:`events_received` and process resulting events.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
EOFError: If :meth:`receive_eof` was called earlier.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.reader.feed_data(data)
|
||||||
|
next(self.parser)
|
||||||
|
|
||||||
|
def receive_eof(self) -> None:
|
||||||
|
"""
|
||||||
|
Receive the end of the data stream from the network.
|
||||||
|
|
||||||
|
After calling this method:
|
||||||
|
|
||||||
|
- You must call :meth:`data_to_send` and send this data to the network;
|
||||||
|
it will return ``[b""]``, signaling the end of the stream, or ``[]``.
|
||||||
|
- You aren't expected to call :meth:`events_received`; it won't return
|
||||||
|
any new events.
|
||||||
|
|
||||||
|
:meth:`receive_eof` is idempotent.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.reader.eof:
|
||||||
|
return
|
||||||
|
self.reader.feed_eof()
|
||||||
|
next(self.parser)
|
||||||
|
|
||||||
|
# Public methods for sending events.
|
||||||
|
|
||||||
|
def send_continuation(self, data: BytesLike, fin: bool) -> None:
|
||||||
|
"""
|
||||||
|
Send a `Continuation frame`_.
|
||||||
|
|
||||||
|
.. _Continuation frame:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: payload containing the same kind of data
|
||||||
|
as the initial frame.
|
||||||
|
fin: FIN bit; set it to :obj:`True` if this is the last frame
|
||||||
|
of a fragmented message and to :obj:`False` otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProtocolError: If a fragmented message isn't in progress.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.expect_continuation_frame:
|
||||||
|
raise ProtocolError("unexpected continuation frame")
|
||||||
|
if self._state is not OPEN:
|
||||||
|
raise InvalidState(f"connection is {self.state.name.lower()}")
|
||||||
|
self.expect_continuation_frame = not fin
|
||||||
|
self.send_frame(Frame(OP_CONT, data, fin))
|
||||||
|
|
||||||
|
def send_text(self, data: BytesLike, fin: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Send a `Text frame`_.
|
||||||
|
|
||||||
|
.. _Text frame:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: payload containing text encoded with UTF-8.
|
||||||
|
fin: FIN bit; set it to :obj:`False` if this is the first frame of
|
||||||
|
a fragmented message.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProtocolError: If a fragmented message is in progress.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.expect_continuation_frame:
|
||||||
|
raise ProtocolError("expected a continuation frame")
|
||||||
|
if self._state is not OPEN:
|
||||||
|
raise InvalidState(f"connection is {self.state.name.lower()}")
|
||||||
|
self.expect_continuation_frame = not fin
|
||||||
|
self.send_frame(Frame(OP_TEXT, data, fin))
|
||||||
|
|
||||||
|
def send_binary(self, data: BytesLike, fin: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Send a `Binary frame`_.
|
||||||
|
|
||||||
|
.. _Binary frame:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: payload containing arbitrary binary data.
|
||||||
|
fin: FIN bit; set it to :obj:`False` if this is the first frame of
|
||||||
|
a fragmented message.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProtocolError: If a fragmented message is in progress.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.expect_continuation_frame:
|
||||||
|
raise ProtocolError("expected a continuation frame")
|
||||||
|
if self._state is not OPEN:
|
||||||
|
raise InvalidState(f"connection is {self.state.name.lower()}")
|
||||||
|
self.expect_continuation_frame = not fin
|
||||||
|
self.send_frame(Frame(OP_BINARY, data, fin))
|
||||||
|
|
||||||
|
def send_close(self, code: CloseCode | int | None = None, reason: str = "") -> None:
|
||||||
|
"""
|
||||||
|
Send a `Close frame`_.
|
||||||
|
|
||||||
|
.. _Close frame:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
code: close code.
|
||||||
|
reason: close reason.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProtocolError: If the code isn't valid or if a reason is provided
|
||||||
|
without a code.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# While RFC 6455 doesn't rule out sending more than one close Frame,
|
||||||
|
# websockets is conservative in what it sends and doesn't allow that.
|
||||||
|
if self._state is not OPEN:
|
||||||
|
raise InvalidState(f"connection is {self.state.name.lower()}")
|
||||||
|
if code is None:
|
||||||
|
if reason != "":
|
||||||
|
raise ProtocolError("cannot send a reason without a code")
|
||||||
|
close = Close(CloseCode.NO_STATUS_RCVD, "")
|
||||||
|
data = b""
|
||||||
|
else:
|
||||||
|
close = Close(code, reason)
|
||||||
|
data = close.serialize()
|
||||||
|
# 7.1.3. The WebSocket Closing Handshake is Started
|
||||||
|
self.send_frame(Frame(OP_CLOSE, data))
|
||||||
|
# Since the state is OPEN, no close frame was received yet.
|
||||||
|
# As a consequence, self.close_rcvd_then_sent remains None.
|
||||||
|
assert self.close_rcvd is None
|
||||||
|
self.close_sent = close
|
||||||
|
self.state = CLOSING
|
||||||
|
|
||||||
|
def send_ping(self, data: BytesLike) -> None:
|
||||||
|
"""
|
||||||
|
Send a `Ping frame`_.
|
||||||
|
|
||||||
|
.. _Ping frame:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: payload containing arbitrary binary data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# RFC 6455 allows control frames after starting the closing handshake.
|
||||||
|
if self._state is not OPEN and self._state is not CLOSING:
|
||||||
|
raise InvalidState(f"connection is {self.state.name.lower()}")
|
||||||
|
self.send_frame(Frame(OP_PING, data))
|
||||||
|
|
||||||
|
def send_pong(self, data: BytesLike) -> None:
|
||||||
|
"""
|
||||||
|
Send a `Pong frame`_.
|
||||||
|
|
||||||
|
.. _Pong frame:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: payload containing arbitrary binary data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# RFC 6455 allows control frames after starting the closing handshake.
|
||||||
|
if self._state is not OPEN and self._state is not CLOSING:
|
||||||
|
raise InvalidState(f"connection is {self.state.name.lower()}")
|
||||||
|
self.send_frame(Frame(OP_PONG, data))
|
||||||
|
|
||||||
|
def fail(self, code: CloseCode | int, reason: str = "") -> None:
|
||||||
|
"""
|
||||||
|
`Fail the WebSocket connection`_.
|
||||||
|
|
||||||
|
.. _Fail the WebSocket connection:
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
code: close code
|
||||||
|
reason: close reason
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ProtocolError: If the code isn't valid.
|
||||||
|
"""
|
||||||
|
# 7.1.7. Fail the WebSocket Connection
|
||||||
|
|
||||||
|
# Send a close frame when the state is OPEN (a close frame was already
|
||||||
|
# sent if it's CLOSING), except when failing the connection because
|
||||||
|
# of an error reading from or writing to the network.
|
||||||
|
if self.state is OPEN:
|
||||||
|
if code != CloseCode.ABNORMAL_CLOSURE:
|
||||||
|
close = Close(code, reason)
|
||||||
|
data = close.serialize()
|
||||||
|
self.send_frame(Frame(OP_CLOSE, data))
|
||||||
|
self.close_sent = close
|
||||||
|
# If recv_messages() raised an exception upon receiving a close
|
||||||
|
# frame but before echoing it, then close_rcvd is not None even
|
||||||
|
# though the state is OPEN. This happens when the connection is
|
||||||
|
# closed while receiving a fragmented message.
|
||||||
|
if self.close_rcvd is not None:
|
||||||
|
self.close_rcvd_then_sent = True
|
||||||
|
self.state = CLOSING
|
||||||
|
|
||||||
|
# When failing the connection, a server closes the TCP connection
|
||||||
|
# without waiting for the client to complete the handshake, while a
|
||||||
|
# client waits for the server to close the TCP connection, possibly
|
||||||
|
# after sending a close frame that the client will ignore.
|
||||||
|
if self.side is SERVER and not self.eof_sent:
|
||||||
|
self.send_eof()
|
||||||
|
|
||||||
|
# 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue
|
||||||
|
# to attempt to process data(including a responding Close frame) from
|
||||||
|
# the remote endpoint after being instructed to _Fail the WebSocket
|
||||||
|
# Connection_."
|
||||||
|
self.parser = self.discard()
|
||||||
|
next(self.parser) # start coroutine
|
||||||
|
|
||||||
|
# Public method for getting incoming events after receiving data.
|
||||||
|
|
||||||
|
def events_received(self) -> list[Event]:
|
||||||
|
"""
|
||||||
|
Fetch events generated from data received from the network.
|
||||||
|
|
||||||
|
Call this method immediately after any of the ``receive_*()`` methods.
|
||||||
|
|
||||||
|
Process resulting events, likely by passing them to the application.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Events read from the connection.
|
||||||
|
"""
|
||||||
|
events, self.events = self.events, []
|
||||||
|
return events
|
||||||
|
|
||||||
|
# Public method for getting outgoing data after receiving data or sending events.
|
||||||
|
|
||||||
|
def data_to_send(self) -> list[bytes]:
|
||||||
|
"""
|
||||||
|
Obtain data to send to the network.
|
||||||
|
|
||||||
|
Call this method immediately after any of the ``receive_*()``,
|
||||||
|
``send_*()``, or :meth:`fail` methods.
|
||||||
|
|
||||||
|
Write resulting data to the connection.
|
||||||
|
|
||||||
|
The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals
|
||||||
|
the end of the data stream. When you receive it, half-close the TCP
|
||||||
|
connection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data to write to the connection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
writes, self.writes = self.writes, []
|
||||||
|
return writes
|
||||||
|
|
||||||
|
def close_expected(self) -> bool:
|
||||||
|
"""
|
||||||
|
Tell if the TCP connection is expected to close soon.
|
||||||
|
|
||||||
|
Call this method immediately after any of the ``receive_*()``,
|
||||||
|
``send_close()``, or :meth:`fail` methods.
|
||||||
|
|
||||||
|
If it returns :obj:`True`, schedule closing the TCP connection after a
|
||||||
|
short timeout if the other side hasn't already closed it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Whether the TCP connection is expected to close soon.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# During the opening handshake, when our state is CONNECTING, we expect
|
||||||
|
# a TCP close if and only if the hansdake fails. When it does, we start
|
||||||
|
# the TCP closing handshake by sending EOF with send_eof().
|
||||||
|
|
||||||
|
# Once the opening handshake completes successfully, we expect a TCP
|
||||||
|
# close if and only if we sent a close frame, meaning that our state
|
||||||
|
# progressed to CLOSING:
|
||||||
|
|
||||||
|
# * Normal closure: once we send a close frame, we expect a TCP close:
|
||||||
|
# server waits for client to complete the TCP closing handshake;
|
||||||
|
# client waits for server to initiate the TCP closing handshake.
|
||||||
|
|
||||||
|
# * Abnormal closure: we always send a close frame and the same logic
|
||||||
|
# applies, except on EOFError where we don't send a close frame
|
||||||
|
# because we already received the TCP close, so we don't expect it.
|
||||||
|
|
||||||
|
# If our state is CLOSED, we already received a TCP close so we don't
|
||||||
|
# expect it anymore.
|
||||||
|
|
||||||
|
# Micro-optimization: put the most common case first
|
||||||
|
if self.state is OPEN:
|
||||||
|
return False
|
||||||
|
if self.state is CLOSING:
|
||||||
|
return True
|
||||||
|
if self.state is CLOSED:
|
||||||
|
return False
|
||||||
|
assert self.state is CONNECTING
|
||||||
|
return self.eof_sent
|
||||||
|
|
||||||
|
# Private methods for receiving data.
|
||||||
|
|
||||||
|
def parse(self) -> Generator[None]:
|
||||||
|
"""
|
||||||
|
Parse incoming data into frames.
|
||||||
|
|
||||||
|
:meth:`receive_data` and :meth:`receive_eof` run this generator
|
||||||
|
coroutine until it needs more data or reaches EOF.
|
||||||
|
|
||||||
|
:meth:`parse` never raises an exception. Instead, it sets the
|
||||||
|
:attr:`parser_exc` and yields control.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if (yield from self.reader.at_eof()):
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("< EOF")
|
||||||
|
# If the WebSocket connection is closed cleanly, with a
|
||||||
|
# closing handhshake, recv_frame() substitutes parse()
|
||||||
|
# with discard(). This branch is reached only when the
|
||||||
|
# connection isn't closed cleanly.
|
||||||
|
raise EOFError("unexpected end of stream")
|
||||||
|
|
||||||
|
max_size = None
|
||||||
|
|
||||||
|
if self.max_message_size is not None:
|
||||||
|
if self.current_size is None:
|
||||||
|
max_size = self.max_message_size
|
||||||
|
else:
|
||||||
|
max_size = self.max_message_size - self.current_size
|
||||||
|
|
||||||
|
if self.max_fragment_size is not None:
|
||||||
|
if max_size is None:
|
||||||
|
max_size = self.max_fragment_size
|
||||||
|
else:
|
||||||
|
max_size = min(max_size, self.max_fragment_size)
|
||||||
|
|
||||||
|
# During a normal closure, execution ends here on the next
|
||||||
|
# iteration of the loop after receiving a close frame. At
|
||||||
|
# this point, recv_frame() replaced parse() by discard().
|
||||||
|
frame = yield from Frame.parse(
|
||||||
|
self.reader.read_exact,
|
||||||
|
mask=self.side is SERVER,
|
||||||
|
max_size=max_size,
|
||||||
|
extensions=self.extensions,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("< %s", frame)
|
||||||
|
|
||||||
|
self.recv_frame(frame)
|
||||||
|
|
||||||
|
except ProtocolError as exc:
|
||||||
|
self.fail(CloseCode.PROTOCOL_ERROR, str(exc))
|
||||||
|
self.parser_exc = exc
|
||||||
|
|
||||||
|
except EOFError as exc:
|
||||||
|
self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc))
|
||||||
|
self.parser_exc = exc
|
||||||
|
|
||||||
|
except UnicodeDecodeError as exc:
|
||||||
|
self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}")
|
||||||
|
self.parser_exc = exc
|
||||||
|
|
||||||
|
except PayloadTooBig as exc:
|
||||||
|
exc.set_current_size(self.current_size)
|
||||||
|
self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc))
|
||||||
|
self.parser_exc = exc
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
self.logger.error("parser failed", exc_info=True)
|
||||||
|
# Don't include exception details, which may be security-sensitive.
|
||||||
|
self.fail(CloseCode.INTERNAL_ERROR)
|
||||||
|
self.parser_exc = exc
|
||||||
|
|
||||||
|
# During an abnormal closure, execution ends here after catching an
|
||||||
|
# exception. At this point, fail() replaced parse() by discard().
|
||||||
|
yield
|
||||||
|
raise AssertionError("parse() shouldn't step after error")
|
||||||
|
|
||||||
|
def discard(self) -> Generator[None]:
|
||||||
|
"""
|
||||||
|
Discard incoming data.
|
||||||
|
|
||||||
|
This coroutine replaces :meth:`parse`:
|
||||||
|
|
||||||
|
- after receiving a close frame, during a normal closure (1.4);
|
||||||
|
- after sending a close frame, during an abnormal closure (7.1.7).
|
||||||
|
|
||||||
|
"""
|
||||||
|
# After the opening handshake completes, the server closes the TCP
|
||||||
|
# connection in the same circumstances where discard() replaces parse().
|
||||||
|
# The client closes it when it receives EOF from the server or times
|
||||||
|
# out. (The latter case cannot be handled in this Sans-I/O layer.)
|
||||||
|
assert (self.side is SERVER or self.state is CONNECTING) == (self.eof_sent)
|
||||||
|
while not (yield from self.reader.at_eof()):
|
||||||
|
self.reader.discard()
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("< EOF")
|
||||||
|
# A server closes the TCP connection immediately, while a client
|
||||||
|
# waits for the server to close the TCP connection.
|
||||||
|
if self.side is CLIENT and self.state is not CONNECTING:
|
||||||
|
self.send_eof()
|
||||||
|
self.state = CLOSED
|
||||||
|
# If discard() completes normally, execution ends here.
|
||||||
|
yield
|
||||||
|
# Once the reader reaches EOF, its feed_data/eof() methods raise an
|
||||||
|
# error, so our receive_data/eof() methods don't step the generator.
|
||||||
|
raise AssertionError("discard() shouldn't step after EOF")
|
||||||
|
|
||||||
|
def recv_frame(self, frame: Frame) -> None:
|
||||||
|
"""
|
||||||
|
Process an incoming frame.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY:
|
||||||
|
if self.current_size is not None:
|
||||||
|
raise ProtocolError("expected a continuation frame")
|
||||||
|
if not frame.fin:
|
||||||
|
self.current_size = len(frame.data)
|
||||||
|
|
||||||
|
elif frame.opcode is OP_CONT:
|
||||||
|
if self.current_size is None:
|
||||||
|
raise ProtocolError("unexpected continuation frame")
|
||||||
|
if frame.fin:
|
||||||
|
self.current_size = None
|
||||||
|
else:
|
||||||
|
self.current_size += len(frame.data)
|
||||||
|
|
||||||
|
elif frame.opcode is OP_PING:
|
||||||
|
# 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST
|
||||||
|
# send a Pong frame in response"
|
||||||
|
pong_frame = Frame(OP_PONG, frame.data)
|
||||||
|
self.send_frame(pong_frame)
|
||||||
|
|
||||||
|
elif frame.opcode is OP_PONG:
|
||||||
|
# 5.5.3 Pong: "A response to an unsolicited Pong frame is not
|
||||||
|
# expected."
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif frame.opcode is OP_CLOSE:
|
||||||
|
# 7.1.5. The WebSocket Connection Close Code
|
||||||
|
# 7.1.6. The WebSocket Connection Close Reason
|
||||||
|
self.close_rcvd = Close.parse(frame.data)
|
||||||
|
if self.state is CLOSING:
|
||||||
|
assert self.close_sent is not None
|
||||||
|
self.close_rcvd_then_sent = False
|
||||||
|
|
||||||
|
if self.current_size is not None:
|
||||||
|
raise ProtocolError("incomplete fragmented message")
|
||||||
|
|
||||||
|
# 5.5.1 Close: "If an endpoint receives a Close frame and did
|
||||||
|
# not previously send a Close frame, the endpoint MUST send a
|
||||||
|
# Close frame in response. (When sending a Close frame in
|
||||||
|
# response, the endpoint typically echos the status code it
|
||||||
|
# received.)"
|
||||||
|
|
||||||
|
if self.state is OPEN:
|
||||||
|
# Echo the original data instead of re-serializing it with
|
||||||
|
# Close.serialize() because that fails when the close frame
|
||||||
|
# is empty and Close.parse() synthesizes a 1005 close code.
|
||||||
|
# The rest is identical to send_close().
|
||||||
|
self.send_frame(Frame(OP_CLOSE, frame.data))
|
||||||
|
self.close_sent = self.close_rcvd
|
||||||
|
self.close_rcvd_then_sent = True
|
||||||
|
self.state = CLOSING
|
||||||
|
|
||||||
|
# 7.1.2. Start the WebSocket Closing Handshake: "Once an
|
||||||
|
# endpoint has both sent and received a Close control frame,
|
||||||
|
# that endpoint SHOULD _Close the WebSocket Connection_"
|
||||||
|
|
||||||
|
# A server closes the TCP connection immediately, while a client
|
||||||
|
# waits for the server to close the TCP connection.
|
||||||
|
if self.side is SERVER:
|
||||||
|
self.send_eof()
|
||||||
|
|
||||||
|
# 1.4. Closing Handshake: "after receiving a control frame
|
||||||
|
# indicating the connection should be closed, a peer discards
|
||||||
|
# any further data received."
|
||||||
|
# RFC 6455 allows reading Ping and Pong frames after a Close frame.
|
||||||
|
# However, that doesn't seem useful; websockets doesn't support it.
|
||||||
|
self.parser = self.discard()
|
||||||
|
next(self.parser) # start coroutine
|
||||||
|
|
||||||
|
else:
|
||||||
|
# This can't happen because Frame.parse() validates opcodes.
|
||||||
|
raise AssertionError(f"unexpected opcode: {frame.opcode:02x}")
|
||||||
|
|
||||||
|
self.events.append(frame)
|
||||||
|
|
||||||
|
# Private methods for sending events.
|
||||||
|
|
||||||
|
def send_frame(self, frame: Frame) -> None:
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("> %s", frame)
|
||||||
|
self.writes.append(
|
||||||
|
frame.serialize(
|
||||||
|
mask=self.side is CLIENT,
|
||||||
|
extensions=self.extensions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_eof(self) -> None:
|
||||||
|
assert not self.eof_sent
|
||||||
|
self.eof_sent = True
|
||||||
|
if self.debug:
|
||||||
|
self.logger.debug("> EOF")
|
||||||
|
self.writes.append(SEND_EOF)
|
||||||
150
venv/Lib/site-packages/websockets/proxy.py
Normal file
150
venv/Lib/site-packages/websockets/proxy.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from .datastructures import Headers
|
||||||
|
from .exceptions import InvalidProxy
|
||||||
|
from .headers import build_authorization_basic, build_host
|
||||||
|
from .http11 import USER_AGENT
|
||||||
|
from .uri import DELIMS, WebSocketURI
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["get_proxy", "parse_proxy", "Proxy"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class Proxy:
|
||||||
|
"""
|
||||||
|
Proxy address.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
scheme: ``"socks5h"``, ``"socks5"``, ``"socks4a"``, ``"socks4"``,
|
||||||
|
``"https"``, or ``"http"``.
|
||||||
|
host: Normalized to lower case.
|
||||||
|
port: Always set even if it's the default.
|
||||||
|
username: Available when the proxy address contains `User Information`_.
|
||||||
|
password: Available when the proxy address contains `User Information`_.
|
||||||
|
|
||||||
|
.. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
scheme: str
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
username: str | None = None
|
||||||
|
password: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_info(self) -> tuple[str, str] | None:
|
||||||
|
if self.username is None:
|
||||||
|
return None
|
||||||
|
assert self.password is not None
|
||||||
|
return (self.username, self.password)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_proxy(proxy: str) -> Proxy:
|
||||||
|
"""
|
||||||
|
Parse and validate a proxy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
proxy: proxy.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Parsed proxy.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidProxy: If ``proxy`` isn't a valid proxy.
|
||||||
|
|
||||||
|
"""
|
||||||
|
parsed = urllib.parse.urlparse(proxy)
|
||||||
|
if parsed.scheme not in ["socks5h", "socks5", "socks4a", "socks4", "https", "http"]:
|
||||||
|
raise InvalidProxy(proxy, f"scheme {parsed.scheme} isn't supported")
|
||||||
|
if parsed.hostname is None:
|
||||||
|
raise InvalidProxy(proxy, "hostname isn't provided")
|
||||||
|
if parsed.path not in ["", "/"]:
|
||||||
|
raise InvalidProxy(proxy, "path is meaningless")
|
||||||
|
if parsed.query != "":
|
||||||
|
raise InvalidProxy(proxy, "query is meaningless")
|
||||||
|
if parsed.fragment != "":
|
||||||
|
raise InvalidProxy(proxy, "fragment is meaningless")
|
||||||
|
|
||||||
|
scheme = parsed.scheme
|
||||||
|
host = parsed.hostname
|
||||||
|
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||||
|
username = parsed.username
|
||||||
|
password = parsed.password
|
||||||
|
# urllib.parse.urlparse accepts URLs with a username but without a
|
||||||
|
# password. This doesn't make sense for HTTP Basic Auth credentials.
|
||||||
|
if username is not None and password is None:
|
||||||
|
raise InvalidProxy(proxy, "username provided without password")
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxy.encode("ascii")
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
# Input contains non-ASCII characters.
|
||||||
|
# It must be an IRI. Convert it to a URI.
|
||||||
|
host = host.encode("idna").decode()
|
||||||
|
if username is not None:
|
||||||
|
assert password is not None
|
||||||
|
username = urllib.parse.quote(username, safe=DELIMS)
|
||||||
|
password = urllib.parse.quote(password, safe=DELIMS)
|
||||||
|
|
||||||
|
return Proxy(scheme, host, port, username, password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy(uri: WebSocketURI) -> str | None:
|
||||||
|
"""
|
||||||
|
Return the proxy to use for connecting to the given WebSocket URI, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if urllib.request.proxy_bypass(f"{uri.host}:{uri.port}"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# According to the _Proxy Usage_ section of RFC 6455, use a SOCKS5 proxy if
|
||||||
|
# available, else favor the proxy for HTTPS connections over the proxy for
|
||||||
|
# HTTP connections.
|
||||||
|
|
||||||
|
# The priority of a proxy for WebSocket connections is unspecified. We give
|
||||||
|
# it the highest priority. This makes it easy to configure a specific proxy
|
||||||
|
# for websockets.
|
||||||
|
|
||||||
|
# getproxies() may return SOCKS proxies as {"socks": "http://host:port"} or
|
||||||
|
# as {"https": "socks5h://host:port"} depending on whether they're declared
|
||||||
|
# in the operating system or in environment variables.
|
||||||
|
|
||||||
|
proxies = urllib.request.getproxies()
|
||||||
|
if uri.secure:
|
||||||
|
schemes = ["wss", "socks", "https"]
|
||||||
|
else:
|
||||||
|
schemes = ["ws", "socks", "https", "http"]
|
||||||
|
|
||||||
|
for scheme in schemes:
|
||||||
|
proxy = proxies.get(scheme)
|
||||||
|
if proxy is not None:
|
||||||
|
if scheme == "socks" and proxy.startswith("http://"):
|
||||||
|
proxy = "socks5h://" + proxy[7:]
|
||||||
|
return proxy
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_connect_request(
|
||||||
|
proxy: Proxy,
|
||||||
|
ws_uri: WebSocketURI,
|
||||||
|
user_agent_header: str | None = USER_AGENT,
|
||||||
|
) -> bytes:
|
||||||
|
host = build_host(ws_uri.host, ws_uri.port, ws_uri.secure, always_include_port=True)
|
||||||
|
headers = Headers()
|
||||||
|
headers["Host"] = build_host(ws_uri.host, ws_uri.port, ws_uri.secure)
|
||||||
|
if user_agent_header is not None:
|
||||||
|
headers["User-Agent"] = user_agent_header
|
||||||
|
if proxy.username is not None:
|
||||||
|
assert proxy.password is not None # enforced by parse_proxy()
|
||||||
|
headers["Proxy-Authorization"] = build_authorization_basic(
|
||||||
|
proxy.username, proxy.password
|
||||||
|
)
|
||||||
|
# We cannot use the Request class because it supports only GET requests.
|
||||||
|
return f"CONNECT {host} HTTP/1.1\r\n".encode() + headers.serialize()
|
||||||
Reference in New Issue
Block a user