Загрузить файлы в «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