summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party/websockets/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /testing/web-platform/tests/tools/third_party/websockets/src
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz
firefox-8dd16259287f58f9273002717ec4d27e97127719.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/websockets/src')
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO174
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt42
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py236
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py139
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py705
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py97
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py29
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py16
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py265
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py13
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py42
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py396
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py289
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py708
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py175
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi1
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py0
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py328
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py773
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py281
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py530
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py46
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py7
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py12
38 files changed, 3903 insertions, 1694 deletions
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO
deleted file mode 100644
index 3b042a3f9f..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO
+++ /dev/null
@@ -1,174 +0,0 @@
-Metadata-Version: 2.1
-Name: websockets
-Version: 10.3
-Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692)
-Home-page: https://github.com/aaugustin/websockets
-Author: Aymeric Augustin
-Author-email: aymeric.augustin@m4x.org
-License: BSD
-Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html
-Project-URL: Documentation, https://websockets.readthedocs.io/
-Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme
-Project-URL: Tracker, https://github.com/aaugustin/websockets/issues
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Web Environment
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: BSD License
-Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Requires-Python: >=3.7
-License-File: LICENSE
-
-.. image:: logo/horizontal.svg
- :width: 480px
- :alt: websockets
-
-|licence| |version| |pyversions| |wheel| |tests| |docs|
-
-.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |version| image:: https://img.shields.io/pypi/v/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg
- :target: https://pypi.python.org/pypi/websockets
-
-.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main
- :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml
-
-.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg
- :target: https://websockets.readthedocs.io/
-
-What is ``websockets``?
------------------------
-
-websockets is a library for building WebSocket_ servers and clients in Python
-with a focus on correctness, simplicity, robustness, and performance.
-
-.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
-
-Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it
-provides an elegant coroutine-based API.
-
-`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_
-
-Here's how a client sends and receives messages:
-
-.. copy-pasted because GitHub doesn't support the include directive
-
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import connect
-
- async def hello(uri):
- async with connect(uri) as websocket:
- await websocket.send("Hello world!")
- await websocket.recv()
-
- asyncio.run(hello("ws://localhost:8765"))
-
-And here's an echo server:
-
-.. code:: python
-
- #!/usr/bin/env python
-
- import asyncio
- from websockets import serve
-
- async def echo(websocket):
- async for message in websocket:
- await websocket.send(message)
-
- async def main():
- async with serve(echo, "localhost", 8765):
- await asyncio.Future() # run forever
-
- asyncio.run(main())
-
-Does that look good?
-
-`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_
-
-Why should I use ``websockets``?
---------------------------------
-
-The development of ``websockets`` is shaped by four principles:
-
-1. **Correctness**: ``websockets`` is heavily tested for compliance
- with :rfc:`6455`. Continuous integration fails under 100% branch
- coverage.
-
-2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and
- ``await ws.send(msg)``. ``websockets`` takes care of managing connections
- so you can focus on your application.
-
-3. **Robustness**: ``websockets`` is built for production. For example, it was
- the only library to `handle backpressure correctly`_ before the issue
- became widely known in the Python community.
-
-4. **Performance**: memory usage is optimized and configurable. A C extension
- accelerates expensive operations. It's pre-compiled for Linux, macOS and
- Windows and packaged in the wheel format for each system and Python version.
-
-Documentation is a first class concern in the project. Head over to `Read the
-Docs`_ and see for yourself.
-
-.. _Read the Docs: https://websockets.readthedocs.io/
-.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers
-
-Why shouldn't I use ``websockets``?
------------------------------------
-
-* If you prefer callbacks over coroutines: ``websockets`` was created to
- provide the best coroutine-based API to manage WebSocket connections in
- Python. Pick another library for a callback-based API.
-
-* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims
- at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol
- and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP
- is minimal — just enough for a HTTP health check.
-
- If you want to do both in the same server, look at HTTP frameworks that
- build on top of ``websockets`` to support WebSocket connections, like
- Sanic_.
-
-.. _Sanic: https://sanicframework.org/en/
-
-What else?
-----------
-
-Bug reports, patches and suggestions are welcome!
-
-To report a security vulnerability, please use the `Tidelift security
-contact`_. Tidelift will coordinate the fix and disclosure.
-
-.. _Tidelift security contact: https://tidelift.com/security
-
-For anything else, please open an issue_ or send a `pull request`_.
-
-.. _issue: https://github.com/aaugustin/websockets/issues/new
-.. _pull request: https://github.com/aaugustin/websockets/compare/
-
-Participants must uphold the `Contributor Covenant code of conduct`_.
-
-.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md
-
-``websockets`` is released under the `BSD license`_.
-
-.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE
-
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt
deleted file mode 100644
index 2a51106fee..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,42 +0,0 @@
-LICENSE
-MANIFEST.in
-README.rst
-setup.cfg
-setup.py
-src/websockets/__init__.py
-src/websockets/__main__.py
-src/websockets/auth.py
-src/websockets/client.py
-src/websockets/connection.py
-src/websockets/datastructures.py
-src/websockets/exceptions.py
-src/websockets/frames.py
-src/websockets/headers.py
-src/websockets/http.py
-src/websockets/http11.py
-src/websockets/imports.py
-src/websockets/py.typed
-src/websockets/server.py
-src/websockets/speedups.c
-src/websockets/streams.py
-src/websockets/typing.py
-src/websockets/uri.py
-src/websockets/utils.py
-src/websockets/version.py
-src/websockets.egg-info/PKG-INFO
-src/websockets.egg-info/SOURCES.txt
-src/websockets.egg-info/dependency_links.txt
-src/websockets.egg-info/not-zip-safe
-src/websockets.egg-info/top_level.txt
-src/websockets/extensions/__init__.py
-src/websockets/extensions/base.py
-src/websockets/extensions/permessage_deflate.py
-src/websockets/legacy/__init__.py
-src/websockets/legacy/auth.py
-src/websockets/legacy/client.py
-src/websockets/legacy/compatibility.py
-src/websockets/legacy/framing.py
-src/websockets/legacy/handshake.py
-src/websockets/legacy/http.py
-src/websockets/legacy/protocol.py
-src/websockets/legacy/server.py \ No newline at end of file
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789179..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe
deleted file mode 100644
index 8b13789179..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt b/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt
deleted file mode 100644
index 5474af7431..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-websockets
-websockets/extensions
-websockets/legacy
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
index ec34841247..fdb028f4c4 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__init__.py
@@ -1,23 +1,24 @@
from __future__ import annotations
+import typing
+
from .imports import lazy_import
-from .version import version as __version__ # noqa
+from .version import version as __version__ # noqa: F401
-__all__ = [ # noqa
+__all__ = [
+ # .client
+ "ClientProtocol",
+ # .datastructures
+ "Headers",
+ "HeadersLike",
+ "MultipleValuesError",
+ # .exceptions
"AbortHandshake",
- "basic_auth_protocol_factory",
- "BasicAuthWebSocketServerProtocol",
- "broadcast",
- "ClientConnection",
- "connect",
"ConnectionClosed",
"ConnectionClosedError",
"ConnectionClosedOK",
- "Data",
"DuplicateParameter",
- "ExtensionName",
- "ExtensionParameter",
"InvalidHandshake",
"InvalidHeader",
"InvalidHeaderFormat",
@@ -31,84 +32,159 @@ __all__ = [ # noqa
"InvalidStatusCode",
"InvalidUpgrade",
"InvalidURI",
- "LoggerLike",
"NegotiationError",
- "Origin",
- "parse_uri",
"PayloadTooBig",
"ProtocolError",
"RedirectHandshake",
"SecurityError",
- "serve",
- "ServerConnection",
- "Subprotocol",
- "unix_connect",
- "unix_serve",
- "WebSocketClientProtocol",
- "WebSocketCommonProtocol",
"WebSocketException",
"WebSocketProtocolError",
+ # .legacy.auth
+ "BasicAuthWebSocketServerProtocol",
+ "basic_auth_protocol_factory",
+ # .legacy.client
+ "WebSocketClientProtocol",
+ "connect",
+ "unix_connect",
+ # .legacy.protocol
+ "WebSocketCommonProtocol",
+ "broadcast",
+ # .legacy.server
"WebSocketServer",
"WebSocketServerProtocol",
- "WebSocketURI",
+ "serve",
+ "unix_serve",
+ # .server
+ "ServerProtocol",
+ # .typing
+ "Data",
+ "ExtensionName",
+ "ExtensionParameter",
+ "LoggerLike",
+ "StatusLike",
+ "Origin",
+ "Subprotocol",
]
-lazy_import(
- globals(),
- aliases={
- "auth": ".legacy",
- "basic_auth_protocol_factory": ".legacy.auth",
- "BasicAuthWebSocketServerProtocol": ".legacy.auth",
- "broadcast": ".legacy.protocol",
- "ClientConnection": ".client",
- "connect": ".legacy.client",
- "unix_connect": ".legacy.client",
- "WebSocketClientProtocol": ".legacy.client",
- "Headers": ".datastructures",
- "MultipleValuesError": ".datastructures",
- "WebSocketException": ".exceptions",
- "ConnectionClosed": ".exceptions",
- "ConnectionClosedError": ".exceptions",
- "ConnectionClosedOK": ".exceptions",
- "InvalidHandshake": ".exceptions",
- "SecurityError": ".exceptions",
- "InvalidMessage": ".exceptions",
- "InvalidHeader": ".exceptions",
- "InvalidHeaderFormat": ".exceptions",
- "InvalidHeaderValue": ".exceptions",
- "InvalidOrigin": ".exceptions",
- "InvalidUpgrade": ".exceptions",
- "InvalidStatus": ".exceptions",
- "InvalidStatusCode": ".exceptions",
- "NegotiationError": ".exceptions",
- "DuplicateParameter": ".exceptions",
- "InvalidParameterName": ".exceptions",
- "InvalidParameterValue": ".exceptions",
- "AbortHandshake": ".exceptions",
- "RedirectHandshake": ".exceptions",
- "InvalidState": ".exceptions",
- "InvalidURI": ".exceptions",
- "PayloadTooBig": ".exceptions",
- "ProtocolError": ".exceptions",
- "WebSocketProtocolError": ".exceptions",
- "protocol": ".legacy",
- "WebSocketCommonProtocol": ".legacy.protocol",
- "ServerConnection": ".server",
- "serve": ".legacy.server",
- "unix_serve": ".legacy.server",
- "WebSocketServerProtocol": ".legacy.server",
- "WebSocketServer": ".legacy.server",
- "Data": ".typing",
- "LoggerLike": ".typing",
- "Origin": ".typing",
- "ExtensionHeader": ".typing",
- "ExtensionParameter": ".typing",
- "Subprotocol": ".typing",
- },
- deprecated_aliases={
- "framing": ".legacy",
- "handshake": ".legacy",
- "parse_uri": ".uri",
- "WebSocketURI": ".uri",
- },
-)
+# When type checking, import non-deprecated aliases eagerly. Else, import on demand.
+if typing.TYPE_CHECKING:
+ from .client import ClientProtocol
+ from .datastructures import Headers, HeadersLike, MultipleValuesError
+ from .exceptions import (
+ AbortHandshake,
+ ConnectionClosed,
+ ConnectionClosedError,
+ ConnectionClosedOK,
+ DuplicateParameter,
+ InvalidHandshake,
+ InvalidHeader,
+ InvalidHeaderFormat,
+ InvalidHeaderValue,
+ InvalidMessage,
+ InvalidOrigin,
+ InvalidParameterName,
+ InvalidParameterValue,
+ InvalidState,
+ InvalidStatus,
+ InvalidStatusCode,
+ InvalidUpgrade,
+ InvalidURI,
+ NegotiationError,
+ PayloadTooBig,
+ ProtocolError,
+ RedirectHandshake,
+ SecurityError,
+ WebSocketException,
+ WebSocketProtocolError,
+ )
+ from .legacy.auth import (
+ BasicAuthWebSocketServerProtocol,
+ basic_auth_protocol_factory,
+ )
+ from .legacy.client import WebSocketClientProtocol, connect, unix_connect
+ from .legacy.protocol import WebSocketCommonProtocol, broadcast
+ from .legacy.server import (
+ WebSocketServer,
+ WebSocketServerProtocol,
+ serve,
+ unix_serve,
+ )
+ from .server import ServerProtocol
+ from .typing import (
+ Data,
+ ExtensionName,
+ ExtensionParameter,
+ LoggerLike,
+ Origin,
+ StatusLike,
+ Subprotocol,
+ )
+else:
+ lazy_import(
+ globals(),
+ aliases={
+ # .client
+ "ClientProtocol": ".client",
+ # .datastructures
+ "Headers": ".datastructures",
+ "HeadersLike": ".datastructures",
+ "MultipleValuesError": ".datastructures",
+ # .exceptions
+ "AbortHandshake": ".exceptions",
+ "ConnectionClosed": ".exceptions",
+ "ConnectionClosedError": ".exceptions",
+ "ConnectionClosedOK": ".exceptions",
+ "DuplicateParameter": ".exceptions",
+ "InvalidHandshake": ".exceptions",
+ "InvalidHeader": ".exceptions",
+ "InvalidHeaderFormat": ".exceptions",
+ "InvalidHeaderValue": ".exceptions",
+ "InvalidMessage": ".exceptions",
+ "InvalidOrigin": ".exceptions",
+ "InvalidParameterName": ".exceptions",
+ "InvalidParameterValue": ".exceptions",
+ "InvalidState": ".exceptions",
+ "InvalidStatus": ".exceptions",
+ "InvalidStatusCode": ".exceptions",
+ "InvalidUpgrade": ".exceptions",
+ "InvalidURI": ".exceptions",
+ "NegotiationError": ".exceptions",
+ "PayloadTooBig": ".exceptions",
+ "ProtocolError": ".exceptions",
+ "RedirectHandshake": ".exceptions",
+ "SecurityError": ".exceptions",
+ "WebSocketException": ".exceptions",
+ "WebSocketProtocolError": ".exceptions",
+ # .legacy.auth
+ "BasicAuthWebSocketServerProtocol": ".legacy.auth",
+ "basic_auth_protocol_factory": ".legacy.auth",
+ # .legacy.client
+ "WebSocketClientProtocol": ".legacy.client",
+ "connect": ".legacy.client",
+ "unix_connect": ".legacy.client",
+ # .legacy.protocol
+ "WebSocketCommonProtocol": ".legacy.protocol",
+ "broadcast": ".legacy.protocol",
+ # .legacy.server
+ "WebSocketServer": ".legacy.server",
+ "WebSocketServerProtocol": ".legacy.server",
+ "serve": ".legacy.server",
+ "unix_serve": ".legacy.server",
+ # .server
+ "ServerProtocol": ".server",
+ # .typing
+ "Data": ".typing",
+ "ExtensionName": ".typing",
+ "ExtensionParameter": ".typing",
+ "LoggerLike": ".typing",
+ "Origin": ".typing",
+ "StatusLike": "typing",
+ "Subprotocol": ".typing",
+ },
+ deprecated_aliases={
+ "framing": ".legacy",
+ "handshake": ".legacy",
+ "parse_uri": ".uri",
+ "WebSocketURI": ".uri",
+ },
+ )
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
index c562d21b54..f2ea5cf4e8 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/__main__.py
@@ -1,16 +1,18 @@
from __future__ import annotations
import argparse
-import asyncio
import os
import signal
import sys
import threading
-from typing import Any, Set
-from .exceptions import ConnectionClosed
-from .frames import Close
-from .legacy.client import connect
+
+try:
+ import readline # noqa: F401
+except ImportError: # Windows has no `readline` normally
+ pass
+
+from .sync.client import ClientConnection, connect
from .version import version as websockets_version
@@ -46,21 +48,6 @@ if sys.platform == "win32":
raise RuntimeError("unable to set console mode")
-def exit_from_event_loop_thread(
- loop: asyncio.AbstractEventLoop,
- stop: asyncio.Future[None],
-) -> None:
- loop.stop()
- if not stop.done():
- # When exiting the thread that runs the event loop, raise
- # KeyboardInterrupt in the main thread to exit the program.
- if sys.platform == "win32":
- ctrl_c = signal.CTRL_C_EVENT
- else:
- ctrl_c = signal.SIGINT
- os.kill(os.getpid(), ctrl_c)
-
-
def print_during_input(string: str) -> None:
sys.stdout.write(
# Save cursor position
@@ -93,63 +80,20 @@ def print_over_input(string: str) -> None:
sys.stdout.flush()
-async def run_client(
- uri: str,
- loop: asyncio.AbstractEventLoop,
- inputs: asyncio.Queue[str],
- stop: asyncio.Future[None],
-) -> None:
- try:
- websocket = await connect(uri)
- except Exception as exc:
- print_over_input(f"Failed to connect to {uri}: {exc}.")
- exit_from_event_loop_thread(loop, stop)
- return
- else:
- print_during_input(f"Connected to {uri}.")
-
- try:
- while True:
- incoming: asyncio.Future[Any] = asyncio.create_task(websocket.recv())
- outgoing: asyncio.Future[Any] = asyncio.create_task(inputs.get())
- done: Set[asyncio.Future[Any]]
- pending: Set[asyncio.Future[Any]]
- done, pending = await asyncio.wait(
- [incoming, outgoing, stop], return_when=asyncio.FIRST_COMPLETED
- )
-
- # Cancel pending tasks to avoid leaking them.
- if incoming in pending:
- incoming.cancel()
- if outgoing in pending:
- outgoing.cancel()
-
- if incoming in done:
- try:
- message = incoming.result()
- except ConnectionClosed:
- break
- else:
- if isinstance(message, str):
- print_during_input("< " + message)
- else:
- print_during_input("< (binary) " + message.hex())
-
- if outgoing in done:
- message = outgoing.result()
- await websocket.send(message)
-
- if stop in done:
- break
-
- finally:
- await websocket.close()
- assert websocket.close_code is not None and websocket.close_reason is not None
- close_status = Close(websocket.close_code, websocket.close_reason)
-
- print_over_input(f"Connection closed: {close_status}.")
-
- exit_from_event_loop_thread(loop, stop)
+def print_incoming_messages(websocket: ClientConnection, stop: threading.Event) -> None:
+ for message in websocket:
+ if isinstance(message, str):
+ print_during_input("< " + message)
+ else:
+ print_during_input("< (binary) " + message.hex())
+ if not stop.is_set():
+ # When the server closes the connection, raise KeyboardInterrupt
+ # in the main thread to exit the program.
+ if sys.platform == "win32":
+ ctrl_c = signal.CTRL_C_EVENT
+ else:
+ ctrl_c = signal.SIGINT
+ os.kill(os.getpid(), ctrl_c)
def main() -> None:
@@ -184,29 +128,17 @@ def main() -> None:
sys.stderr.flush()
try:
- import readline # noqa
- except ImportError: # Windows has no `readline` normally
- pass
-
- # Create an event loop that will run in a background thread.
- loop = asyncio.new_event_loop()
-
- # Due to zealous removal of the loop parameter in the Queue constructor,
- # we need a factory coroutine to run in the freshly created event loop.
- async def queue_factory() -> asyncio.Queue[str]:
- return asyncio.Queue()
-
- # Create a queue of user inputs. There's no need to limit its size.
- inputs: asyncio.Queue[str] = loop.run_until_complete(queue_factory())
-
- # Create a stop condition when receiving SIGINT or SIGTERM.
- stop: asyncio.Future[None] = loop.create_future()
+ websocket = connect(args.uri)
+ except Exception as exc:
+ print(f"Failed to connect to {args.uri}: {exc}.")
+ sys.exit(1)
+ else:
+ print(f"Connected to {args.uri}.")
- # Schedule the task that will manage the connection.
- loop.create_task(run_client(args.uri, loop, inputs, stop))
+ stop = threading.Event()
- # Start the event loop in a background thread.
- thread = threading.Thread(target=loop.run_forever)
+ # Start the thread that reads messages from the connection.
+ thread = threading.Thread(target=print_incoming_messages, args=(websocket, stop))
thread.start()
# Read from stdin in the main thread in order to receive signals.
@@ -214,17 +146,14 @@ def main() -> None:
while True:
# Since there's no size limit, put_nowait is identical to put.
message = input("> ")
- loop.call_soon_threadsafe(inputs.put_nowait, message)
+ websocket.send(message)
except (KeyboardInterrupt, EOFError): # ^C, ^D
- loop.call_soon_threadsafe(stop.set_result, None)
+ stop.set()
+ websocket.close()
+ print_over_input("Connection closed.")
- # Wait for the event loop to terminate.
thread.join()
- # For reasons unclear, even though the loop is closed in the thread,
- # it still thinks it's running here.
- loop.close()
-
if __name__ == "__main__":
main()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
index afcb38cffe..b792e02f5c 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/auth.py
@@ -1,4 +1,6 @@
from __future__ import annotations
# See #940 for why lazy_import isn't used here for backwards compatibility.
-from .legacy.auth import * # noqa
+# See #1400 for why listing compatibility imports in __all__ helps PyCharm.
+from .legacy.auth import *
+from .legacy.auth import __all__ # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
index df8e53429a..b2f622042d 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/client.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from typing import Generator, List, Optional, Sequence
+import warnings
+from typing import Any, Generator, List, Optional, Sequence
-from .connection import CLIENT, CONNECTING, OPEN, Connection, State
from .datastructures import Headers, MultipleValuesError
from .exceptions import (
InvalidHandshake,
@@ -23,8 +23,8 @@ from .headers import (
parse_subprotocol,
parse_upgrade,
)
-from .http import USER_AGENT
from .http11 import Request, Response
+from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State
from .typing import (
ConnectionOption,
ExtensionHeader,
@@ -38,13 +38,15 @@ from .utils import accept_key, generate_key
# See #940 for why lazy_import isn't used here for backwards compatibility.
-from .legacy.client import * # isort:skip # noqa
+# See #1400 for why listing compatibility imports in __all__ helps PyCharm.
+from .legacy.client import * # isort:skip # noqa: I001
+from .legacy.client import __all__ as legacy__all__
-__all__ = ["ClientConnection"]
+__all__ = ["ClientProtocol"] + legacy__all__
-class ClientConnection(Connection):
+class ClientProtocol(Protocol):
"""
Sans-I/O implementation of a WebSocket client connection.
@@ -60,16 +62,17 @@ class ClientConnection(Connection):
preference.
state: initial state of the WebSocket connection.
max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
+ :obj:`None` disables the limit.
logger: logger for this connection;
defaults to ``logging.getLogger("websockets.client")``;
- see the :doc:`logging guide <../topics/logging>` for details.
+ see the :doc:`logging guide <../../topics/logging>` for details.
"""
def __init__(
self,
wsuri: WebSocketURI,
+ *,
origin: Optional[Origin] = None,
extensions: Optional[Sequence[ClientExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
@@ -89,7 +92,7 @@ class ClientConnection(Connection):
self.available_subprotocols = subprotocols
self.key = generate_key()
- def connect(self) -> Request: # noqa: F811
+ def connect(self) -> Request:
"""
Create a handshake request to open a connection.
@@ -131,8 +134,6 @@ class ClientConnection(Connection):
protocol_header = build_subprotocol(self.available_subprotocols)
headers["Sec-WebSocket-Protocol"] = protocol_header
- headers["User-Agent"] = USER_AGENT
-
return Request(self.wsuri.resource_name, headers)
def process_response(self, response: Response) -> None:
@@ -223,7 +224,6 @@ class ClientConnection(Connection):
extensions = headers.get_all("Sec-WebSocket-Extensions")
if extensions:
-
if self.available_extensions is None:
raise InvalidHandshake("no extensions supported")
@@ -232,9 +232,7 @@ class ClientConnection(Connection):
)
for name, response_params in parsed_extensions:
-
for extension_factory in self.available_extensions:
-
# Skip non-matching extensions based on their name.
if extension_factory.name != name:
continue
@@ -281,7 +279,6 @@ class ClientConnection(Connection):
subprotocols = headers.get_all("Sec-WebSocket-Protocol")
if subprotocols:
-
if self.available_subprotocols is None:
raise InvalidHandshake("no subprotocols supported")
@@ -317,11 +314,17 @@ class ClientConnection(Connection):
def parse(self) -> Generator[None, None, None]:
if self.state is CONNECTING:
- response = yield from Response.parse(
- self.reader.read_line,
- self.reader.read_exact,
- self.reader.read_to_eof,
- )
+ try:
+ response = yield from Response.parse(
+ self.reader.read_line,
+ self.reader.read_exact,
+ self.reader.read_to_eof,
+ )
+ except Exception as exc:
+ self.handshake_exc = exc
+ self.parser = self.discard()
+ next(self.parser) # start coroutine
+ yield
if self.debug:
code, phrase = response.status_code, response.reason_phrase
@@ -335,13 +338,23 @@ class ClientConnection(Connection):
self.process_response(response)
except InvalidHandshake as exc:
response._exception = exc
+ self.events.append(response)
self.handshake_exc = exc
self.parser = self.discard()
next(self.parser) # start coroutine
- else:
- assert self.state is CONNECTING
- self.state = OPEN
- finally:
- self.events.append(response)
+ yield
+
+ assert self.state is CONNECTING
+ self.state = OPEN
+ self.events.append(response)
yield from super().parse()
+
+
+class ClientConnection(ClientProtocol):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ warnings.warn(
+ "ClientConnection was renamed to ClientProtocol",
+ DeprecationWarning,
+ )
+ super().__init__(*args, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py
index db8b536993..88bcda1aaf 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/connection.py
@@ -1,702 +1,13 @@
from __future__ import annotations
-import enum
-import logging
-import uuid
-from typing import Generator, List, Optional, Type, Union
+import warnings
-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,
- Frame,
-)
-from .http11 import Request, Response
-from .streams import StreamReader
-from .typing import LoggerLike, Origin, Subprotocol
-
-
-__all__ = [
- "Connection",
- "Side",
- "State",
- "SEND_EOF",
-]
-
-Event = Union[Request, Response, Frame]
-"""Events that :meth:`~Connection.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 Connection:
- """
- 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` to disable the limit.
- 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: Optional[int] = 2**20,
- logger: Optional[LoggerLike] = 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.
- self.max_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.cur_size: Optional[int] = 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: Optional[Origin] = None
- self.extensions: List[Extension] = []
- self.subprotocol: Optional[Subprotocol] = None
-
- # Close code and reason, set when a close frame is sent or received.
- self.close_rcvd: Optional[Close] = None
- self.close_sent: Optional[Close] = None
- self.close_rcvd_then_sent: Optional[bool] = None
-
- # Track if an exception happened during the handshake.
- self.handshake_exc: Optional[Exception] = 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: Optional[Exception] = None
-
- @property
- def state(self) -> State:
- """
- WebSocket connection state.
-
- Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`.
-
- """
- 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) -> Optional[int]:
- """
- `WebSocket close code`_.
-
- .. _WebSocket close code:
- https://www.rfc-editor.org/rfc/rfc6455.html#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 1006
- else:
- return self.close_rcvd.code
-
- @property
- def close_reason(self) -> Optional[str]:
- """
- `WebSocket close reason`_.
-
- .. _WebSocket close reason:
- https://www.rfc-editor.org/rfc/rfc6455.html#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.connection.State.CLOSING`; wait until
- it's :attr:`~websockets.connection.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) -> 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.
- - You aren't expected to call :meth:`events_received`; it won't return
- any new events.
-
- Raises:
- EOFError: if :meth:`receive_eof` was called earlier.
-
- """
- self.reader.feed_eof()
- next(self.parser)
-
- # Public methods for sending events.
-
- def send_continuation(self, data: bytes, 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")
- self.expect_continuation_frame = not fin
- self.send_frame(Frame(OP_CONT, data, fin))
-
- def send_text(self, data: bytes, 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")
- self.expect_continuation_frame = not fin
- self.send_frame(Frame(OP_TEXT, data, fin))
-
- def send_binary(self, data: bytes, fin: bool = True) -> None:
- """
- Send a `Binary frame`_.
+# lazy_import doesn't support this use case.
+from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401
- .. _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")
- self.expect_continuation_frame = not fin
- self.send_frame(Frame(OP_BINARY, data, fin))
-
- def send_close(self, code: Optional[int] = 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 a fragmented message is being sent, if the code
- isn't valid, or if a reason is provided without a code
-
- """
- if self.expect_continuation_frame:
- raise ProtocolError("expected a continuation frame")
- if code is None:
- if reason != "":
- raise ProtocolError("cannot send a reason without a code")
- close = Close(1005, "")
- data = b""
- else:
- close = Close(code, reason)
- data = close.serialize()
- # send_frame() guarantees that self.state is OPEN at this point.
- # 7.1.3. The WebSocket Closing Handshake is Started
- self.send_frame(Frame(OP_CLOSE, data))
- self.close_sent = close
- self.state = CLOSING
-
- def send_ping(self, data: bytes) -> 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.
-
- """
- self.send_frame(Frame(OP_PING, data))
-
- def send_pong(self, data: bytes) -> 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.
-
- """
- self.send_frame(Frame(OP_PONG, data))
-
- def fail(self, code: 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 != 1006:
- close = Close(code, reason)
- data = close.serialize()
- self.send_frame(Frame(OP_CLOSE, data))
- self.close_sent = close
- 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:
- List[Event]: 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.connection.SEND_EOF` signals
- the end of the data stream. When you receive it, half-close the TCP
- connection.
-
- Returns:
- List[bytes]: 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_*()`` 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:
- bool: Whether the TCP connection is expected to close soon.
-
- """
- # We expect a TCP close if and only if we sent a close frame:
- # * 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.
- # We already got a TCP Close if and only if the state is CLOSED.
- return self.state is CLOSING or self.handshake_exc is not None
-
- # Private methods for receiving data.
-
- def parse(self) -> Generator[None, None, 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.
-
- """
- 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")
-
- if self.max_size is None:
- max_size = None
- elif self.cur_size is None:
- max_size = self.max_size
- else:
- max_size = self.max_size - self.cur_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(1002, str(exc))
- self.parser_exc = exc
-
- except EOFError as exc:
- self.fail(1006, str(exc))
- self.parser_exc = exc
-
- except UnicodeDecodeError as exc:
- self.fail(1007, f"{exc.reason} at position {exc.start}")
- self.parser_exc = exc
-
- except PayloadTooBig as exc:
- self.fail(1009, 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(1011)
- 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") # pragma: no cover
-
- def discard(self) -> Generator[None, None, 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).
-
- """
- # The server close the TCP connection in the same circumstances where
- # discard() replaces parse(). The client closes the connection later,
- # after the server closes the connection or a timeout elapses.
- # (The latter case cannot be handled in this Sans-I/O layer.)
- assert (self.side is SERVER) == (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:
- 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") # pragma: no cover
-
- 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.cur_size is not None:
- raise ProtocolError("expected a continuation frame")
- if frame.fin:
- self.cur_size = None
- else:
- self.cur_size = len(frame.data)
-
- elif frame.opcode is OP_CONT:
- if self.cur_size is None:
- raise ProtocolError("unexpected continuation frame")
- if frame.fin:
- self.cur_size = None
- else:
- self.cur_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.cur_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() synthetizes 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."
- self.parser = self.discard()
- next(self.parser) # start coroutine
-
- else: # pragma: no cover
- # 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.state is not OPEN:
- raise InvalidState(
- f"cannot write to a WebSocket in the {self.state.name} state"
- )
-
- 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)
+warnings.warn(
+ "websockets.connection was renamed to websockets.protocol "
+ "and Connection was renamed to Protocol",
+ DeprecationWarning,
+)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py
index 36a2cbaf99..a0a648463a 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/datastructures.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import sys
from typing import (
Any,
Dict,
@@ -9,17 +8,12 @@ from typing import (
List,
Mapping,
MutableMapping,
+ Protocol,
Tuple,
Union,
)
-if sys.version_info[:2] >= (3, 8):
- from typing import Protocol
-else: # pragma: no cover
- Protocol = object # mypy will report errors on Python 3.7.
-
-
__all__ = ["Headers", "HeadersLike", "MultipleValuesError"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
index 0c4fc51851..f7169e3b17 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/exceptions.py
@@ -34,6 +34,7 @@ import http
from typing import Optional
from . import datastructures, frames, http11
+from .typing import StatusLike
__all__ = [
@@ -120,19 +121,23 @@ class ConnectionClosed(WebSocketException):
@property
def code(self) -> int:
- return 1006 if self.rcvd is None else self.rcvd.code
+ if self.rcvd is None:
+ return frames.CloseCode.ABNORMAL_CLOSURE
+ return self.rcvd.code
@property
def reason(self) -> str:
- return "" if self.rcvd is None else self.rcvd.reason
+ if self.rcvd is None:
+ return ""
+ return self.rcvd.reason
class ConnectionClosedError(ConnectionClosed):
"""
Like :exc:`ConnectionClosed`, when the connection terminated with an error.
- A close code other than 1000 (OK) or 1001 (going away) was received or
- sent, or the closing handshake didn't complete properly.
+ A close frame with a code other than 1000 (OK) or 1001 (going away) was
+ received or sent, or the closing handshake didn't complete properly.
"""
@@ -141,7 +146,8 @@ class ConnectionClosedOK(ConnectionClosed):
"""
Like :exc:`ConnectionClosed`, when the connection terminated properly.
- A close code 1000 (OK) or 1001 (going away) was received and sent.
+ A close code with code 1000 (OK) or 1001 (going away) or without a code was
+ received and sent.
"""
@@ -171,7 +177,7 @@ class InvalidMessage(InvalidHandshake):
class InvalidHeader(InvalidHandshake):
"""
- Raised when a HTTP header doesn't have a valid format or value.
+ Raised when an HTTP header doesn't have a valid format or value.
"""
@@ -190,7 +196,7 @@ class InvalidHeader(InvalidHandshake):
class InvalidHeaderFormat(InvalidHeader):
"""
- Raised when a HTTP header cannot be parsed.
+ Raised when an HTTP header cannot be parsed.
The format of the header doesn't match the grammar for that header.
@@ -202,7 +208,7 @@ class InvalidHeaderFormat(InvalidHeader):
class InvalidHeaderValue(InvalidHeader):
"""
- Raised when a HTTP header has a wrong value.
+ Raised when an HTTP header has a wrong value.
The format of the header is correct but a value isn't acceptable.
@@ -310,7 +316,7 @@ class InvalidParameterValue(NegotiationError):
class AbortHandshake(InvalidHandshake):
"""
- Raised to abort the handshake on purpose and return a HTTP response.
+ Raised to abort the handshake on purpose and return an HTTP response.
This exception is an implementation detail.
@@ -325,11 +331,12 @@ class AbortHandshake(InvalidHandshake):
def __init__(
self,
- status: http.HTTPStatus,
+ status: StatusLike,
headers: datastructures.HeadersLike,
body: bytes = b"",
) -> None:
- self.status = status
+ # If a user passes an int instead of a HTTPStatus, fix it automatically.
+ self.status = http.HTTPStatus(status)
self.headers = datastructures.Headers(headers)
self.body = body
@@ -369,7 +376,7 @@ class InvalidState(WebSocketException, AssertionError):
class InvalidURI(WebSocketException):
"""
- Raised when connecting to an URI that isn't a valid WebSocket URI.
+ Raised when connecting to a URI that isn't a valid WebSocket URI.
"""
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
index 0609676185..6c481a46cc 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/base.py
@@ -38,6 +38,7 @@ class Extension:
PayloadTooBig: if decoding the payload exceeds ``max_size``.
"""
+ raise NotImplementedError
def encode(self, frame: frames.Frame) -> frames.Frame:
"""
@@ -50,6 +51,7 @@ class Extension:
Frame: Encoded frame.
"""
+ raise NotImplementedError
class ClientExtensionFactory:
@@ -69,6 +71,7 @@ class ClientExtensionFactory:
List[ExtensionParameter]: Parameters to send to the server.
"""
+ raise NotImplementedError
def process_response_params(
self,
@@ -91,6 +94,7 @@ class ClientExtensionFactory:
NegotiationError: if parameters aren't acceptable.
"""
+ raise NotImplementedError
class ServerExtensionFactory:
@@ -126,3 +130,4 @@ class ServerExtensionFactory:
the client aren't acceptable.
"""
+ raise NotImplementedError
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
index e0de5e8f85..b391837c66 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py
@@ -211,7 +211,6 @@ def _extract_parameters(
client_max_window_bits: Optional[Union[int, bool]] = None
for name, value in params:
-
if name == "server_no_context_takeover":
if server_no_context_takeover:
raise exceptions.DuplicateParameter(name)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py
index 043b688b52..6b1befb2e0 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/frames.py
@@ -13,7 +13,7 @@ from .typing import Data
try:
from .speedups import apply_mask
-except ImportError: # pragma: no cover
+except ImportError:
from .utils import apply_mask
@@ -52,45 +52,70 @@ DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY
CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG
-# See https://www.iana.org/assignments/websocket/websocket.xhtml
-CLOSE_CODES = {
- 1000: "OK",
- 1001: "going away",
- 1002: "protocol error",
- 1003: "unsupported type",
+class CloseCode(enum.IntEnum):
+ """Close code values for WebSocket close frames."""
+
+ NORMAL_CLOSURE = 1000
+ GOING_AWAY = 1001
+ PROTOCOL_ERROR = 1002
+ UNSUPPORTED_DATA = 1003
# 1004 is reserved
- 1005: "no status code [internal]",
- 1006: "connection closed abnormally [internal]",
- 1007: "invalid data",
- 1008: "policy violation",
- 1009: "message too big",
- 1010: "extension required",
- 1011: "unexpected error",
- 1012: "service restart",
- 1013: "try again later",
- 1014: "bad gateway",
- 1015: "TLS failure [internal]",
+ NO_STATUS_RCVD = 1005
+ ABNORMAL_CLOSURE = 1006
+ INVALID_DATA = 1007
+ POLICY_VIOLATION = 1008
+ MESSAGE_TOO_BIG = 1009
+ MANDATORY_EXTENSION = 1010
+ INTERNAL_ERROR = 1011
+ SERVICE_RESTART = 1012
+ TRY_AGAIN_LATER = 1013
+ BAD_GATEWAY = 1014
+ TLS_HANDSHAKE = 1015
+
+
+# See https://www.iana.org/assignments/websocket/websocket.xhtml
+CLOSE_CODE_EXPLANATIONS: dict[int, str] = {
+ CloseCode.NORMAL_CLOSURE: "OK",
+ CloseCode.GOING_AWAY: "going away",
+ CloseCode.PROTOCOL_ERROR: "protocol error",
+ CloseCode.UNSUPPORTED_DATA: "unsupported data",
+ CloseCode.NO_STATUS_RCVD: "no status received [internal]",
+ CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]",
+ CloseCode.INVALID_DATA: "invalid frame payload data",
+ CloseCode.POLICY_VIOLATION: "policy violation",
+ CloseCode.MESSAGE_TOO_BIG: "message too big",
+ CloseCode.MANDATORY_EXTENSION: "mandatory extension",
+ CloseCode.INTERNAL_ERROR: "internal error",
+ CloseCode.SERVICE_RESTART: "service restart",
+ CloseCode.TRY_AGAIN_LATER: "try again later",
+ CloseCode.BAD_GATEWAY: "bad gateway",
+ CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]",
}
# Close code that are allowed in a close frame.
# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`.
EXTERNAL_CLOSE_CODES = {
- 1000,
- 1001,
- 1002,
- 1003,
- 1007,
- 1008,
- 1009,
- 1010,
- 1011,
- 1012,
- 1013,
- 1014,
+ CloseCode.NORMAL_CLOSURE,
+ CloseCode.GOING_AWAY,
+ CloseCode.PROTOCOL_ERROR,
+ CloseCode.UNSUPPORTED_DATA,
+ CloseCode.INVALID_DATA,
+ CloseCode.POLICY_VIOLATION,
+ CloseCode.MESSAGE_TOO_BIG,
+ CloseCode.MANDATORY_EXTENSION,
+ CloseCode.INTERNAL_ERROR,
+ CloseCode.SERVICE_RESTART,
+ CloseCode.TRY_AGAIN_LATER,
+ CloseCode.BAD_GATEWAY,
}
-OK_CLOSE_CODES = {1000, 1001}
+
+OK_CLOSE_CODES = {
+ CloseCode.NORMAL_CLOSURE,
+ CloseCode.GOING_AWAY,
+ CloseCode.NO_STATUS_RCVD,
+}
BytesLike = bytes, bytearray, memoryview
@@ -123,7 +148,7 @@ class Frame:
def __str__(self) -> str:
"""
- Return a human-readable represention of a frame.
+ Return a human-readable representation of a frame.
"""
coding = None
@@ -191,6 +216,8 @@ class Frame:
extensions: list of extensions, applied in reverse order.
Raises:
+ EOFError: if the connection is closed without a full WebSocket frame.
+ UnicodeDecodeError: if the frame contains invalid UTF-8.
PayloadTooBig: if the frame's payload size exceeds ``max_size``.
ProtocolError: if the frame contains incorrect values.
@@ -383,7 +410,7 @@ class Close:
def __str__(self) -> str:
"""
- Return a human-readable represention of a close code and reason.
+ Return a human-readable representation of a close code and reason.
"""
if 3000 <= self.code < 4000:
@@ -391,7 +418,7 @@ class Close:
elif 4000 <= self.code < 5000:
explanation = "private use"
else:
- explanation = CLOSE_CODES.get(self.code, "unknown")
+ explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown")
result = f"{self.code} ({explanation})"
if self.reason:
@@ -419,7 +446,7 @@ class Close:
close.check()
return close
elif len(data) == 0:
- return cls(1005, "")
+ return cls(CloseCode.NO_STATUS_RCVD, "")
else:
raise exceptions.ProtocolError("close frame too short")
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
index b14fa94bdc..9f86f6a1ff 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import sys
+import typing
from .imports import lazy_import
from .version import version as websockets_version
@@ -9,18 +10,22 @@ from .version import version as websockets_version
# For backwards compatibility:
-lazy_import(
- globals(),
- # Headers and MultipleValuesError used to be defined in this module.
- aliases={
- "Headers": ".datastructures",
- "MultipleValuesError": ".datastructures",
- },
- deprecated_aliases={
- "read_request": ".legacy.http",
- "read_response": ".legacy.http",
- },
-)
+# When type checking, import non-deprecated aliases eagerly. Else, import on demand.
+if typing.TYPE_CHECKING:
+ from .datastructures import Headers, MultipleValuesError # noqa: F401
+else:
+ lazy_import(
+ globals(),
+ # Headers and MultipleValuesError used to be defined in this module.
+ aliases={
+ "Headers": ".datastructures",
+ "MultipleValuesError": ".datastructures",
+ },
+ deprecated_aliases={
+ "read_request": ".legacy.http",
+ "read_response": ".legacy.http",
+ },
+ )
__all__ = ["USER_AGENT"]
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py
index 84048fa47b..ec4e3b8b7d 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/http11.py
@@ -8,14 +8,12 @@ from typing import Callable, Generator, Optional
from . import datastructures, exceptions
-# Maximum total size of headers is around 256 * 4 KiB = 1 MiB
-MAX_HEADERS = 256
+# Maximum total size of headers is around 128 * 8 KiB = 1 MiB.
+MAX_HEADERS = 128
-# We can use the same limit for the request line and header lines:
-# "GET <4096 bytes> HTTP/1.1\r\n" = 4111 bytes
-# "Set-Cookie: <4097 bytes>\r\n" = 4111 bytes
-# (RFC requires 4096 bytes; for some reason Firefox supports 4097 bytes.)
-MAX_LINE = 4111
+# Limit request line and header lines. 8KiB is the most common default
+# configuration of popular HTTP servers.
+MAX_LINE = 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.
@@ -70,7 +68,7 @@ class Request:
def exception(self) -> Optional[Exception]: # pragma: no cover
warnings.warn(
"Request.exception is deprecated; "
- "use ServerConnection.handshake_exc instead",
+ "use ServerProtocol.handshake_exc instead",
DeprecationWarning,
)
return self._exception
@@ -174,7 +172,7 @@ class Response:
def exception(self) -> Optional[Exception]: # pragma: no cover
warnings.warn(
"Response.exception is deprecated; "
- "use ClientConnection.handshake_exc instead",
+ "use ClientProtocol.handshake_exc instead",
DeprecationWarning,
)
return self._exception
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py
new file mode 100644
index 0000000000..8264094f5b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py
@@ -0,0 +1,265 @@
+# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py
+# Licensed under the Apache License (Apache-2.0)
+
+import asyncio
+import enum
+import sys
+import warnings
+from types import TracebackType
+from typing import Optional, Type
+
+
+# From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py
+# Licensed under the Python Software Foundation License (PSF-2.0)
+
+if sys.version_info >= (3, 11):
+ from typing import final
+else:
+ # @final exists in 3.8+, but we backport it for all versions
+ # before 3.11 to keep support for the __final__ attribute.
+ # See https://bugs.python.org/issue46342
+ def final(f):
+ """This decorator can be used to indicate to type checkers that
+ the decorated method cannot be overridden, and decorated class
+ cannot be subclassed. For example:
+
+ class Base:
+ @final
+ def done(self) -> None:
+ ...
+ class Sub(Base):
+ def done(self) -> None: # Error reported by type checker
+ ...
+ @final
+ class Leaf:
+ ...
+ class Other(Leaf): # Error reported by type checker
+ ...
+
+ There is no runtime checking of these properties. The decorator
+ sets the ``__final__`` attribute to ``True`` on the decorated object
+ to allow runtime introspection.
+ """
+ try:
+ f.__final__ = True
+ except (AttributeError, TypeError):
+ # Skip the attribute silently if it is not writable.
+ # AttributeError happens if the object has __slots__ or a
+ # read-only property, TypeError if it's a builtin class.
+ pass
+ return f
+
+
+# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py
+
+__version__ = "4.0.2"
+
+
+__all__ = ("timeout", "timeout_at", "Timeout")
+
+
+def timeout(delay: Optional[float]) -> "Timeout":
+ """timeout context manager.
+
+ Useful in cases when you want to apply timeout logic around block
+ of code or in cases when asyncio.wait_for is not suitable. For example:
+
+ >>> async with timeout(0.001):
+ ... async with aiohttp.get('https://github.com') as r:
+ ... await r.text()
+
+
+ delay - value in seconds or None to disable timeout logic
+ """
+ loop = asyncio.get_running_loop()
+ if delay is not None:
+ deadline = loop.time() + delay # type: Optional[float]
+ else:
+ deadline = None
+ return Timeout(deadline, loop)
+
+
+def timeout_at(deadline: Optional[float]) -> "Timeout":
+ """Schedule the timeout at absolute time.
+
+ deadline argument points on the time in the same clock system
+ as loop.time().
+
+ Please note: it is not POSIX time but a time with
+ undefined starting base, e.g. the time of the system power on.
+
+ >>> async with timeout_at(loop.time() + 10):
+ ... async with aiohttp.get('https://github.com') as r:
+ ... await r.text()
+
+
+ """
+ loop = asyncio.get_running_loop()
+ return Timeout(deadline, loop)
+
+
+class _State(enum.Enum):
+ INIT = "INIT"
+ ENTER = "ENTER"
+ TIMEOUT = "TIMEOUT"
+ EXIT = "EXIT"
+
+
+@final
+class Timeout:
+ # Internal class, please don't instantiate it directly
+ # Use timeout() and timeout_at() public factories instead.
+ #
+ # Implementation note: `async with timeout()` is preferred
+ # over `with timeout()`.
+ # While technically the Timeout class implementation
+ # doesn't need to be async at all,
+ # the `async with` statement explicitly points that
+ # the context manager should be used from async function context.
+ #
+ # This design allows to avoid many silly misusages.
+ #
+ # TimeoutError is raised immediately when scheduled
+ # if the deadline is passed.
+ # The purpose is to time out as soon as possible
+ # without waiting for the next await expression.
+
+ __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler")
+
+ def __init__(
+ self, deadline: Optional[float], loop: asyncio.AbstractEventLoop
+ ) -> None:
+ self._loop = loop
+ self._state = _State.INIT
+
+ self._timeout_handler = None # type: Optional[asyncio.Handle]
+ if deadline is None:
+ self._deadline = None # type: Optional[float]
+ else:
+ self.update(deadline)
+
+ def __enter__(self) -> "Timeout":
+ warnings.warn(
+ "with timeout() is deprecated, use async with timeout() instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self._do_enter()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> Optional[bool]:
+ self._do_exit(exc_type)
+ return None
+
+ async def __aenter__(self) -> "Timeout":
+ self._do_enter()
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> Optional[bool]:
+ self._do_exit(exc_type)
+ return None
+
+ @property
+ def expired(self) -> bool:
+ """Is timeout expired during execution?"""
+ return self._state == _State.TIMEOUT
+
+ @property
+ def deadline(self) -> Optional[float]:
+ return self._deadline
+
+ def reject(self) -> None:
+ """Reject scheduled timeout if any."""
+ # cancel is maybe better name but
+ # task.cancel() raises CancelledError in asyncio world.
+ if self._state not in (_State.INIT, _State.ENTER):
+ raise RuntimeError(f"invalid state {self._state.value}")
+ self._reject()
+
+ def _reject(self) -> None:
+ if self._timeout_handler is not None:
+ self._timeout_handler.cancel()
+ self._timeout_handler = None
+
+ def shift(self, delay: float) -> None:
+ """Advance timeout on delay seconds.
+
+ The delay can be negative.
+
+ Raise RuntimeError if shift is called when deadline is not scheduled
+ """
+ deadline = self._deadline
+ if deadline is None:
+ raise RuntimeError("cannot shift timeout if deadline is not scheduled")
+ self.update(deadline + delay)
+
+ def update(self, deadline: float) -> None:
+ """Set deadline to absolute value.
+
+ deadline argument points on the time in the same clock system
+ as loop.time().
+
+ If new deadline is in the past the timeout is raised immediately.
+
+ Please note: it is not POSIX time but a time with
+ undefined starting base, e.g. the time of the system power on.
+ """
+ if self._state == _State.EXIT:
+ raise RuntimeError("cannot reschedule after exit from context manager")
+ if self._state == _State.TIMEOUT:
+ raise RuntimeError("cannot reschedule expired timeout")
+ if self._timeout_handler is not None:
+ self._timeout_handler.cancel()
+ self._deadline = deadline
+ if self._state != _State.INIT:
+ self._reschedule()
+
+ def _reschedule(self) -> None:
+ assert self._state == _State.ENTER
+ deadline = self._deadline
+ if deadline is None:
+ return
+
+ now = self._loop.time()
+ if self._timeout_handler is not None:
+ self._timeout_handler.cancel()
+
+ task = asyncio.current_task()
+ if deadline <= now:
+ self._timeout_handler = self._loop.call_soon(self._on_timeout, task)
+ else:
+ self._timeout_handler = self._loop.call_at(deadline, self._on_timeout, task)
+
+ def _do_enter(self) -> None:
+ if self._state != _State.INIT:
+ raise RuntimeError(f"invalid state {self._state.value}")
+ self._state = _State.ENTER
+ self._reschedule()
+
+ def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None:
+ if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT:
+ self._timeout_handler = None
+ raise asyncio.TimeoutError
+ # timeout has not expired
+ self._state = _State.EXIT
+ self._reject()
+ return None
+
+ def _on_timeout(self, task: "asyncio.Task[None]") -> None:
+ task.cancel()
+ self._state = _State.TIMEOUT
+ # drop the reference early
+ self._timeout_handler = None
+
+
+# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py
index 8825c14ecf..d3425836e1 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/auth.py
@@ -67,7 +67,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol):
Returns:
bool: :obj:`True` if the handshake should continue;
- :obj:`False` if it should fail with a HTTP 401 error.
+ :obj:`False` if it should fail with an HTTP 401 error.
"""
if self._check_credentials is not None:
@@ -81,7 +81,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol):
request_headers: Headers,
) -> Optional[HTTPResponse]:
"""
- Check HTTP Basic Auth and return a HTTP 401 response if needed.
+ Check HTTP Basic Auth and return an HTTP 401 response if needed.
"""
try:
@@ -118,8 +118,8 @@ def basic_auth_protocol_factory(
realm: Optional[str] = None,
credentials: Optional[Union[Credentials, Iterable[Credentials]]] = None,
check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None,
- create_protocol: Optional[Callable[[Any], BasicAuthWebSocketServerProtocol]] = None,
-) -> Callable[[Any], BasicAuthWebSocketServerProtocol]:
+ create_protocol: Optional[Callable[..., BasicAuthWebSocketServerProtocol]] = None,
+) -> Callable[..., BasicAuthWebSocketServerProtocol]:
"""
Protocol factory that enforces HTTP Basic Auth.
@@ -135,20 +135,20 @@ def basic_auth_protocol_factory(
)
Args:
- realm: indicates the scope of protection. It should contain only ASCII
- characters because the encoding of non-ASCII characters is
- undefined. Refer to section 2.2 of :rfc:`7235` for details.
- credentials: defines hard coded authorized credentials. It can be a
+ realm: Scope of protection. It should contain only ASCII characters
+ because the encoding of non-ASCII characters is undefined.
+ Refer to section 2.2 of :rfc:`7235` for details.
+ credentials: Hard coded authorized credentials. It can be a
``(username, password)`` pair or a list of such pairs.
- check_credentials: defines a coroutine that verifies credentials.
- This coroutine receives ``username`` and ``password`` arguments
+ check_credentials: Coroutine that verifies credentials.
+ It receives ``username`` and ``password`` arguments
and returns a :class:`bool`. One of ``credentials`` or
``check_credentials`` must be provided but not both.
- create_protocol: factory that creates the protocol. By default, this
+ create_protocol: Factory that creates the protocol. By default, this
is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced
by a subclass.
Raises:
- TypeError: if the ``credentials`` or ``check_credentials`` argument is
+ TypeError: If the ``credentials`` or ``check_credentials`` argument is
wrong.
"""
@@ -175,11 +175,7 @@ def basic_auth_protocol_factory(
return hmac.compare_digest(expected_password, password)
if create_protocol is None:
- # Not sure why mypy cannot figure this out.
- create_protocol = cast(
- Callable[[Any], BasicAuthWebSocketServerProtocol],
- BasicAuthWebSocketServerProtocol,
- )
+ create_protocol = BasicAuthWebSocketServerProtocol
return functools.partial(
create_protocol,
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py
index fadc3efe87..48622523ee 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/client.py
@@ -44,6 +44,7 @@ from ..headers import (
from ..http import USER_AGENT
from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol
from ..uri import WebSocketURI, parse_uri
+from .compatibility import asyncio_timeout
from .handshake import build_request, check_response
from .http import read_response
from .protocol import WebSocketCommonProtocol
@@ -65,12 +66,13 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
await process(message)
The iterator exits normally when the connection is closed with close code
- 1000 (OK) or 1001 (going away). It raises
+ 1000 (OK) or 1001 (going away) or without a close code. It raises
a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection
is closed with any other code.
See :func:`connect` for the documentation of ``logger``, ``origin``,
- ``extensions``, ``subprotocols``, and ``extra_headers``.
+ ``extensions``, ``subprotocols``, ``extra_headers``, and
+ ``user_agent_header``.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -89,6 +91,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
extensions: Optional[Sequence[ClientExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
**kwargs: Any,
) -> None:
if logger is None:
@@ -98,6 +101,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
self.available_extensions = extensions
self.available_subprotocols = subprotocols
self.extra_headers = extra_headers
+ self.user_agent_header = user_agent_header
def write_http_request(self, path: str, headers: Headers) -> None:
"""
@@ -127,16 +131,12 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
after this coroutine returns.
Raises:
- InvalidMessage: if the HTTP message is malformed or isn't an
+ InvalidMessage: If the HTTP message is malformed or isn't an
HTTP/1.1 GET response.
"""
try:
status_code, reason, headers = await read_response(self.reader)
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError: # pragma: no cover
- raise
except Exception as exc:
raise InvalidMessage("did not receive a valid HTTP response") from exc
@@ -185,7 +185,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Extensions")
if header_values:
-
if available_extensions is None:
raise InvalidHandshake("no extensions supported")
@@ -194,9 +193,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
)
for name, response_params in parsed_header_values:
-
for extension_factory in available_extensions:
-
# Skip non-matching extensions based on their name.
if extension_factory.name != name:
continue
@@ -242,7 +239,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Protocol")
if header_values:
-
if available_subprotocols is None:
raise InvalidHandshake("no subprotocols supported")
@@ -274,15 +270,15 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
Args:
wsuri: URI of the WebSocket server.
- origin: value of the ``Origin`` header.
- available_extensions: list of supported extensions, in order in
- which they should be tried.
- available_subprotocols: list of supported subprotocols, in order
- of decreasing preference.
- extra_headers: arbitrary HTTP headers to add to the request.
+ origin: Value of the ``Origin`` header.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
+ preference.
+ extra_headers: Arbitrary HTTP headers to add to the handshake request.
Raises:
- InvalidHandshake: if the handshake fails.
+ InvalidHandshake: If the handshake fails.
"""
request_headers = Headers()
@@ -315,7 +311,8 @@ class WebSocketClientProtocol(WebSocketCommonProtocol):
if self.extra_headers is not None:
request_headers.update(self.extra_headers)
- request_headers.setdefault("User-Agent", USER_AGENT)
+ if self.user_agent_header is not None:
+ request_headers.setdefault("User-Agent", self.user_agent_header)
self.write_http_request(wsuri.resource_name, request_headers)
@@ -376,25 +373,26 @@ class Connect:
Args:
uri: URI of the WebSocket server.
- create_protocol: factory for the :class:`asyncio.Protocol` managing
- the connection; defaults to :class:`WebSocketClientProtocol`; may
- be set to a wrapper or a subclass to customize connection handling.
- logger: logger for this connection;
- defaults to ``logging.getLogger("websockets.client")``;
- see the :doc:`logging guide <../topics/logging>` for details.
- compression: shortcut that enables the "permessage-deflate" extension
- by default; may be set to :obj:`None` to disable compression;
- see the :doc:`compression guide <../topics/compression>` for details.
- origin: value of the ``Origin`` header. This is useful when connecting
- to a server that validates the ``Origin`` header to defend against
- Cross-Site WebSocket Hijacking attacks.
- extensions: list of supported extensions, in order in which they
- should be tried.
- subprotocols: list of supported subprotocols, in order of decreasing
+ create_protocol: Factory for the :class:`asyncio.Protocol` managing
+ the connection. It defaults to :class:`WebSocketClientProtocol`.
+ Set it to a wrapper or a subclass to customize connection handling.
+ logger: Logger for this client.
+ It defaults to ``logging.getLogger("websockets.client")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ origin: Value of the ``Origin`` header, for servers that require it.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
preference.
- extra_headers: arbitrary HTTP headers to add to the request.
- open_timeout: timeout for opening the connection in seconds;
- :obj:`None` to disable the timeout
+ extra_headers: Arbitrary HTTP headers to add to the handshake request.
+ user_agent_header: Value of the ``User-Agent`` request header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``.
+ Setting it to :obj:`None` removes the header.
+ open_timeout: Timeout for opening the connection in seconds.
+ :obj:`None` disables the timeout.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -415,13 +413,11 @@ class Connect:
the TCP connection. The host name from ``uri`` is still used in the TLS
handshake for secure connections and in the ``Host`` header.
- Returns:
- WebSocketClientProtocol: WebSocket connection.
-
Raises:
- InvalidURI: if ``uri`` isn't a valid WebSocket URI.
- InvalidHandshake: if the opening handshake fails.
- ~asyncio.TimeoutError: if the opening handshake times out.
+ InvalidURI: If ``uri`` isn't a valid WebSocket URI.
+ OSError: If the TCP connection fails.
+ InvalidHandshake: If the opening handshake fails.
+ ~asyncio.TimeoutError: If the opening handshake times out.
"""
@@ -431,13 +427,14 @@ class Connect:
self,
uri: str,
*,
- create_protocol: Optional[Callable[[Any], WebSocketClientProtocol]] = None,
+ create_protocol: Optional[Callable[..., WebSocketClientProtocol]] = None,
logger: Optional[LoggerLike] = None,
compression: Optional[str] = "deflate",
origin: Optional[Origin] = None,
extensions: Optional[Sequence[ClientExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
open_timeout: Optional[float] = 10,
ping_interval: Optional[float] = 20,
ping_timeout: Optional[float] = 20,
@@ -503,6 +500,7 @@ class Connect:
extensions=extensions,
subprotocols=subprotocols,
extra_headers=extra_headers,
+ user_agent_header=user_agent_header,
ping_interval=ping_interval,
ping_timeout=ping_timeout,
close_timeout=close_timeout,
@@ -530,6 +528,8 @@ class Connect:
else:
# If sock is given, host and port shouldn't be specified.
host, port = None, None
+ if kwargs.get("ssl"):
+ kwargs.setdefault("server_hostname", wsuri.host)
# If host and port are given, override values from the URI.
host = kwargs.pop("host", host)
port = kwargs.pop("port", port)
@@ -597,10 +597,6 @@ class Connect:
try:
async with self as protocol:
yield protocol
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError: # pragma: no cover
- raise
except Exception:
# Add a random initial delay between 0 and 5 seconds.
# See 7.2.3. Recovering from Abnormal Closure in RFC 6544.
@@ -647,13 +643,13 @@ class Connect:
return self.__await_impl_timeout__().__await__()
async def __await_impl_timeout__(self) -> WebSocketClientProtocol:
- return await asyncio.wait_for(self.__await_impl__(), self.open_timeout)
+ async with asyncio_timeout(self.open_timeout):
+ return await self.__await_impl__()
async def __await_impl__(self) -> WebSocketClientProtocol:
for redirects in range(self.MAX_REDIRECTS_ALLOWED):
- transport, protocol = await self._create_connection()
- protocol = cast(WebSocketClientProtocol, protocol)
-
+ _transport, _protocol = await self._create_connection()
+ protocol = cast(WebSocketClientProtocol, _protocol)
try:
await protocol.handshake(
self._wsuri,
@@ -701,7 +697,7 @@ def unix_connect(
It's mainly useful for debugging servers listening on Unix sockets.
Args:
- path: file system path to the Unix socket.
+ path: File system path to the Unix socket.
uri: URI of the WebSocket server; the host is used in the TLS
handshake for secure connections and in the ``Host`` header.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py
index df81de9dbc..6bd01e70de 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py
@@ -1,13 +1,12 @@
from __future__ import annotations
-import asyncio
import sys
-from typing import Any, Dict
-def loop_if_py_lt_38(loop: asyncio.AbstractEventLoop) -> Dict[str, Any]:
- """
- Helper for the removal of the loop argument in Python 3.10.
+__all__ = ["asyncio_timeout"]
- """
- return {"loop": loop} if sys.version_info[:2] < (3, 8) else {}
+
+if sys.version_info[:2] >= (3, 11):
+ from asyncio import timeout as asyncio_timeout # noqa: F401
+else:
+ from .async_timeout import timeout as asyncio_timeout # noqa: F401
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py
index c4de7eb28b..b77b869e3f 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/framing.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import dataclasses
import struct
from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence, Tuple
@@ -10,12 +9,11 @@ from ..exceptions import PayloadTooBig, ProtocolError
try:
from ..speedups import apply_mask
-except ImportError: # pragma: no cover
+except ImportError:
from ..utils import apply_mask
class Frame(NamedTuple):
-
fin: bool
opcode: frames.Opcode
data: bytes
@@ -53,16 +51,16 @@ class Frame(NamedTuple):
Read a WebSocket frame.
Args:
- reader: coroutine that reads exactly the requested number of
+ reader: Coroutine that reads exactly the requested number of
bytes, unless the end of file is reached.
- mask: whether the frame should be masked i.e. whether the read
+ mask: Whether the frame should be masked i.e. whether the read
happens on the server side.
- max_size: maximum payload size in bytes.
- extensions: list of extensions, applied in reverse order.
+ max_size: Maximum payload size in bytes.
+ extensions: List of extensions, applied in reverse order.
Raises:
- PayloadTooBig: if the frame exceeds ``max_size``.
- ProtocolError: if the frame contains incorrect values.
+ PayloadTooBig: If the frame exceeds ``max_size``.
+ ProtocolError: If the frame contains incorrect values.
"""
@@ -130,14 +128,14 @@ class Frame(NamedTuple):
Write a WebSocket frame.
Args:
- frame: frame to write.
- write: function that writes bytes.
- mask: whether the frame should be masked i.e. whether the write
+ frame: Frame to write.
+ write: Function that writes bytes.
+ mask: Whether the frame should be masked i.e. whether the write
happens on the client side.
- extensions: list of extensions, applied in order.
+ extensions: List of extensions, applied in order.
Raises:
- ProtocolError: if the frame contains incorrect values.
+ ProtocolError: If the frame contains incorrect values.
"""
# The frame is written in a single call to write in order to prevent
@@ -147,8 +145,11 @@ class Frame(NamedTuple):
# Backwards compatibility with previously documented public APIs
-
-from ..frames import Close, prepare_ctrl as encode_data, prepare_data # noqa
+from ..frames import ( # noqa: E402, F401, I001
+ Close,
+ prepare_ctrl as encode_data,
+ prepare_data,
+)
def parse_close(data: bytes) -> Tuple[int, str]:
@@ -156,14 +157,15 @@ def parse_close(data: bytes) -> Tuple[int, str]:
Parse the payload from a close frame.
Returns:
- Tuple[int, str]: close code and reason.
+ Close code and reason.
Raises:
- ProtocolError: if data is ill-formed.
- UnicodeDecodeError: if the reason isn't valid UTF-8.
+ ProtocolError: If data is ill-formed.
+ UnicodeDecodeError: If the reason isn't valid UTF-8.
"""
- return dataclasses.astuple(Close.parse(data)) # type: ignore
+ close = Close.parse(data)
+ return close.code, close.reason
def serialize_close(code: int, reason: str) -> bytes:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py
index 569937bb9a..ad8faf0404 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py
@@ -21,7 +21,7 @@ def build_request(headers: Headers) -> str:
Update request headers passed in argument.
Args:
- headers: handshake request headers.
+ headers: Handshake request headers.
Returns:
str: ``key`` that must be passed to :func:`check_response`.
@@ -45,14 +45,14 @@ def check_request(headers: Headers) -> str:
the responsibility of the caller.
Args:
- headers: handshake request headers.
+ headers: Handshake request headers.
Returns:
str: ``key`` that must be passed to :func:`build_response`.
Raises:
- InvalidHandshake: if the handshake request is invalid;
- then the server must return 400 Bad Request error.
+ InvalidHandshake: If the handshake request is invalid.
+ Then, the server must return a 400 Bad Request error.
"""
connection: List[ConnectionOption] = sum(
@@ -110,8 +110,8 @@ def build_response(headers: Headers, key: str) -> None:
Update response headers passed in argument.
Args:
- headers: handshake response headers.
- key: returned by :func:`check_request`.
+ headers: Handshake response headers.
+ key: Returned by :func:`check_request`.
"""
headers["Upgrade"] = "websocket"
@@ -128,11 +128,11 @@ def check_response(headers: Headers, key: str) -> None:
the caller.
Args:
- headers: handshake response headers.
- key: returned by :func:`build_request`.
+ headers: Handshake response headers.
+ key: Returned by :func:`build_request`.
Raises:
- InvalidHandshake: if the handshake response is invalid.
+ InvalidHandshake: If the handshake response is invalid.
"""
connection: List[ConnectionOption] = sum(
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py
index cc2ef1f067..2ac7f7092d 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/http.py
@@ -10,8 +10,8 @@ from ..exceptions import SecurityError
__all__ = ["read_request", "read_response"]
-MAX_HEADERS = 256
-MAX_LINE = 4110
+MAX_HEADERS = 128
+MAX_LINE = 8192
def d(value: bytes) -> str:
@@ -56,12 +56,12 @@ async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]:
body, it may be read from ``stream`` after this coroutine returns.
Args:
- stream: input to read the request from
+ stream: Input to read the request from.
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
+ 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://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1
@@ -103,12 +103,12 @@ async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers
body, it may be read from ``stream`` after this coroutine returns.
Args:
- stream: input to read the response from
+ stream: Input to read the response from.
Raises:
- EOFError: if the connection is closed without a full HTTP response
- SecurityError: if the response exceeds a security limit
- ValueError: if the response isn't well formatted
+ EOFError: If the connection is closed without a full HTTP response.
+ SecurityError: If the response exceeds a security limit.
+ ValueError: If the response isn't well formatted.
"""
# https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2
@@ -192,7 +192,7 @@ async def read_line(stream: asyncio.StreamReader) -> bytes:
"""
# Security: this is bounded by the StreamReader's limit (default = 32 KiB).
line = await stream.readline()
- # Security: this guarantees header values are small (hard-coded = 4 KiB)
+ # Security: this guarantees header values are small (hard-coded = 8 KiB)
if len(line) > MAX_LINE:
raise SecurityError("line too long")
# Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py
index 3f734fe760..19cee0e652 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py
@@ -7,6 +7,8 @@ import logging
import random
import ssl
import struct
+import sys
+import time
import uuid
import warnings
from typing import (
@@ -14,17 +16,18 @@ from typing import (
AsyncIterable,
AsyncIterator,
Awaitable,
+ Callable,
Deque,
Dict,
Iterable,
List,
Mapping,
Optional,
+ Tuple,
Union,
cast,
)
-from ..connection import State
from ..datastructures import Headers
from ..exceptions import (
ConnectionClosed,
@@ -44,12 +47,14 @@ from ..frames import (
OP_PONG,
OP_TEXT,
Close,
+ CloseCode,
Opcode,
prepare_ctrl,
prepare_data,
)
+from ..protocol import State
from ..typing import Data, LoggerLike, Subprotocol
-from .compatibility import loop_if_py_lt_38
+from .compatibility import asyncio_timeout
from .framing import Frame
@@ -76,38 +81,38 @@ class WebSocketCommonProtocol(asyncio.Protocol):
simplicity.
Once the connection is open, a Ping_ frame is sent every ``ping_interval``
- seconds. This serves as a keepalive. It helps keeping the connection
- open, especially in the presence of proxies with short timeouts on
- inactive connections. Set ``ping_interval`` to :obj:`None` to disable
- this behavior.
+ seconds. This serves as a keepalive. It helps keeping the connection open,
+ especially in the presence of proxies with short timeouts on inactive
+ connections. Set ``ping_interval`` to :obj:`None` to disable this behavior.
.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
If the corresponding Pong_ frame isn't received within ``ping_timeout``
- seconds, the connection is considered unusable and is closed with code
- 1011. This ensures that the remote endpoint remains responsive. Set
+ seconds, the connection is considered unusable and is closed with code 1011.
+ This ensures that the remote endpoint remains responsive. Set
``ping_timeout`` to :obj:`None` to disable this behavior.
.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+ See the discussion of :doc:`timeouts <../../topics/timeouts>` for details.
+
The ``close_timeout`` parameter defines a maximum wait time for completing
the closing handshake and terminating the TCP connection. For legacy
reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds
for clients and ``4 * close_timeout`` for servers.
- See the discussion of :doc:`timeouts <../topics/timeouts>` for details.
-
- ``close_timeout`` needs to be a parameter of the protocol because
- websockets usually calls :meth:`close` implicitly upon exit:
+ ``close_timeout`` is a parameter of the protocol because websockets usually
+ calls :meth:`close` implicitly upon exit:
- * on the client side, when :func:`~websockets.client.connect` is used as a
+ * on the client side, when using :func:`~websockets.client.connect` as a
context manager;
- * on the server side, when the connection handler terminates;
+ * on the server side, when the connection handler terminates.
- To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`.
+ To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or
+ :func:`~asyncio.wait_for`.
The ``max_size`` parameter enforces the maximum size for incoming messages
- in bytes. The default value is 1 MiB. If a larger message is received,
+ in bytes. The default value is 1 MiB. If a larger message is received,
:meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError`
and the connection will be closed with code 1009.
@@ -124,38 +129,38 @@ class WebSocketCommonProtocol(asyncio.Protocol):
Since Python can use up to 4 bytes of memory to represent a single
character, each connection may use up to ``4 * max_size * max_queue``
- bytes of memory to store incoming messages. By default, this is 128 MiB.
+ bytes of memory to store incoming messages. By default, this is 128 MiB.
You may want to lower the limits, depending on your application's
requirements.
The ``read_limit`` argument sets the high-water limit of the buffer for
incoming bytes. The low-water limit is half the high-water limit. The
- default value is 64 KiB, half of asyncio's default (based on the current
+ default value is 64 KiB, half of asyncio's default (based on the current
implementation of :class:`~asyncio.StreamReader`).
The ``write_limit`` argument sets the high-water limit of the buffer for
outgoing bytes. The low-water limit is a quarter of the high-water limit.
- The default value is 64 KiB, equal to asyncio's default (based on the
+ The default value is 64 KiB, equal to asyncio's default (based on the
current implementation of ``FlowControlMixin``).
- See the discussion of :doc:`memory usage <../topics/memory>` for details.
+ See the discussion of :doc:`memory usage <../../topics/memory>` for details.
Args:
- logger: logger for this connection;
- defaults to ``logging.getLogger("websockets.protocol")``;
- see the :doc:`logging guide <../topics/logging>` for details.
- ping_interval: delay between keepalive pings in seconds;
- :obj:`None` to disable keepalive pings.
- ping_timeout: timeout for keepalive pings in seconds;
- :obj:`None` to disable timeouts.
- close_timeout: timeout for closing the connection in seconds;
- for legacy reasons, the actual timeout is 4 or 5 times larger.
- max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
- max_queue: maximum number of incoming messages in receive buffer;
- :obj:`None` to disable the limit.
- read_limit: high-water mark of read buffer in bytes.
- write_limit: high-water mark of write buffer in bytes.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.protocol")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ ping_interval: Delay between keepalive pings in seconds.
+ :obj:`None` disables keepalive pings.
+ ping_timeout: Timeout for keepalive pings in seconds.
+ :obj:`None` disables timeouts.
+ close_timeout: Timeout for closing the connection in seconds.
+ For legacy reasons, the actual timeout is 4 or 5 times larger.
+ max_size: Maximum size of incoming messages in bytes.
+ :obj:`None` disables the limit.
+ max_queue: Maximum number of incoming messages in receive buffer.
+ :obj:`None` disables the limit.
+ read_limit: High-water mark of read buffer in bytes.
+ write_limit: High-water mark of write buffer in bytes.
"""
@@ -217,8 +222,6 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Logger or LoggerAdapter for this connection.
if logger is None:
logger = logging.getLogger("websockets.protocol")
- # https://github.com/python/typeshed/issues/5561
- logger = cast(logging.Logger, logger)
self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self})
"""Logger for this connection."""
@@ -242,7 +245,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self._paused = False
self._drain_waiter: Optional[asyncio.Future[None]] = None
- self._drain_lock = asyncio.Lock(**loop_if_py_lt_38(loop))
+ self._drain_lock = asyncio.Lock()
# This class implements the data transfer and closing handshake, which
# are shared between the client-side and the server-side.
@@ -285,7 +288,19 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self._fragmented_message_waiter: Optional[asyncio.Future[None]] = None
# Mapping of ping IDs to pong waiters, in chronological order.
- self.pings: Dict[bytes, asyncio.Future[None]] = {}
+ self.pings: Dict[bytes, Tuple[asyncio.Future[float], float]] = {}
+
+ self.latency: float = 0
+ """
+ Latency of the connection, in seconds.
+
+ This value is updated after sending a ping frame and receiving a
+ matching pong frame. Before the first ping, :attr:`latency` is ``0``.
+
+ By default, websockets enables a :ref:`keepalive <keepalive>` mechanism
+ that sends ping frames automatically at regular intervals. You can also
+ send ping frames and measure latency with :meth:`ping`.
+ """
# Task running the data transfer.
self.transfer_data_task: asyncio.Task[None]
@@ -325,7 +340,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# write(...); yield from drain()
# in a loop would never call connection_lost(), so it
# would not see an error when the socket is closed.
- await asyncio.sleep(0, **loop_if_py_lt_38(self.loop))
+ await asyncio.sleep(0)
await self._drain_helper()
def connection_open(self) -> None:
@@ -445,7 +460,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if self.state is not State.CLOSED:
return None
elif self.close_rcvd is None:
- return 1006
+ return CloseCode.ABNORMAL_CLOSURE
else:
return self.close_rcvd.code
@@ -471,10 +486,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
"""
Iterate on incoming messages.
- The iterator exits normally when the connection is closed with the
- close code 1000 (OK) or 1001(going away). It raises
- a :exc:`~websockets.exceptions.ConnectionClosedError` exception when
- the connection is closed with any other code.
+ The iterator exits normally when the connection is closed with the close
+ code 1000 (OK) or 1001 (going away) or without a close code.
+
+ It raises a :exc:`~websockets.exceptions.ConnectionClosedError`
+ exception when the connection is closed with any other code.
"""
try:
@@ -488,8 +504,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
Receive the next message.
When the connection is closed, :meth:`recv` raises
- :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it
- raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises
+ :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
connection closure and
:exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
error or a network failure. This is how you detect the end of the
@@ -498,8 +514,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
Canceling :meth:`recv` is safe. There's no risk of losing the next
message. The next invocation of :meth:`recv` will return it.
- This makes it possible to enforce a timeout by wrapping :meth:`recv`
- in :func:`~asyncio.wait_for`.
+ This makes it possible to enforce a timeout by wrapping :meth:`recv` in
+ :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`.
Returns:
Data: A string (:class:`str`) for a Text_ frame. A bytestring
@@ -509,8 +525,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
.. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
Raises:
- ConnectionClosed: when the connection is closed.
- RuntimeError: if two coroutines call :meth:`recv` concurrently.
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If two coroutines call :meth:`recv` concurrently.
"""
if self._pop_message_waiter is not None:
@@ -536,7 +552,6 @@ class WebSocketCommonProtocol(asyncio.Protocol):
await asyncio.wait(
[pop_message_waiter, self.transfer_data_task],
return_when=asyncio.FIRST_COMPLETED,
- **loop_if_py_lt_38(self.loop),
)
finally:
self._pop_message_waiter = None
@@ -613,8 +628,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
to send.
Raises:
- ConnectionClosed: when the connection is closed.
- TypeError: if ``message`` doesn't have a supported type.
+ ConnectionClosed: When the connection is closed.
+ TypeError: If ``message`` doesn't have a supported type.
"""
await self.ensure_open()
@@ -639,16 +654,15 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Fragmented message -- regular iterator.
elif isinstance(message, Iterable):
-
# Work around https://github.com/python/mypy/issues/6227
message = cast(Iterable[Data], message)
iter_message = iter(message)
try:
- message_chunk = next(iter_message)
+ fragment = next(iter_message)
except StopIteration:
return
- opcode, data = prepare_data(message_chunk)
+ opcode, data = prepare_data(fragment)
self._fragmented_message_waiter = asyncio.Future()
try:
@@ -656,8 +670,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
await self.write_frame(False, opcode, data)
# Other fragments.
- for message_chunk in iter_message:
- confirm_opcode, data = prepare_data(message_chunk)
+ for fragment in iter_message:
+ confirm_opcode, data = prepare_data(fragment)
if confirm_opcode != opcode:
raise TypeError("data contains inconsistent types")
await self.write_frame(False, OP_CONT, data)
@@ -668,7 +682,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
except (Exception, asyncio.CancelledError):
# We're half-way through a fragmented message and we can't
# complete it. This makes the connection unusable.
- self.fail_connection(1011)
+ self.fail_connection(CloseCode.INTERNAL_ERROR)
raise
finally:
@@ -678,18 +692,22 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Fragmented message -- asynchronous iterator
elif isinstance(message, AsyncIterable):
- # aiter_message = aiter(message) without aiter
- # https://github.com/python/mypy/issues/5738
- aiter_message = type(message).__aiter__(message) # type: ignore
+ # Implement aiter_message = aiter(message) without aiter
+ # Work around https://github.com/python/mypy/issues/5738
+ aiter_message = cast(
+ Callable[[AsyncIterable[Data]], AsyncIterator[Data]],
+ type(message).__aiter__,
+ )(message)
try:
- # message_chunk = anext(aiter_message) without anext
- # https://github.com/python/mypy/issues/5738
- message_chunk = await type(aiter_message).__anext__( # type: ignore
- aiter_message
- )
+ # Implement fragment = anext(aiter_message) without anext
+ # Work around https://github.com/python/mypy/issues/5738
+ fragment = await cast(
+ Callable[[AsyncIterator[Data]], Awaitable[Data]],
+ type(aiter_message).__anext__,
+ )(aiter_message)
except StopAsyncIteration:
return
- opcode, data = prepare_data(message_chunk)
+ opcode, data = prepare_data(fragment)
self._fragmented_message_waiter = asyncio.Future()
try:
@@ -697,11 +715,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
await self.write_frame(False, opcode, data)
# Other fragments.
- # https://github.com/python/mypy/issues/5738
- # coverage reports this code as not covered, but it is
- # exercised by tests - changing it breaks the tests!
- async for message_chunk in aiter_message: # type: ignore # pragma: no cover # noqa
- confirm_opcode, data = prepare_data(message_chunk)
+ async for fragment in aiter_message:
+ confirm_opcode, data = prepare_data(fragment)
if confirm_opcode != opcode:
raise TypeError("data contains inconsistent types")
await self.write_frame(False, OP_CONT, data)
@@ -712,7 +727,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
except (Exception, asyncio.CancelledError):
# We're half-way through a fragmented message and we can't
# complete it. This makes the connection unusable.
- self.fail_connection(1011)
+ self.fail_connection(CloseCode.INTERNAL_ERROR)
raise
finally:
@@ -722,7 +737,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
else:
raise TypeError("data must be str, bytes-like, or iterable")
- async def close(self, code: int = 1000, reason: str = "") -> None:
+ async def close(
+ self,
+ code: int = CloseCode.NORMAL_CLOSURE,
+ reason: str = "",
+ ) -> None:
"""
Perform the closing handshake.
@@ -747,19 +766,16 @@ class WebSocketCommonProtocol(asyncio.Protocol):
"""
try:
- await asyncio.wait_for(
- self.write_close_frame(Close(code, reason)),
- self.close_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.close_timeout):
+ await self.write_close_frame(Close(code, reason))
except asyncio.TimeoutError:
# If the close frame cannot be sent because the send buffers
# are full, the closing handshake won't complete anyway.
# Fail the connection to shut down faster.
self.fail_connection()
- # If no close frame is received within the timeout, wait_for() cancels
- # the data transfer task and raises TimeoutError.
+ # If no close frame is received within the timeout, asyncio_timeout()
+ # cancels the data transfer task and raises TimeoutError.
# If close() is called multiple times concurrently and one of these
# calls hits the timeout, the data transfer task will be canceled.
@@ -768,11 +784,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
try:
# If close() is canceled during the wait, self.transfer_data_task
# is canceled before the timeout elapses.
- await asyncio.wait_for(
- self.transfer_data_task,
- self.close_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.close_timeout):
+ await self.transfer_data_task
except (asyncio.TimeoutError, asyncio.CancelledError):
pass
@@ -798,8 +811,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
- A ping may serve as a keepalive or as a check that the remote endpoint
- received all messages up to this point
+ A ping may serve as a keepalive, as a check that the remote endpoint
+ received all messages up to this point, or to measure :attr:`latency`.
Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return
immediately, it means the write buffer is full. If you don't want to
@@ -814,18 +827,20 @@ class WebSocketCommonProtocol(asyncio.Protocol):
containing four random bytes.
Returns:
- ~asyncio.Future: A future that will be completed when the
- corresponding pong is received. You can ignore it if you
- don't intend to wait.
+ ~asyncio.Future[float]: A future that will be completed when the
+ corresponding pong is received. You can ignore it if you don't
+ intend to wait. The result of the future is the latency of the
+ connection in seconds.
::
pong_waiter = await ws.ping()
- await pong_waiter # only if you want to wait for the pong
+ # only if you want to wait for the corresponding pong
+ latency = await pong_waiter
Raises:
- ConnectionClosed: when the connection is closed.
- RuntimeError: if another ping was sent with the same data and
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If another ping was sent with the same data and
the corresponding pong wasn't received yet.
"""
@@ -842,11 +857,14 @@ class WebSocketCommonProtocol(asyncio.Protocol):
while data is None or data in self.pings:
data = struct.pack("!I", random.getrandbits(32))
- self.pings[data] = self.loop.create_future()
+ pong_waiter = self.loop.create_future()
+ # Resolution of time.monotonic() may be too low on Windows.
+ ping_timestamp = time.perf_counter()
+ self.pings[data] = (pong_waiter, ping_timestamp)
await self.write_frame(True, OP_PING, data)
- return asyncio.shield(self.pings[data])
+ return asyncio.shield(pong_waiter)
async def pong(self, data: Data = b"") -> None:
"""
@@ -861,11 +879,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
wait, you should close the connection.
Args:
- data (Data): payload of the pong; a string will be encoded to
+ data (Data): Payload of the pong. A string will be encoded to
UTF-8.
Raises:
- ConnectionClosed: when the connection is closed.
+ ConnectionClosed: When the connection is closed.
"""
await self.ensure_open()
@@ -973,7 +991,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
except ProtocolError as exc:
self.transfer_data_exc = exc
- self.fail_connection(1002)
+ self.fail_connection(CloseCode.PROTOCOL_ERROR)
except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc:
# Reading data with self.reader.readexactly may raise:
@@ -984,15 +1002,15 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# bytes are available than requested;
# - ssl.SSLError if the other side infringes the TLS protocol.
self.transfer_data_exc = exc
- self.fail_connection(1006)
+ self.fail_connection(CloseCode.ABNORMAL_CLOSURE)
except UnicodeDecodeError as exc:
self.transfer_data_exc = exc
- self.fail_connection(1007)
+ self.fail_connection(CloseCode.INVALID_DATA)
except PayloadTooBig as exc:
self.transfer_data_exc = exc
- self.fail_connection(1009)
+ self.fail_connection(CloseCode.MESSAGE_TOO_BIG)
except Exception as exc:
# This shouldn't happen often because exceptions expected under
@@ -1001,7 +1019,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self.logger.error("data transfer failed", exc_info=True)
self.transfer_data_exc = exc
- self.fail_connection(1011)
+ self.fail_connection(CloseCode.INTERNAL_ERROR)
async def read_message(self) -> Optional[Data]:
"""
@@ -1030,7 +1048,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
return frame.data.decode("utf-8") if text else frame.data
# 5.4. Fragmentation
- chunks: List[Data] = []
+ fragments: List[Data] = []
max_size = self.max_size
if text:
decoder_factory = codecs.getincrementaldecoder("utf-8")
@@ -1038,14 +1056,14 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if max_size is None:
def append(frame: Frame) -> None:
- nonlocal chunks
- chunks.append(decoder.decode(frame.data, frame.fin))
+ nonlocal fragments
+ fragments.append(decoder.decode(frame.data, frame.fin))
else:
def append(frame: Frame) -> None:
- nonlocal chunks, max_size
- chunks.append(decoder.decode(frame.data, frame.fin))
+ nonlocal fragments, max_size
+ fragments.append(decoder.decode(frame.data, frame.fin))
assert isinstance(max_size, int)
max_size -= len(frame.data)
@@ -1053,14 +1071,14 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if max_size is None:
def append(frame: Frame) -> None:
- nonlocal chunks
- chunks.append(frame.data)
+ nonlocal fragments
+ fragments.append(frame.data)
else:
def append(frame: Frame) -> None:
- nonlocal chunks, max_size
- chunks.append(frame.data)
+ nonlocal fragments, max_size
+ fragments.append(frame.data)
assert isinstance(max_size, int)
max_size -= len(frame.data)
@@ -1074,7 +1092,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
raise ProtocolError("unexpected opcode")
append(frame)
- return ("" if text else b"").join(chunks)
+ return ("" if text else b"").join(fragments)
async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]:
"""
@@ -1099,7 +1117,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
try:
# Echo the original data instead of re-serializing it with
# Close.serialize() because that fails when the close frame
- # is empty and Close.parse() synthetizes a 1005 close code.
+ # is empty and Close.parse() synthesizes a 1005 close code.
await self.write_close_frame(self.close_rcvd, frame.data)
except ConnectionClosed:
# Connection closed before we could echo the close frame.
@@ -1117,18 +1135,20 @@ class WebSocketCommonProtocol(asyncio.Protocol):
elif frame.opcode == OP_PONG:
if frame.data in self.pings:
+ pong_timestamp = time.perf_counter()
# Sending a pong for only the most recent ping is legal.
# Acknowledge all previous pings too in that case.
ping_id = None
ping_ids = []
- for ping_id, ping in self.pings.items():
+ for ping_id, (pong_waiter, ping_timestamp) in self.pings.items():
ping_ids.append(ping_id)
- if not ping.done():
- ping.set_result(None)
+ if not pong_waiter.done():
+ pong_waiter.set_result(pong_timestamp - ping_timestamp)
if ping_id == frame.data:
+ self.latency = pong_timestamp - ping_timestamp
break
- else: # pragma: no cover
- assert False, "ping_id is in self.pings"
+ else:
+ raise AssertionError("solicited pong not found in pings")
# Remove acknowledged pings from self.pings.
for ping_id in ping_ids:
del self.pings[ping_id]
@@ -1231,10 +1251,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
try:
while True:
- await asyncio.sleep(
- self.ping_interval,
- **loop_if_py_lt_38(self.loop),
- )
+ await asyncio.sleep(self.ping_interval)
# ping() raises CancelledError if the connection is closed,
# when close_connection() cancels self.keepalive_ping_task.
@@ -1247,23 +1264,18 @@ class WebSocketCommonProtocol(asyncio.Protocol):
if self.ping_timeout is not None:
try:
- await asyncio.wait_for(
- pong_waiter,
- self.ping_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.ping_timeout):
+ await pong_waiter
self.logger.debug("% received keepalive pong")
except asyncio.TimeoutError:
if self.debug:
self.logger.debug("! timed out waiting for keepalive pong")
- self.fail_connection(1011, "keepalive ping timeout")
+ self.fail_connection(
+ CloseCode.INTERNAL_ERROR,
+ "keepalive ping timeout",
+ )
break
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError:
- raise
-
except ConnectionClosed:
pass
@@ -1297,9 +1309,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# A client should wait for a TCP close from the server.
if self.is_client and hasattr(self, "transfer_data_task"):
if await self.wait_for_connection_lost():
- # Coverage marks this line as a partially executed branch.
- # I supect a bug in coverage. Ignore it for now.
- return # pragma: no cover
+ return
if self.debug:
self.logger.debug("! timed out waiting for TCP close")
@@ -1317,9 +1327,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
pass
if await self.wait_for_connection_lost():
- # Coverage marks this line as a partially executed branch.
- # I supect a bug in coverage. Ignore it for now.
- return # pragma: no cover
+ return
if self.debug:
self.logger.debug("! timed out waiting for TCP close")
@@ -1352,12 +1360,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# Abort the TCP connection. Buffers are discarded.
if self.debug:
self.logger.debug("x aborting TCP connection")
- self.transport.abort()
+ # Due to a bug in coverage, this is erroneously reported as not covered.
+ self.transport.abort() # pragma: no cover
# connection_lost() is called quickly after aborting.
- # Coverage marks this line as a partially executed branch.
- # I supect a bug in coverage. Ignore it for now.
- await self.wait_for_connection_lost() # pragma: no cover
+ await self.wait_for_connection_lost()
async def wait_for_connection_lost(self) -> bool:
"""
@@ -1369,11 +1376,8 @@ class WebSocketCommonProtocol(asyncio.Protocol):
"""
if not self.connection_lost_waiter.done():
try:
- await asyncio.wait_for(
- asyncio.shield(self.connection_lost_waiter),
- self.close_timeout,
- **loop_if_py_lt_38(self.loop),
- )
+ async with asyncio_timeout(self.close_timeout):
+ await asyncio.shield(self.connection_lost_waiter)
except asyncio.TimeoutError:
pass
# Re-check self.connection_lost_waiter.done() synchronously because
@@ -1381,7 +1385,11 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# and the moment this coroutine resumes running.
return self.connection_lost_waiter.done()
- def fail_connection(self, code: int = 1006, reason: str = "") -> None:
+ def fail_connection(
+ self,
+ code: int = CloseCode.ABNORMAL_CLOSURE,
+ reason: str = "",
+ ) -> None:
"""
7.1.7. Fail the WebSocket Connection
@@ -1412,7 +1420,7 @@ class WebSocketCommonProtocol(asyncio.Protocol):
# sent if it's CLOSING), except when failing the connection because of
# an error reading from or writing to the network.
# Don't send a close frame if the connection is broken.
- if code != 1006 and self.state is State.OPEN:
+ if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN:
close = Close(code, reason)
# Write the close frame without draining the write buffer.
@@ -1449,13 +1457,13 @@ class WebSocketCommonProtocol(asyncio.Protocol):
assert self.state is State.CLOSED
exc = self.connection_closed_exc()
- for ping in self.pings.values():
- ping.set_exception(exc)
+ for pong_waiter, _ping_timestamp in self.pings.values():
+ pong_waiter.set_exception(exc)
# If the exception is never retrieved, it will be logged when ping
# is garbage-collected. This is confusing for users.
# Given that ping is done (with an exception), canceling it does
# nothing, but it prevents logging the exception.
- ping.cancel()
+ pong_waiter.cancel()
# asyncio.Protocol methods
@@ -1496,7 +1504,6 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self.connection_lost_waiter.set_result(None)
if True: # pragma: no cover
-
# Copied from asyncio.StreamReaderProtocol
if self.reader is not None:
if exc is None:
@@ -1552,13 +1559,17 @@ class WebSocketCommonProtocol(asyncio.Protocol):
self.reader.feed_eof()
-def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> None:
+def broadcast(
+ websockets: Iterable[WebSocketCommonProtocol],
+ message: Data,
+ raise_exceptions: bool = False,
+) -> None:
"""
Broadcast a message to several WebSocket connections.
- A string (:class:`str`) is sent as a Text_ frame. A bytestring or
- bytes-like object (:class:`bytes`, :class:`bytearray`, or
- :class:`memoryview`) is sent as a Binary_ frame.
+ A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like
+ object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent
+ as a Binary_ frame.
.. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
.. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
@@ -1566,33 +1577,42 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N
:func:`broadcast` pushes the message synchronously to all connections even
if their write buffers are overflowing. There's no backpressure.
- :func:`broadcast` skips silently connections that aren't open in order to
- avoid errors on connections where the closing handshake is in progress.
-
- If you broadcast messages faster than a connection can handle them,
- messages will pile up in its write buffer until the connection times out.
- Keep low values for ``ping_interval`` and ``ping_timeout`` to prevent
- excessive memory usage by slow connections when you use :func:`broadcast`.
+ If you broadcast messages faster than a connection can handle them, messages
+ will pile up in its write buffer until the connection times out. Keep
+ ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage
+ from slow connections.
Unlike :meth:`~websockets.server.WebSocketServerProtocol.send`,
:func:`broadcast` doesn't support sending fragmented messages. Indeed,
- fragmentation is useful for sending large messages without buffering
- them in memory, while :func:`broadcast` buffers one copy per connection
- as fast as possible.
+ fragmentation is useful for sending large messages without buffering them in
+ memory, while :func:`broadcast` buffers one copy per connection as fast as
+ possible.
+
+ :func:`broadcast` skips connections that aren't open in order to avoid
+ errors on connections where the closing handshake is in progress.
+
+ :func:`broadcast` ignores failures to write the message on some connections.
+ It continues writing to other connections. On Python 3.11 and above, you
+ may set ``raise_exceptions`` to :obj:`True` to record failures and raise all
+ exceptions in a :pep:`654` :exc:`ExceptionGroup`.
Args:
- websockets (Iterable[WebSocketCommonProtocol]): WebSocket connections
- to which the message will be sent.
- message (Data): message to send.
+ websockets: WebSocket connections to which the message will be sent.
+ message: Message to send.
+ raise_exceptions: Whether to raise an exception in case of failures.
Raises:
- RuntimeError: if a connection is busy sending a fragmented message.
- TypeError: if ``message`` doesn't have a supported type.
+ TypeError: If ``message`` doesn't have a supported type.
"""
if not isinstance(message, (str, bytes, bytearray, memoryview)):
raise TypeError("data must be str or bytes-like")
+ if raise_exceptions:
+ if sys.version_info[:2] < (3, 11): # pragma: no cover
+ raise ValueError("raise_exceptions requires at least Python 3.11")
+ exceptions = []
+
opcode, data = prepare_data(message)
for websocket in websockets:
@@ -1600,6 +1620,26 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N
continue
if websocket._fragmented_message_waiter is not None:
- raise RuntimeError("busy sending a fragmented message")
+ if raise_exceptions:
+ exception = RuntimeError("sending a fragmented message")
+ exceptions.append(exception)
+ else:
+ websocket.logger.warning(
+ "skipped broadcast: sending a fragmented message",
+ )
+
+ try:
+ websocket.write_frame_sync(True, opcode, data)
+ except Exception as write_exception:
+ if raise_exceptions:
+ exception = RuntimeError("failed to write message")
+ exception.__cause__ = write_exception
+ exceptions.append(exception)
+ else:
+ websocket.logger.warning(
+ "skipped broadcast: failed to write message",
+ exc_info=True,
+ )
- websocket.write_frame_sync(True, opcode, data)
+ if raise_exceptions:
+ raise ExceptionGroup("skipped broadcast", exceptions)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py
index 3e51db1b71..7c24dd74af 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/legacy/server.py
@@ -25,7 +25,6 @@ from typing import (
cast,
)
-from ..connection import State
from ..datastructures import Headers, HeadersLike, MultipleValuesError
from ..exceptions import (
AbortHandshake,
@@ -45,8 +44,9 @@ from ..headers import (
validate_subprotocols,
)
from ..http import USER_AGENT
-from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol
-from .compatibility import loop_if_py_lt_38
+from ..protocol import State
+from ..typing import ExtensionHeader, LoggerLike, Origin, StatusLike, Subprotocol
+from .compatibility import asyncio_timeout
from .handshake import build_response, check_request
from .http import read_request
from .protocol import WebSocketCommonProtocol
@@ -57,7 +57,7 @@ __all__ = ["serve", "unix_serve", "WebSocketServerProtocol", "WebSocketServer"]
HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]]
-HTTPResponse = Tuple[http.HTTPStatus, HeadersLike, bytes]
+HTTPResponse = Tuple[StatusLike, HeadersLike, bytes]
class WebSocketServerProtocol(WebSocketCommonProtocol):
@@ -73,7 +73,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
await process(message)
The iterator exits normally when the connection is closed with close code
- 1000 (OK) or 1001 (going away). It raises
+ 1000 (OK) or 1001 (going away) or without a close code. It raises
a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection
is closed with any other code.
@@ -84,7 +84,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
ws_server: WebSocket server that created this connection.
See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``,
- ``extensions``, ``subprotocols``, and ``extra_headers``.
+ ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -108,12 +108,14 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
extensions: Optional[Sequence[ServerExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLikeOrCallable] = None,
+ server_header: Optional[str] = USER_AGENT,
process_request: Optional[
Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]]
] = None,
select_subprotocol: Optional[
Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol]
] = None,
+ open_timeout: Optional[float] = 10,
**kwargs: Any,
) -> None:
if logger is None:
@@ -132,8 +134,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
self.available_extensions = extensions
self.available_subprotocols = subprotocols
self.extra_headers = extra_headers
+ self.server_header = server_header
self._process_request = process_request
self._select_subprotocol = select_subprotocol
+ self.open_timeout = open_timeout
def connection_made(self, transport: asyncio.BaseTransport) -> None:
"""
@@ -153,22 +157,20 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
Handle the lifecycle of a WebSocket connection.
Since this method doesn't have a caller able to handle exceptions, it
- attemps to log relevant ones and guarantees that the TCP connection is
+ attempts to log relevant ones and guarantees that the TCP connection is
closed before exiting.
"""
try:
-
try:
- await self.handshake(
- origins=self.origins,
- available_extensions=self.available_extensions,
- available_subprotocols=self.available_subprotocols,
- extra_headers=self.extra_headers,
- )
- # Remove this branch when dropping support for Python < 3.8
- # because CancelledError no longer inherits Exception.
- except asyncio.CancelledError: # pragma: no cover
+ async with asyncio_timeout(self.open_timeout):
+ await self.handshake(
+ origins=self.origins,
+ available_extensions=self.available_extensions,
+ available_subprotocols=self.available_subprotocols,
+ extra_headers=self.extra_headers,
+ )
+ except asyncio.TimeoutError: # pragma: no cover
raise
except ConnectionError:
raise
@@ -216,14 +218,16 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
)
headers.setdefault("Date", email.utils.formatdate(usegmt=True))
- headers.setdefault("Server", USER_AGENT)
+ if self.server_header is not None:
+ headers.setdefault("Server", self.server_header)
+
headers.setdefault("Content-Length", str(len(body)))
headers.setdefault("Content-Type", "text/plain")
headers.setdefault("Connection", "close")
self.write_http_response(status, headers, body)
self.logger.info(
- "connection failed (%d %s)", status.value, status.phrase
+ "connection rejected (%d %s)", status.value, status.phrase
)
await self.close_transport()
return
@@ -325,9 +329,9 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
You may override this method in a :class:`WebSocketServerProtocol`
subclass, for example:
- * to return a HTTP 200 OK response on a given path; then a load
+ * to return an HTTP 200 OK response on a given path; then a load
balancer can use this path for a health check;
- * to authenticate the request and return a HTTP 401 Unauthorized or a
+ * to authenticate the request and return an HTTP 401 Unauthorized or an
HTTP 403 Forbidden when authentication fails.
You may also override this method with the ``process_request``
@@ -345,7 +349,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
request_headers: request headers.
Returns:
- Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]: :obj:`None`
+ Optional[Tuple[StatusLike, HeadersLike, bytes]]: :obj:`None`
to continue the WebSocket handshake normally.
An HTTP response, represented by a 3-uple of the response status,
@@ -439,15 +443,12 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Extensions")
if header_values and available_extensions:
-
parsed_header_values: List[ExtensionHeader] = sum(
[parse_extension(header_value) for header_value in header_values], []
)
for name, request_params in parsed_header_values:
-
for ext_factory in available_extensions:
-
# Skip non-matching extensions based on their name.
if ext_factory.name != name:
continue
@@ -499,7 +500,6 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
header_values = headers.get_all("Sec-WebSocket-Protocol")
if header_values and available_subprotocols:
-
parsed_header_values: List[Subprotocol] = sum(
[parse_subprotocol(header_value) for header_value in header_values], []
)
@@ -516,31 +516,29 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
server_subprotocols: Sequence[Subprotocol],
) -> Optional[Subprotocol]:
"""
- Pick a subprotocol among those offered by the client.
+ Pick a subprotocol among those supported by the client and the server.
- If several subprotocols are supported by the client and the server,
- the default implementation selects the preferred subprotocol by
- giving equal value to the priorities of the client and the server.
- If no subprotocol is supported by the client and the server, it
- proceeds without a subprotocol.
+ If several subprotocols are available, select the preferred subprotocol
+ by giving equal weight to the preferences of the client and the server.
- This is unlikely to be the most useful implementation in practice.
- Many servers providing a subprotocol will require that the client
- uses that subprotocol. Such rules can be implemented in a subclass.
+ If no subprotocol is available, proceed without a subprotocol.
- You may also override this method with the ``select_subprotocol``
- argument of :func:`serve` and :class:`WebSocketServerProtocol`.
+ You may provide a ``select_subprotocol`` argument to :func:`serve` or
+ :class:`WebSocketServerProtocol` to override this logic. For example,
+ you could reject the handshake if the client doesn't support a
+ particular subprotocol, rather than accept the handshake without that
+ subprotocol.
Args:
client_subprotocols: list of subprotocols offered by the client.
server_subprotocols: list of subprotocols available on the server.
Returns:
- Optional[Subprotocol]: Selected subprotocol.
+ Optional[Subprotocol]: Selected subprotocol, if a common subprotocol
+ was found.
:obj:`None` to continue without a subprotocol.
-
"""
if self._select_subprotocol is not None:
return self._select_subprotocol(client_subprotocols, server_subprotocols)
@@ -548,10 +546,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
subprotocols = set(client_subprotocols) & set(server_subprotocols)
if not subprotocols:
return None
- priority = lambda p: (
- client_subprotocols.index(p) + server_subprotocols.index(p)
- )
- return sorted(subprotocols, key=priority)[0]
+ return sorted(
+ subprotocols,
+ key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p),
+ )[0]
async def handshake(
self,
@@ -594,7 +592,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
# The connection may drop while process_request is running.
if self.state is State.CLOSED:
- raise self.connection_closed_exc() # pragma: no cover
+ # This subclass of ConnectionError is silently ignored in handler().
+ raise BrokenPipeError("connection closed during opening handshake")
# Change the response to a 503 error if the server is shutting down.
if not self.ws_server.is_serving():
@@ -635,7 +634,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol):
response_headers.update(extra_headers)
response_headers.setdefault("Date", email.utils.formatdate(usegmt=True))
- response_headers.setdefault("Server", USER_AGENT)
+ if self.server_header is not None:
+ response_headers.setdefault("Server", self.server_header)
self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers)
@@ -658,9 +658,9 @@ class WebSocketServer:
when shutting down.
Args:
- logger: logger for this server;
- defaults to ``logging.getLogger("websockets.server")``;
- see the :doc:`logging guide <../topics/logging>` for details.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.server")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
"""
@@ -707,7 +707,7 @@ class WebSocketServer:
self.logger.info("server listening on %s", name)
# Initialized here because we need a reference to the event loop.
- # This should be moved back to __init__ in Python 3.10.
+ # This should be moved back to __init__ when dropping Python < 3.10.
self.closed_waiter = server.get_loop().create_future()
def register(self, protocol: WebSocketServerProtocol) -> None:
@@ -724,26 +724,30 @@ class WebSocketServer:
"""
self.websockets.remove(protocol)
- def close(self) -> None:
+ def close(self, close_connections: bool = True) -> None:
"""
Close the server.
- This method:
+ * Close the underlying :class:`~asyncio.Server`.
+ * When ``close_connections`` is :obj:`True`, which is the default,
+ close existing connections. Specifically:
- * closes the underlying :class:`~asyncio.Server`;
- * rejects new WebSocket connections with an HTTP 503 (service
- unavailable) error; this happens when the server accepted the TCP
- connection but didn't complete the WebSocket opening handshake prior
- to closing;
- * closes open WebSocket connections with close code 1001 (going away).
+ * Reject opening WebSocket connections with an HTTP 503 (service
+ unavailable) error. This happens when the server accepted the TCP
+ connection but didn't complete the opening handshake before closing.
+ * Close open WebSocket connections with close code 1001 (going away).
+
+ * Wait until all connection handlers terminate.
:meth:`close` is idempotent.
"""
if self.close_task is None:
- self.close_task = self.get_loop().create_task(self._close())
+ self.close_task = self.get_loop().create_task(
+ self._close(close_connections)
+ )
- async def _close(self) -> None:
+ async def _close(self, close_connections: bool) -> None:
"""
Implementation of :meth:`close`.
@@ -757,36 +761,30 @@ class WebSocketServer:
# Stop accepting new connections.
self.server.close()
- # Wait until self.server.close() completes.
- await self.server.wait_closed()
-
# Wait until all accepted connections reach connection_made() and call
# register(). See https://bugs.python.org/issue34852 for details.
- await asyncio.sleep(0, **loop_if_py_lt_38(self.get_loop()))
-
- # Close OPEN connections with status code 1001. Since the server was
- # closed, handshake() closes OPENING connections with a HTTP 503
- # error. Wait until all connections are closed.
-
- close_tasks = [
- asyncio.create_task(websocket.close(1001))
- for websocket in self.websockets
- if websocket.state is not State.CONNECTING
- ]
- # asyncio.wait doesn't accept an empty first argument.
- if close_tasks:
- await asyncio.wait(
- close_tasks,
- **loop_if_py_lt_38(self.get_loop()),
- )
-
- # Wait until all connection handlers are complete.
+ await asyncio.sleep(0)
+
+ if close_connections:
+ # Close OPEN connections with close code 1001. After server.close(),
+ # handshake() closes OPENING connections with an HTTP 503 error.
+ close_tasks = [
+ asyncio.create_task(websocket.close(1001))
+ for websocket in self.websockets
+ if websocket.state is not State.CONNECTING
+ ]
+ # asyncio.wait doesn't accept an empty first argument.
+ if close_tasks:
+ await asyncio.wait(close_tasks)
+
+ # Wait until all TCP connections are closed.
+ await self.server.wait_closed()
+ # Wait until all connection handlers terminate.
# asyncio.wait doesn't accept an empty first argument.
if self.websockets:
await asyncio.wait(
- [websocket.handler_task for websocket in self.websockets],
- **loop_if_py_lt_38(self.get_loop()),
+ [websocket.handler_task for websocket in self.websockets]
)
# Tell wait_closed() to return.
@@ -829,19 +827,37 @@ class WebSocketServer:
"""
return self.server.is_serving()
- async def start_serving(self) -> None:
+ async def start_serving(self) -> None: # pragma: no cover
"""
See :meth:`asyncio.Server.start_serving`.
+ Typical use::
+
+ server = await serve(..., start_serving=False)
+ # perform additional setup here...
+ # ... then start the server
+ await server.start_serving()
+
"""
- await self.server.start_serving() # pragma: no cover
+ await self.server.start_serving()
- async def serve_forever(self) -> None:
+ async def serve_forever(self) -> None: # pragma: no cover
"""
See :meth:`asyncio.Server.serve_forever`.
+ Typical use::
+
+ server = await serve(...)
+ # this coroutine doesn't return
+ # canceling it stops the server
+ await server.serve_forever()
+
+ This is an alternative to using :func:`serve` as an asynchronous context
+ manager. Shutdown is triggered by canceling :meth:`serve_forever`
+ instead of exiting a :func:`serve` context.
+
"""
- await self.server.serve_forever() # pragma: no cover
+ await self.server.serve_forever()
@property
def sockets(self) -> Iterable[socket.socket]:
@@ -851,17 +867,17 @@ class WebSocketServer:
"""
return self.server.sockets
- async def __aenter__(self) -> WebSocketServer:
- return self # pragma: no cover
+ async def __aenter__(self) -> WebSocketServer: # pragma: no cover
+ return self
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
- ) -> None:
- self.close() # pragma: no cover
- await self.wait_closed() # pragma: no cover
+ ) -> None: # pragma: no cover
+ self.close()
+ await self.wait_closed()
class Serve:
@@ -879,53 +895,61 @@ class Serve:
server performs the closing handshake and closes the connection.
Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object
- provides :meth:`~WebSocketServer.close` and
- :meth:`~WebSocketServer.wait_closed` methods for shutting down the server.
+ provides a :meth:`~WebSocketServer.close` method to shut down the server::
- :func:`serve` can be used as an asynchronous context manager::
+ stop = asyncio.Future() # set this future to exit the server
+
+ server = await serve(...)
+ await stop
+ await server.close()
+
+ :func:`serve` can be used as an asynchronous context manager. Then, the
+ server is shut down automatically when exiting the context::
stop = asyncio.Future() # set this future to exit the server
async with serve(...):
await stop
- The server is shut down automatically when exiting the context.
-
Args:
- ws_handler: connection handler. It receives the WebSocket connection,
+ ws_handler: Connection handler. It receives the WebSocket connection,
which is a :class:`WebSocketServerProtocol`, in argument.
- host: network interfaces the server is bound to;
- see :meth:`~asyncio.loop.create_server` for details.
- port: TCP port the server listens on;
- see :meth:`~asyncio.loop.create_server` for details.
- create_protocol: factory for the :class:`asyncio.Protocol` managing
- the connection; defaults to :class:`WebSocketServerProtocol`; may
- be set to a wrapper or a subclass to customize connection handling.
- logger: logger for this server;
- defaults to ``logging.getLogger("websockets.server")``;
- see the :doc:`logging guide <../topics/logging>` for details.
- compression: shortcut that enables the "permessage-deflate" extension
- by default; may be set to :obj:`None` to disable compression;
- see the :doc:`compression guide <../topics/compression>` for details.
- origins: acceptable values of the ``Origin`` header; include
- :obj:`None` in the list if the lack of an origin is acceptable.
- This is useful for defending against Cross-Site WebSocket
- Hijacking attacks.
- extensions: list of supported extensions, in order in which they
- should be tried.
- subprotocols: list of supported subprotocols, in order of decreasing
+ host: Network interfaces the server binds to.
+ See :meth:`~asyncio.loop.create_server` for details.
+ port: TCP port the server listens on.
+ See :meth:`~asyncio.loop.create_server` for details.
+ create_protocol: Factory for the :class:`asyncio.Protocol` managing
+ the connection. It defaults to :class:`WebSocketServerProtocol`.
+ Set it to a wrapper or a subclass to customize connection handling.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.server")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ origins: Acceptable values of the ``Origin`` header, for defending
+ against Cross-Site WebSocket Hijacking attacks. Include :obj:`None`
+ in the list if the lack of an origin is acceptable.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
preference.
extra_headers (Union[HeadersLike, Callable[[str, Headers], HeadersLike]]):
- arbitrary HTTP headers to add to the request; this can be
+ Arbitrary HTTP headers to add to the response. This can be
a :data:`~websockets.datastructures.HeadersLike` or a callable
taking the request path and headers in arguments and returning
a :data:`~websockets.datastructures.HeadersLike`.
+ server_header: Value of the ``Server`` response header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``.
+ Setting it to :obj:`None` removes the header.
process_request (Optional[Callable[[str, Headers], \
- Awaitable[Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]]]]):
- intercept HTTP request before the opening handshake;
- see :meth:`~WebSocketServerProtocol.process_request` for details.
- select_subprotocol: select a subprotocol supported by the client;
- see :meth:`~WebSocketServerProtocol.select_subprotocol` for details.
+ Awaitable[Optional[Tuple[StatusLike, HeadersLike, bytes]]]]]):
+ Intercept HTTP request before the opening handshake.
+ See :meth:`~WebSocketServerProtocol.process_request` for details.
+ select_subprotocol: Select a subprotocol supported by the client.
+ See :meth:`~WebSocketServerProtocol.select_subprotocol` for details.
+ open_timeout: Timeout for opening connections in seconds.
+ :obj:`None` disables the timeout.
See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the
documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``,
@@ -955,19 +979,21 @@ class Serve:
host: Optional[Union[str, Sequence[str]]] = None,
port: Optional[int] = None,
*,
- create_protocol: Optional[Callable[[Any], WebSocketServerProtocol]] = None,
+ create_protocol: Optional[Callable[..., WebSocketServerProtocol]] = None,
logger: Optional[LoggerLike] = None,
compression: Optional[str] = "deflate",
origins: Optional[Sequence[Optional[Origin]]] = None,
extensions: Optional[Sequence[ServerExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
extra_headers: Optional[HeadersLikeOrCallable] = None,
+ server_header: Optional[str] = USER_AGENT,
process_request: Optional[
Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]]
] = None,
select_subprotocol: Optional[
Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol]
] = None,
+ open_timeout: Optional[float] = 10,
ping_interval: Optional[float] = 20,
ping_timeout: Optional[float] = 20,
close_timeout: Optional[float] = None,
@@ -1030,6 +1056,7 @@ class Serve:
host=host,
port=port,
secure=secure,
+ open_timeout=open_timeout,
ping_interval=ping_interval,
ping_timeout=ping_timeout,
close_timeout=close_timeout,
@@ -1043,6 +1070,7 @@ class Serve:
extensions=extensions,
subprotocols=subprotocols,
extra_headers=extra_headers,
+ server_header=server_header,
process_request=process_request,
select_subprotocol=select_subprotocol,
logger=logger,
@@ -1106,17 +1134,18 @@ def unix_serve(
**kwargs: Any,
) -> Serve:
"""
- Similar to :func:`serve`, but for listening on Unix sockets.
+ Start a WebSocket server listening on a Unix socket.
- This function builds upon the event
- loop's :meth:`~asyncio.loop.create_unix_server` method.
+ This function is identical to :func:`serve`, except the ``host`` and
+ ``port`` arguments are replaced by ``path``. It is only available on Unix.
- It is only available on Unix.
+ Unrecognized keyword arguments are passed the event loop's
+ :meth:`~asyncio.loop.create_unix_server` method.
It's useful for deploying a server behind a reverse proxy such as nginx.
Args:
- path: file system path to the Unix socket.
+ path: File system path to the Unix socket.
"""
return serve(ws_handler, path=path, unix=True, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py
new file mode 100644
index 0000000000..765e6b9bb4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/protocol.py
@@ -0,0 +1,708 @@
+from __future__ import annotations
+
+import enum
+import logging
+import uuid
+from typing import Generator, List, Optional, Type, Union
+
+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 LoggerLike, Origin, Subprotocol
+
+
+__all__ = [
+ "Protocol",
+ "Side",
+ "State",
+ "SEND_EOF",
+]
+
+Event = Union[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.
+ 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: Optional[int] = 2**20,
+ logger: Optional[LoggerLike] = 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.
+ self.max_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.cur_size: Optional[int] = 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: Optional[Origin] = None
+ self.extensions: List[Extension] = []
+ self.subprotocol: Optional[Subprotocol] = None
+
+ # Close code and reason, set when a close frame is sent or received.
+ self.close_rcvd: Optional[Close] = None
+ self.close_sent: Optional[Close] = None
+ self.close_rcvd_then_sent: Optional[bool] = None
+
+ # Track if an exception happened during the handshake.
+ self.handshake_exc: Optional[Exception] = 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: Optional[Exception] = None
+
+ @property
+ def state(self) -> State:
+ """
+ WebSocket connection state.
+
+ Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`.
+
+ """
+ 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) -> Optional[int]:
+ """
+ `WebSocket close code`_.
+
+ .. _WebSocket close code:
+ https://www.rfc-editor.org/rfc/rfc6455.html#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) -> Optional[str]:
+ """
+ `WebSocket close reason`_.
+
+ .. _WebSocket close reason:
+ https://www.rfc-editor.org/rfc/rfc6455.html#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) -> 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.
+
+ Raises:
+ EOFError: if :meth:`receive_eof` was called earlier.
+
+ """
+ self.reader.feed_eof()
+ next(self.parser)
+
+ # Public methods for sending events.
+
+ def send_continuation(self, data: bytes, 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")
+ self.expect_continuation_frame = not fin
+ self.send_frame(Frame(OP_CONT, data, fin))
+
+ def send_text(self, data: bytes, 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")
+ self.expect_continuation_frame = not fin
+ self.send_frame(Frame(OP_TEXT, data, fin))
+
+ def send_binary(self, data: bytes, 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")
+ self.expect_continuation_frame = not fin
+ self.send_frame(Frame(OP_BINARY, data, fin))
+
+ def send_close(self, code: Optional[int] = 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 a fragmented message is being sent, if the code
+ isn't valid, or if a reason is provided without a code
+
+ """
+ if self.expect_continuation_frame:
+ raise ProtocolError("expected a continuation frame")
+ 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()
+ # send_frame() guarantees that self.state is OPEN at this point.
+ # 7.1.3. The WebSocket Closing Handshake is Started
+ self.send_frame(Frame(OP_CLOSE, data))
+ self.close_sent = close
+ self.state = CLOSING
+
+ def send_ping(self, data: bytes) -> 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.
+
+ """
+ self.send_frame(Frame(OP_PING, data))
+
+ def send_pong(self, data: bytes) -> 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.
+
+ """
+ self.send_frame(Frame(OP_PONG, data))
+
+ def fail(self, code: 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
+ 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:
+ List[Event]: 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:
+ List[bytes]: 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:
+ bool: Whether the TCP connection is expected to close soon.
+
+ """
+ # We expect a TCP close if and only if we sent a close frame:
+ # * 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.
+ # We already got a TCP Close if and only if the state is CLOSED.
+ return self.state is CLOSING or self.handshake_exc is not None
+
+ # Private methods for receiving data.
+
+ def parse(self) -> Generator[None, None, 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")
+
+ if self.max_size is None:
+ max_size = None
+ elif self.cur_size is None:
+ max_size = self.max_size
+ else:
+ max_size = self.max_size - self.cur_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:
+ 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, None, 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).
+
+ """
+ # The server close the TCP connection in the same circumstances where
+ # discard() replaces parse(). The client closes the connection later,
+ # after the server closes the connection or a timeout elapses.
+ # (The latter case cannot be handled in this Sans-I/O layer.)
+ assert (self.side is SERVER) == (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:
+ 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.cur_size is not None:
+ raise ProtocolError("expected a continuation frame")
+ if frame.fin:
+ self.cur_size = None
+ else:
+ self.cur_size = len(frame.data)
+
+ elif frame.opcode is OP_CONT:
+ if self.cur_size is None:
+ raise ProtocolError("unexpected continuation frame")
+ if frame.fin:
+ self.cur_size = None
+ else:
+ self.cur_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.cur_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."
+ 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.state is not OPEN:
+ raise InvalidState(
+ f"cannot write to a WebSocket in the {self.state.name} state"
+ )
+
+ 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)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
index 5dad50b6a1..191660553f 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/server.py
@@ -4,9 +4,9 @@ import base64
import binascii
import email.utils
import http
-from typing import Generator, List, Optional, Sequence, Tuple, cast
+import warnings
+from typing import Any, Callable, Generator, List, Optional, Sequence, Tuple, cast
-from .connection import CONNECTING, OPEN, SERVER, Connection, State
from .datastructures import Headers, MultipleValuesError
from .exceptions import (
InvalidHandshake,
@@ -25,13 +25,14 @@ from .headers import (
parse_subprotocol,
parse_upgrade,
)
-from .http import USER_AGENT
from .http11 import Request, Response
+from .protocol import CONNECTING, OPEN, SERVER, Protocol, State
from .typing import (
ConnectionOption,
ExtensionHeader,
LoggerLike,
Origin,
+ StatusLike,
Subprotocol,
UpgradeProtocol,
)
@@ -39,13 +40,15 @@ from .utils import accept_key
# See #940 for why lazy_import isn't used here for backwards compatibility.
-from .legacy.server import * # isort:skip # noqa
+# See #1400 for why listing compatibility imports in __all__ helps PyCharm.
+from .legacy.server import * # isort:skip # noqa: I001
+from .legacy.server import __all__ as legacy__all__
-__all__ = ["ServerConnection"]
+__all__ = ["ServerProtocol"] + legacy__all__
-class ServerConnection(Connection):
+class ServerProtocol(Protocol):
"""
Sans-I/O implementation of a WebSocket server connection.
@@ -58,20 +61,31 @@ class ServerConnection(Connection):
should be tried.
subprotocols: list of supported subprotocols, in order of decreasing
preference.
+ select_subprotocol: Callback for selecting a subprotocol among
+ those supported by the client and the server. It has the same
+ signature as the :meth:`select_subprotocol` method, including a
+ :class:`ServerProtocol` instance as first argument.
state: initial state of the WebSocket connection.
max_size: maximum size of incoming messages in bytes;
- :obj:`None` to disable the limit.
+ :obj:`None` disables the limit.
logger: logger for this connection;
defaults to ``logging.getLogger("websockets.client")``;
- see the :doc:`logging guide <../topics/logging>` for details.
+ see the :doc:`logging guide <../../topics/logging>` for details.
"""
def __init__(
self,
+ *,
origins: Optional[Sequence[Optional[Origin]]] = None,
extensions: Optional[Sequence[ServerExtensionFactory]] = None,
subprotocols: Optional[Sequence[Subprotocol]] = None,
+ select_subprotocol: Optional[
+ Callable[
+ [ServerProtocol, Sequence[Subprotocol]],
+ Optional[Subprotocol],
+ ]
+ ] = None,
state: State = CONNECTING,
max_size: Optional[int] = 2**20,
logger: Optional[LoggerLike] = None,
@@ -85,6 +99,14 @@ class ServerConnection(Connection):
self.origins = origins
self.available_extensions = extensions
self.available_subprotocols = subprotocols
+ if select_subprotocol is not None:
+ # Bind select_subprotocol then shadow self.select_subprotocol.
+ # Use setattr to work around https://github.com/python/mypy/issues/2427.
+ setattr(
+ self,
+ "select_subprotocol",
+ select_subprotocol.__get__(self, self.__class__),
+ )
def accept(self, request: Request) -> Response:
"""
@@ -95,13 +117,13 @@ class ServerConnection(Connection):
You must send the handshake response with :meth:`send_response`.
- You can modify it before sending it, for example to add HTTP headers.
+ You may modify it before sending it, for example to add HTTP headers.
Args:
request: WebSocket handshake request event received from the client.
Returns:
- Response: WebSocket handshake response event to send to the client.
+ WebSocket handshake response event to send to the client.
"""
try:
@@ -145,6 +167,8 @@ class ServerConnection(Connection):
f"Failed to open a WebSocket connection: {exc}.\n",
)
except Exception as exc:
+ # Handle exceptions raised by user-provided select_subprotocol and
+ # unexpected errors.
request._exception = exc
self.handshake_exc = exc
self.logger.error("opening handshake failed", exc_info=True)
@@ -170,13 +194,12 @@ class ServerConnection(Connection):
if protocol_header is not None:
headers["Sec-WebSocket-Protocol"] = protocol_header
- headers["Server"] = USER_AGENT
-
self.logger.info("connection open")
return Response(101, "Switching Protocols", headers)
def process_request(
- self, request: Request
+ self,
+ request: Request,
) -> Tuple[str, Optional[str], Optional[str]]:
"""
Check a handshake request and negotiate extensions and subprotocol.
@@ -274,6 +297,7 @@ class ServerConnection(Connection):
Optional[Origin]: origin, if it is acceptable.
Raises:
+ InvalidHandshake: if the Origin header is invalid.
InvalidOrigin: if the origin isn't acceptable.
"""
@@ -298,8 +322,8 @@ class ServerConnection(Connection):
Accept or reject each extension proposed in the client request.
Negotiate parameters for accepted extensions.
- :rfc:`6455` leaves the rules up to the specification of each
- :extension.
+ Per :rfc:`6455`, negotiation rules are defined by the specification of
+ each extension.
To provide this level of flexibility, for each extension proposed by
the client, we check for a match with each extension available in the
@@ -324,7 +348,7 @@ class ServerConnection(Connection):
HTTP response header and list of accepted extensions.
Raises:
- InvalidHandshake: to abort the handshake with an HTTP 400 error.
+ InvalidHandshake: if the Sec-WebSocket-Extensions header is invalid.
"""
response_header_value: Optional[str] = None
@@ -335,15 +359,12 @@ class ServerConnection(Connection):
header_values = headers.get_all("Sec-WebSocket-Extensions")
if header_values and self.available_extensions:
-
parsed_header_values: List[ExtensionHeader] = sum(
[parse_extension(header_value) for header_value in header_values], []
)
for name, request_params in parsed_header_values:
-
for ext_factory in self.available_extensions:
-
# Skip non-matching extensions based on their name.
if ext_factory.name != name:
continue
@@ -384,64 +405,83 @@ class ServerConnection(Connection):
also the value of the ``Sec-WebSocket-Protocol`` response header.
Raises:
- InvalidHandshake: to abort the handshake with an HTTP 400 error.
+ InvalidHandshake: if the Sec-WebSocket-Subprotocol header is invalid.
"""
- subprotocol: Optional[Subprotocol] = None
-
- header_values = headers.get_all("Sec-WebSocket-Protocol")
-
- if header_values and self.available_subprotocols:
-
- parsed_header_values: List[Subprotocol] = sum(
- [parse_subprotocol(header_value) for header_value in header_values], []
- )
-
- subprotocol = self.select_subprotocol(
- parsed_header_values, self.available_subprotocols
- )
+ subprotocols: Sequence[Subprotocol] = sum(
+ [
+ parse_subprotocol(header_value)
+ for header_value in headers.get_all("Sec-WebSocket-Protocol")
+ ],
+ [],
+ )
- return subprotocol
+ return self.select_subprotocol(subprotocols)
def select_subprotocol(
self,
- client_subprotocols: Sequence[Subprotocol],
- server_subprotocols: Sequence[Subprotocol],
+ subprotocols: Sequence[Subprotocol],
) -> Optional[Subprotocol]:
"""
Pick a subprotocol among those offered by the client.
- If several subprotocols are supported by the client and the server,
- the default implementation selects the preferred subprotocols by
- giving equal value to the priorities of the client and the server.
+ If several subprotocols are supported by both the client and the server,
+ pick the first one in the list declared the server.
+
+ If the server doesn't support any subprotocols, continue without a
+ subprotocol, regardless of what the client offers.
+
+ If the server supports at least one subprotocol and the client doesn't
+ offer any, abort the handshake with an HTTP 400 error.
- If no common subprotocol is supported by the client and the server, it
- proceeds without a subprotocol.
+ You provide a ``select_subprotocol`` argument to :class:`ServerProtocol`
+ to override this logic. For example, you could accept the connection
+ even if client doesn't offer a subprotocol, rather than reject it.
- This is unlikely to be the most useful implementation in practice, as
- many servers providing a subprotocol will require that the client uses
- that subprotocol.
+ Here's how to negotiate the ``chat`` subprotocol if the client supports
+ it and continue without a subprotocol otherwise::
+
+ def select_subprotocol(protocol, subprotocols):
+ if "chat" in subprotocols:
+ return "chat"
Args:
- client_subprotocols: list of subprotocols offered by the client.
- server_subprotocols: list of subprotocols available on the server.
+ subprotocols: list of subprotocols offered by the client.
Returns:
- Optional[Subprotocol]: Subprotocol, if a common subprotocol was
- found.
+ Optional[Subprotocol]: Selected subprotocol, if a common subprotocol
+ was found.
+
+ :obj:`None` to continue without a subprotocol.
+
+ Raises:
+ NegotiationError: custom implementations may raise this exception
+ to abort the handshake with an HTTP 400 error.
"""
- subprotocols = set(client_subprotocols) & set(server_subprotocols)
- if not subprotocols:
+ # Server doesn't offer any subprotocols.
+ if not self.available_subprotocols: # None or empty list
return None
- priority = lambda p: (
- client_subprotocols.index(p) + server_subprotocols.index(p)
+
+ # Server offers at least one subprotocol but client doesn't offer any.
+ if not subprotocols:
+ raise NegotiationError("missing subprotocol")
+
+ # Server and client both offer subprotocols. Look for a shared one.
+ proposed_subprotocols = set(subprotocols)
+ for subprotocol in self.available_subprotocols:
+ if subprotocol in proposed_subprotocols:
+ return subprotocol
+
+ # No common subprotocol was found.
+ raise NegotiationError(
+ "invalid subprotocol; expected one of "
+ + ", ".join(self.available_subprotocols)
)
- return sorted(subprotocols, key=priority)[0]
def reject(
self,
- status: http.HTTPStatus,
+ status: StatusLike,
text: str,
) -> Response:
"""
@@ -462,6 +502,8 @@ class ServerConnection(Connection):
Response: WebSocket handshake response event to send to the client.
"""
+ # If a user passes an int instead of a HTTPStatus, fix it automatically.
+ status = http.HTTPStatus(status)
body = text.encode()
headers = Headers(
[
@@ -469,16 +511,15 @@ class ServerConnection(Connection):
("Connection", "close"),
("Content-Length", str(len(body))),
("Content-Type", "text/plain; charset=utf-8"),
- ("Server", USER_AGENT),
]
)
response = Response(status.value, status.phrase, headers, body)
# When reject() is called from accept(), handshake_exc is already set.
# If a user calls reject(), set handshake_exc to guarantee invariant:
- # "handshake_exc is None if and only if opening handshake succeded."
+ # "handshake_exc is None if and only if opening handshake succeeded."
if self.handshake_exc is None:
self.handshake_exc = InvalidStatus(response)
- self.logger.info("connection failed (%d %s)", status.value, status.phrase)
+ self.logger.info("connection rejected (%d %s)", status.value, status.phrase)
return response
def send_response(self, response: Response) -> None:
@@ -509,7 +550,16 @@ class ServerConnection(Connection):
def parse(self) -> Generator[None, None, None]:
if self.state is CONNECTING:
- request = yield from Request.parse(self.reader.read_line)
+ try:
+ request = yield from Request.parse(
+ self.reader.read_line,
+ )
+ except Exception as exc:
+ self.handshake_exc = exc
+ self.send_eof()
+ self.parser = self.discard()
+ next(self.parser) # start coroutine
+ yield
if self.debug:
self.logger.debug("< GET %s HTTP/1.1", request.path)
@@ -519,3 +569,12 @@ class ServerConnection(Connection):
self.events.append(request)
yield from super().parse()
+
+
+class ServerConnection(ServerProtocol):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ warnings.warn(
+ "ServerConnection was renamed to ServerProtocol",
+ DeprecationWarning,
+ )
+ super().__init__(*args, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi
new file mode 100644
index 0000000000..821438a064
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/speedups.pyi
@@ -0,0 +1 @@
+def apply_mask(data: bytes, mask: bytes) -> bytes: ...
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/__init__.py
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py
new file mode 100644
index 0000000000..087ff5f569
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/client.py
@@ -0,0 +1,328 @@
+from __future__ import annotations
+
+import socket
+import ssl
+import threading
+from typing import Any, Optional, Sequence, Type
+
+from ..client import ClientProtocol
+from ..datastructures import HeadersLike
+from ..extensions.base import ClientExtensionFactory
+from ..extensions.permessage_deflate import enable_client_permessage_deflate
+from ..headers import validate_subprotocols
+from ..http import USER_AGENT
+from ..http11 import Response
+from ..protocol import CONNECTING, OPEN, Event
+from ..typing import LoggerLike, Origin, Subprotocol
+from ..uri import parse_uri
+from .connection import Connection
+from .utils import Deadline
+
+
+__all__ = ["connect", "unix_connect", "ClientConnection"]
+
+
+class ClientConnection(Connection):
+ """
+ Threaded implementation of a WebSocket client connection.
+
+ :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for
+ receiving and sending messages.
+
+ It supports iteration to receive messages::
+
+ for message in websocket:
+ process(message)
+
+ The iterator exits normally when the connection is closed with close code
+ 1000 (OK) or 1001 (going away) or without a close code. It raises a
+ :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is
+ closed with any other code.
+
+ Args:
+ socket: Socket connected to a WebSocket server.
+ protocol: Sans-I/O connection.
+ close_timeout: Timeout for closing the connection in seconds.
+
+ """
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ protocol: ClientProtocol,
+ *,
+ close_timeout: Optional[float] = 10,
+ ) -> None:
+ self.protocol: ClientProtocol
+ self.response_rcvd = threading.Event()
+ super().__init__(
+ socket,
+ protocol,
+ close_timeout=close_timeout,
+ )
+
+ def handshake(
+ self,
+ additional_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
+ timeout: Optional[float] = None,
+ ) -> None:
+ """
+ Perform the opening handshake.
+
+ """
+ with self.send_context(expected_state=CONNECTING):
+ self.request = self.protocol.connect()
+ if additional_headers is not None:
+ self.request.headers.update(additional_headers)
+ if user_agent_header is not None:
+ self.request.headers["User-Agent"] = user_agent_header
+ self.protocol.send_request(self.request)
+
+ if not self.response_rcvd.wait(timeout):
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise TimeoutError("timed out during handshake")
+
+ if self.response is None:
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise ConnectionError("connection closed during handshake")
+
+ if self.protocol.state is not OPEN:
+ self.recv_events_thread.join(self.close_timeout)
+ self.close_socket()
+ self.recv_events_thread.join()
+
+ if self.protocol.handshake_exc is not None:
+ raise self.protocol.handshake_exc
+
+ def process_event(self, event: Event) -> None:
+ """
+ Process one incoming event.
+
+ """
+ # First event - handshake response.
+ if self.response is None:
+ assert isinstance(event, Response)
+ self.response = event
+ self.response_rcvd.set()
+ # Later events - frames.
+ else:
+ super().process_event(event)
+
+ def recv_events(self) -> None:
+ """
+ Read incoming data from the socket and process events.
+
+ """
+ try:
+ super().recv_events()
+ finally:
+ # If the connection is closed during the handshake, unblock it.
+ self.response_rcvd.set()
+
+
+def connect(
+ uri: str,
+ *,
+ # TCP/TLS — unix and path are only for unix_connect()
+ sock: Optional[socket.socket] = None,
+ ssl_context: Optional[ssl.SSLContext] = None,
+ server_hostname: Optional[str] = None,
+ unix: bool = False,
+ path: Optional[str] = None,
+ # WebSocket
+ origin: Optional[Origin] = None,
+ extensions: Optional[Sequence[ClientExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ additional_headers: Optional[HeadersLike] = None,
+ user_agent_header: Optional[str] = USER_AGENT,
+ compression: Optional[str] = "deflate",
+ # Timeouts
+ open_timeout: Optional[float] = 10,
+ close_timeout: Optional[float] = 10,
+ # Limits
+ max_size: Optional[int] = 2**20,
+ # Logging
+ logger: Optional[LoggerLike] = None,
+ # Escape hatch for advanced customization
+ create_connection: Optional[Type[ClientConnection]] = None,
+) -> ClientConnection:
+ """
+ Connect to the WebSocket server at ``uri``.
+
+ This function returns a :class:`ClientConnection` instance, which you can
+ use to send and receive messages.
+
+ :func:`connect` may be used as a context manager::
+
+ async with websockets.sync.client.connect(...) as websocket:
+ ...
+
+ The connection is closed automatically when exiting the context.
+
+ Args:
+ uri: URI of the WebSocket server.
+ sock: Preexisting TCP socket. ``sock`` overrides the host and port
+ from ``uri``. You may call :func:`socket.create_connection` to
+ create a suitable TCP socket.
+ ssl_context: Configuration for enabling TLS on the connection.
+ server_hostname: Host name for the TLS handshake. ``server_hostname``
+ overrides the host name from ``uri``.
+ origin: Value of the ``Origin`` header, for servers that require it.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
+ preference.
+ additional_headers (HeadersLike | None): Arbitrary HTTP headers to add
+ to the handshake request.
+ user_agent_header: Value of the ``User-Agent`` request header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``.
+ Setting it to :obj:`None` removes the header.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ open_timeout: Timeout for opening the connection in seconds.
+ :obj:`None` disables the timeout.
+ close_timeout: Timeout for closing the connection in seconds.
+ :obj:`None` disables the timeout.
+ max_size: Maximum size of incoming messages in bytes.
+ :obj:`None` disables the limit.
+ logger: Logger for this client.
+ It defaults to ``logging.getLogger("websockets.client")``.
+ See the :doc:`logging guide <../../topics/logging>` for details.
+ create_connection: Factory for the :class:`ClientConnection` managing
+ the connection. Set it to a wrapper or a subclass to customize
+ connection handling.
+
+ Raises:
+ InvalidURI: If ``uri`` isn't a valid WebSocket URI.
+ OSError: If the TCP connection fails.
+ InvalidHandshake: If the opening handshake fails.
+ TimeoutError: If the opening handshake times out.
+
+ """
+
+ # Process parameters
+
+ wsuri = parse_uri(uri)
+ if not wsuri.secure and ssl_context is not None:
+ raise TypeError("ssl_context argument is incompatible with a ws:// URI")
+
+ if unix:
+ if path is None and sock is None:
+ raise TypeError("missing path argument")
+ elif path is not None and sock is not None:
+ raise TypeError("path and sock arguments are incompatible")
+ else:
+ assert path is None # private argument, only set by unix_connect()
+
+ if subprotocols is not None:
+ validate_subprotocols(subprotocols)
+
+ if compression == "deflate":
+ extensions = enable_client_permessage_deflate(extensions)
+ elif compression is not None:
+ raise ValueError(f"unsupported compression: {compression}")
+
+ # Calculate timeouts on the TCP, TLS, and WebSocket handshakes.
+ # The TCP and TLS timeouts must be set on the socket, then removed
+ # to avoid conflicting with the WebSocket timeout in handshake().
+ deadline = Deadline(open_timeout)
+
+ if create_connection is None:
+ create_connection = ClientConnection
+
+ try:
+ # Connect socket
+
+ if sock is None:
+ if unix:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.settimeout(deadline.timeout())
+ assert path is not None # validated above -- this is for mpypy
+ sock.connect(path)
+ else:
+ sock = socket.create_connection(
+ (wsuri.host, wsuri.port),
+ deadline.timeout(),
+ )
+ sock.settimeout(None)
+
+ # Disable Nagle algorithm
+
+ if not unix:
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
+
+ # Initialize TLS wrapper and perform TLS handshake
+
+ if wsuri.secure:
+ if ssl_context is None:
+ ssl_context = ssl.create_default_context()
+ if server_hostname is None:
+ server_hostname = wsuri.host
+ sock.settimeout(deadline.timeout())
+ sock = ssl_context.wrap_socket(sock, server_hostname=server_hostname)
+ sock.settimeout(None)
+
+ # Initialize WebSocket connection
+
+ protocol = ClientProtocol(
+ wsuri,
+ origin=origin,
+ extensions=extensions,
+ subprotocols=subprotocols,
+ state=CONNECTING,
+ max_size=max_size,
+ logger=logger,
+ )
+
+ # Initialize WebSocket protocol
+
+ connection = create_connection(
+ sock,
+ protocol,
+ close_timeout=close_timeout,
+ )
+ # On failure, handshake() closes the socket and raises an exception.
+ connection.handshake(
+ additional_headers,
+ user_agent_header,
+ deadline.timeout(),
+ )
+
+ except Exception:
+ if sock is not None:
+ sock.close()
+ raise
+
+ return connection
+
+
+def unix_connect(
+ path: Optional[str] = None,
+ uri: Optional[str] = None,
+ **kwargs: Any,
+) -> ClientConnection:
+ """
+ Connect to a WebSocket server listening on a Unix socket.
+
+ This function is identical to :func:`connect`, except for the additional
+ ``path`` argument. It's only available on Unix.
+
+ It's mainly useful for debugging servers listening on Unix sockets.
+
+ Args:
+ path: File system path to the Unix socket.
+ uri: URI of the WebSocket server. ``uri`` defaults to
+ ``ws://localhost/`` or, when a ``ssl_context`` is provided, to
+ ``wss://localhost/``.
+
+ """
+ if uri is None:
+ if kwargs.get("ssl_context") is None:
+ uri = "ws://localhost/"
+ else:
+ uri = "wss://localhost/"
+ return connect(uri=uri, unix=True, path=path, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py
new file mode 100644
index 0000000000..4a8879e370
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/connection.py
@@ -0,0 +1,773 @@
+from __future__ import annotations
+
+import contextlib
+import logging
+import random
+import socket
+import struct
+import threading
+import uuid
+from types import TracebackType
+from typing import Any, Dict, Iterable, Iterator, Mapping, Optional, Type, Union
+
+from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError
+from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl
+from ..http11 import Request, Response
+from ..protocol import CLOSED, OPEN, Event, Protocol, State
+from ..typing import Data, LoggerLike, Subprotocol
+from .messages import Assembler
+from .utils import Deadline
+
+
+__all__ = ["Connection"]
+
+logger = logging.getLogger(__name__)
+
+
+class Connection:
+ """
+ Threaded implementation of a WebSocket connection.
+
+ :class:`Connection` provides APIs shared between WebSocket servers and
+ clients.
+
+ You shouldn't use it directly. Instead, use
+ :class:`~websockets.sync.client.ClientConnection` or
+ :class:`~websockets.sync.server.ServerConnection`.
+
+ """
+
+ recv_bufsize = 65536
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ protocol: Protocol,
+ *,
+ close_timeout: Optional[float] = 10,
+ ) -> None:
+ self.socket = socket
+ self.protocol = protocol
+ self.close_timeout = close_timeout
+
+ # Inject reference to this instance in the protocol's logger.
+ self.protocol.logger = logging.LoggerAdapter(
+ self.protocol.logger,
+ {"websocket": self},
+ )
+
+ # Copy attributes from the protocol for convenience.
+ self.id: uuid.UUID = self.protocol.id
+ """Unique identifier of the connection. Useful in logs."""
+ self.logger: LoggerLike = self.protocol.logger
+ """Logger for this connection."""
+ self.debug = self.protocol.debug
+
+ # HTTP handshake request and response.
+ self.request: Optional[Request] = None
+ """Opening handshake request."""
+ self.response: Optional[Response] = None
+ """Opening handshake response."""
+
+ # Mutex serializing interactions with the protocol.
+ self.protocol_mutex = threading.Lock()
+
+ # Assembler turning frames into messages and serializing reads.
+ self.recv_messages = Assembler()
+
+ # Whether we are busy sending a fragmented message.
+ self.send_in_progress = False
+
+ # Deadline for the closing handshake.
+ self.close_deadline: Optional[Deadline] = None
+
+ # Mapping of ping IDs to pong waiters, in chronological order.
+ self.pings: Dict[bytes, threading.Event] = {}
+
+ # Receiving events from the socket.
+ self.recv_events_thread = threading.Thread(target=self.recv_events)
+ self.recv_events_thread.start()
+
+ # Exception raised in recv_events, to be chained to ConnectionClosed
+ # in the user thread in order to show why the TCP connection dropped.
+ self.recv_events_exc: Optional[BaseException] = None
+
+ # Public attributes
+
+ @property
+ def local_address(self) -> Any:
+ """
+ Local address of the connection.
+
+ For IPv4 connections, this is a ``(host, port)`` tuple.
+
+ The format of the address depends on the address family.
+ See :meth:`~socket.socket.getsockname`.
+
+ """
+ return self.socket.getsockname()
+
+ @property
+ def remote_address(self) -> Any:
+ """
+ Remote address of the connection.
+
+ For IPv4 connections, this is a ``(host, port)`` tuple.
+
+ The format of the address depends on the address family.
+ See :meth:`~socket.socket.getpeername`.
+
+ """
+ return self.socket.getpeername()
+
+ @property
+ def subprotocol(self) -> Optional[Subprotocol]:
+ """
+ Subprotocol negotiated during the opening handshake.
+
+ :obj:`None` if no subprotocol was negotiated.
+
+ """
+ return self.protocol.subprotocol
+
+ # Public methods
+
+ def __enter__(self) -> Connection:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ if exc_type is None:
+ self.close()
+ else:
+ self.close(CloseCode.INTERNAL_ERROR)
+
+ def __iter__(self) -> Iterator[Data]:
+ """
+ Iterate on incoming messages.
+
+ The iterator calls :meth:`recv` and yields messages in an infinite loop.
+
+ It exits when the connection is closed normally. It raises a
+ :exc:`~websockets.exceptions.ConnectionClosedError` exception after a
+ protocol error or a network failure.
+
+ """
+ try:
+ while True:
+ yield self.recv()
+ except ConnectionClosedOK:
+ return
+
+ def recv(self, timeout: Optional[float] = None) -> Data:
+ """
+ Receive the next message.
+
+ When the connection is closed, :meth:`recv` raises
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises
+ :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure
+ and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
+ error or a network failure. This is how you detect the end of the
+ message stream.
+
+ If ``timeout`` is :obj:`None`, block until a message is received. If
+ ``timeout`` is set and no message is received within ``timeout``
+ seconds, raise :exc:`TimeoutError`. Set ``timeout`` to ``0`` to check if
+ a message was already received.
+
+ If the message is fragmented, wait until all fragments are received,
+ reassemble them, and return the whole message.
+
+ Returns:
+ A string (:class:`str`) for a Text_ frame or a bytestring
+ (:class:`bytes`) for a Binary_ frame.
+
+ .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+ .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If two threads call :meth:`recv` or
+ :meth:`recv_streaming` concurrently.
+
+ """
+ try:
+ return self.recv_messages.get(timeout)
+ except EOFError:
+ raise self.protocol.close_exc from self.recv_events_exc
+ except RuntimeError:
+ raise RuntimeError(
+ "cannot call recv while another thread "
+ "is already running recv or recv_streaming"
+ ) from None
+
+ def recv_streaming(self) -> Iterator[Data]:
+ """
+ Receive the next message frame by frame.
+
+ If the message is fragmented, yield each fragment as it is received.
+ The iterator must be fully consumed, or else the connection will become
+ unusable.
+
+ :meth:`recv_streaming` raises the same exceptions as :meth:`recv`.
+
+ Returns:
+ An iterator of strings (:class:`str`) for a Text_ frame or
+ bytestrings (:class:`bytes`) for a Binary_ frame.
+
+ .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+ .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If two threads call :meth:`recv` or
+ :meth:`recv_streaming` concurrently.
+
+ """
+ try:
+ yield from self.recv_messages.get_iter()
+ except EOFError:
+ raise self.protocol.close_exc from self.recv_events_exc
+ except RuntimeError:
+ raise RuntimeError(
+ "cannot call recv_streaming while another thread "
+ "is already running recv or recv_streaming"
+ ) from None
+
+ def send(self, message: Union[Data, Iterable[Data]]) -> None:
+ """
+ Send a message.
+
+ A string (:class:`str`) is sent as a Text_ frame. A bytestring or
+ bytes-like object (:class:`bytes`, :class:`bytearray`, or
+ :class:`memoryview`) is sent as a Binary_ frame.
+
+ .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+ .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6
+
+ :meth:`send` also accepts an iterable of strings, bytestrings, or
+ bytes-like objects to enable fragmentation_. Each item is treated as a
+ message fragment and sent in its own frame. All items must be of the
+ same type, or else :meth:`send` will raise a :exc:`TypeError` and the
+ connection will be closed.
+
+ .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4
+
+ :meth:`send` rejects dict-like objects because this is often an error.
+ (If you really want to send the keys of a dict-like object as fragments,
+ call its :meth:`~dict.keys` method and pass the result to :meth:`send`.)
+
+ When the connection is closed, :meth:`send` raises
+ :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it
+ raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal
+ connection closure and
+ :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol
+ error or a network failure.
+
+ Args:
+ message: Message to send.
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If a connection is busy sending a fragmented message.
+ TypeError: If ``message`` doesn't have a supported type.
+
+ """
+ # Unfragmented message -- this case must be handled first because
+ # strings and bytes-like objects are iterable.
+
+ if isinstance(message, str):
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.protocol.send_text(message.encode("utf-8"))
+
+ elif isinstance(message, BytesLike):
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.protocol.send_binary(message)
+
+ # Catch a common mistake -- passing a dict to send().
+
+ elif isinstance(message, Mapping):
+ raise TypeError("data is a dict-like object")
+
+ # Fragmented message -- regular iterator.
+
+ elif isinstance(message, Iterable):
+ chunks = iter(message)
+ try:
+ chunk = next(chunks)
+ except StopIteration:
+ return
+
+ try:
+ # First fragment.
+ if isinstance(chunk, str):
+ text = True
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.send_in_progress = True
+ self.protocol.send_text(
+ chunk.encode("utf-8"),
+ fin=False,
+ )
+ elif isinstance(chunk, BytesLike):
+ text = False
+ with self.send_context():
+ if self.send_in_progress:
+ raise RuntimeError(
+ "cannot call send while another thread "
+ "is already running send"
+ )
+ self.send_in_progress = True
+ self.protocol.send_binary(
+ chunk,
+ fin=False,
+ )
+ else:
+ raise TypeError("data iterable must contain bytes or str")
+
+ # Other fragments
+ for chunk in chunks:
+ if isinstance(chunk, str) and text:
+ with self.send_context():
+ assert self.send_in_progress
+ self.protocol.send_continuation(
+ chunk.encode("utf-8"),
+ fin=False,
+ )
+ elif isinstance(chunk, BytesLike) and not text:
+ with self.send_context():
+ assert self.send_in_progress
+ self.protocol.send_continuation(
+ chunk,
+ fin=False,
+ )
+ else:
+ raise TypeError("data iterable must contain uniform types")
+
+ # Final fragment.
+ with self.send_context():
+ self.protocol.send_continuation(b"", fin=True)
+ self.send_in_progress = False
+
+ except RuntimeError:
+ # We didn't start sending a fragmented message.
+ raise
+
+ except Exception:
+ # We're half-way through a fragmented message and we can't
+ # complete it. This makes the connection unusable.
+ with self.send_context():
+ self.protocol.fail(
+ CloseCode.INTERNAL_ERROR,
+ "error in fragmented message",
+ )
+ raise
+
+ else:
+ raise TypeError("data must be bytes, str, or iterable")
+
+ def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None:
+ """
+ Perform the closing handshake.
+
+ :meth:`close` waits for the other end to complete the handshake, for the
+ TCP connection to terminate, and for all incoming messages to be read
+ with :meth:`recv`.
+
+ :meth:`close` is idempotent: it doesn't do anything once the
+ connection is closed.
+
+ Args:
+ code: WebSocket close code.
+ reason: WebSocket close reason.
+
+ """
+ try:
+ # The context manager takes care of waiting for the TCP connection
+ # to terminate after calling a method that sends a close frame.
+ with self.send_context():
+ if self.send_in_progress:
+ self.protocol.fail(
+ CloseCode.INTERNAL_ERROR,
+ "close during fragmented message",
+ )
+ else:
+ self.protocol.send_close(code, reason)
+ except ConnectionClosed:
+ # Ignore ConnectionClosed exceptions raised from send_context().
+ # They mean that the connection is closed, which was the goal.
+ pass
+
+ def ping(self, data: Optional[Data] = None) -> threading.Event:
+ """
+ Send a Ping_.
+
+ .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
+
+ A ping may serve as a keepalive or as a check that the remote endpoint
+ received all messages up to this point
+
+ Args:
+ data: Payload of the ping. A :class:`str` will be encoded to UTF-8.
+ If ``data`` is :obj:`None`, the payload is four random bytes.
+
+ Returns:
+ An event that will be set when the corresponding pong is received.
+ You can ignore it if you don't intend to wait.
+
+ ::
+
+ pong_event = ws.ping()
+ pong_event.wait() # only if you want to wait for the pong
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+ RuntimeError: If another ping was sent with the same data and
+ the corresponding pong wasn't received yet.
+
+ """
+ if data is not None:
+ data = prepare_ctrl(data)
+
+ with self.send_context():
+ # Protect against duplicates if a payload is explicitly set.
+ if data in self.pings:
+ raise RuntimeError("already waiting for a pong with the same data")
+
+ # Generate a unique random payload otherwise.
+ while data is None or data in self.pings:
+ data = struct.pack("!I", random.getrandbits(32))
+
+ pong_waiter = threading.Event()
+ self.pings[data] = pong_waiter
+ self.protocol.send_ping(data)
+ return pong_waiter
+
+ def pong(self, data: Data = b"") -> None:
+ """
+ Send a Pong_.
+
+ .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+
+ An unsolicited pong may serve as a unidirectional heartbeat.
+
+ Args:
+ data: Payload of the pong. A :class:`str` will be encoded to UTF-8.
+
+ Raises:
+ ConnectionClosed: When the connection is closed.
+
+ """
+ data = prepare_ctrl(data)
+
+ with self.send_context():
+ self.protocol.send_pong(data)
+
+ # Private methods
+
+ def process_event(self, event: Event) -> None:
+ """
+ Process one incoming event.
+
+ This method is overridden in subclasses to handle the handshake.
+
+ """
+ assert isinstance(event, Frame)
+ if event.opcode in DATA_OPCODES:
+ self.recv_messages.put(event)
+
+ if event.opcode is Opcode.PONG:
+ self.acknowledge_pings(bytes(event.data))
+
+ def acknowledge_pings(self, data: bytes) -> None:
+ """
+ Acknowledge pings when receiving a pong.
+
+ """
+ with self.protocol_mutex:
+ # Ignore unsolicited pong.
+ if data not in self.pings:
+ return
+ # Sending a pong for only the most recent ping is legal.
+ # Acknowledge all previous pings too in that case.
+ ping_id = None
+ ping_ids = []
+ for ping_id, ping in self.pings.items():
+ ping_ids.append(ping_id)
+ ping.set()
+ if ping_id == data:
+ break
+ else:
+ raise AssertionError("solicited pong not found in pings")
+ # Remove acknowledged pings from self.pings.
+ for ping_id in ping_ids:
+ del self.pings[ping_id]
+
+ def recv_events(self) -> None:
+ """
+ Read incoming data from the socket and process events.
+
+ Run this method in a thread as long as the connection is alive.
+
+ ``recv_events()`` exits immediately when the ``self.socket`` is closed.
+
+ """
+ try:
+ while True:
+ try:
+ if self.close_deadline is not None:
+ self.socket.settimeout(self.close_deadline.timeout())
+ data = self.socket.recv(self.recv_bufsize)
+ except Exception as exc:
+ if self.debug:
+ self.logger.debug("error while receiving data", exc_info=True)
+ # When the closing handshake is initiated by our side,
+ # recv() may block until send_context() closes the socket.
+ # In that case, send_context() already set recv_events_exc.
+ # Calling set_recv_events_exc() avoids overwriting it.
+ with self.protocol_mutex:
+ self.set_recv_events_exc(exc)
+ break
+
+ if data == b"":
+ break
+
+ # Acquire the connection lock.
+ with self.protocol_mutex:
+ # Feed incoming data to the connection.
+ self.protocol.receive_data(data)
+
+ # This isn't expected to raise an exception.
+ events = self.protocol.events_received()
+
+ # Write outgoing data to the socket.
+ try:
+ self.send_data()
+ except Exception as exc:
+ if self.debug:
+ self.logger.debug("error while sending data", exc_info=True)
+ # Similarly to the above, avoid overriding an exception
+ # set by send_context(), in case of a race condition
+ # i.e. send_context() closes the socket after recv()
+ # returns above but before send_data() calls send().
+ self.set_recv_events_exc(exc)
+ break
+
+ if self.protocol.close_expected():
+ # If the connection is expected to close soon, set the
+ # close deadline based on the close timeout.
+ if self.close_deadline is None:
+ self.close_deadline = Deadline(self.close_timeout)
+
+ # Unlock conn_mutex before processing events. Else, the
+ # application can't send messages in response to events.
+
+ # If self.send_data raised an exception, then events are lost.
+ # Given that automatic responses write small amounts of data,
+ # this should be uncommon, so we don't handle the edge case.
+
+ try:
+ for event in events:
+ # This may raise EOFError if the closing handshake
+ # times out while a message is waiting to be read.
+ self.process_event(event)
+ except EOFError:
+ break
+
+ # Breaking out of the while True: ... loop means that we believe
+ # that the socket doesn't work anymore.
+ with self.protocol_mutex:
+ # Feed the end of the data stream to the connection.
+ self.protocol.receive_eof()
+
+ # This isn't expected to generate events.
+ assert not self.protocol.events_received()
+
+ # There is no error handling because send_data() can only write
+ # the end of the data stream here and it handles errors itself.
+ self.send_data()
+
+ except Exception as exc:
+ # This branch should never run. It's a safety net in case of bugs.
+ self.logger.error("unexpected internal error", exc_info=True)
+ with self.protocol_mutex:
+ self.set_recv_events_exc(exc)
+ # We don't know where we crashed. Force protocol state to CLOSED.
+ self.protocol.state = CLOSED
+ finally:
+ # This isn't expected to raise an exception.
+ self.close_socket()
+
+ @contextlib.contextmanager
+ def send_context(
+ self,
+ *,
+ expected_state: State = OPEN, # CONNECTING during the opening handshake
+ ) -> Iterator[None]:
+ """
+ Create a context for writing to the connection from user code.
+
+ On entry, :meth:`send_context` acquires the connection lock and checks
+ that the connection is open; on exit, it writes outgoing data to the
+ socket::
+
+ with self.send_context():
+ self.protocol.send_text(message.encode("utf-8"))
+
+ When the connection isn't open on entry, when the connection is expected
+ to close on exit, or when an unexpected error happens, terminating the
+ connection, :meth:`send_context` waits until the connection is closed
+ then raises :exc:`~websockets.exceptions.ConnectionClosed`.
+
+ """
+ # Should we wait until the connection is closed?
+ wait_for_close = False
+ # Should we close the socket and raise ConnectionClosed?
+ raise_close_exc = False
+ # What exception should we chain ConnectionClosed to?
+ original_exc: Optional[BaseException] = None
+
+ # Acquire the protocol lock.
+ with self.protocol_mutex:
+ if self.protocol.state is expected_state:
+ # Let the caller interact with the protocol.
+ try:
+ yield
+ except (ProtocolError, RuntimeError):
+ # The protocol state wasn't changed. Exit immediately.
+ raise
+ except Exception as exc:
+ self.logger.error("unexpected internal error", exc_info=True)
+ # This branch should never run. It's a safety net in case of
+ # bugs. Since we don't know what happened, we will close the
+ # connection and raise the exception to the caller.
+ wait_for_close = False
+ raise_close_exc = True
+ original_exc = exc
+ else:
+ # Check if the connection is expected to close soon.
+ if self.protocol.close_expected():
+ wait_for_close = True
+ # If the connection is expected to close soon, set the
+ # close deadline based on the close timeout.
+
+ # Since we tested earlier that protocol.state was OPEN
+ # (or CONNECTING) and we didn't release protocol_mutex,
+ # it is certain that self.close_deadline is still None.
+ assert self.close_deadline is None
+ self.close_deadline = Deadline(self.close_timeout)
+ # Write outgoing data to the socket.
+ try:
+ self.send_data()
+ except Exception as exc:
+ if self.debug:
+ self.logger.debug("error while sending data", exc_info=True)
+ # While the only expected exception here is OSError,
+ # other exceptions would be treated identically.
+ wait_for_close = False
+ raise_close_exc = True
+ original_exc = exc
+
+ else: # self.protocol.state is not expected_state
+ # Minor layering violation: we assume that the connection
+ # will be closing soon if it isn't in the expected state.
+ wait_for_close = True
+ raise_close_exc = True
+
+ # To avoid a deadlock, release the connection lock by exiting the
+ # context manager before waiting for recv_events() to terminate.
+
+ # If the connection is expected to close soon and the close timeout
+ # elapses, close the socket to terminate the connection.
+ if wait_for_close:
+ if self.close_deadline is None:
+ timeout = self.close_timeout
+ else:
+ # Thread.join() returns immediately if timeout is negative.
+ timeout = self.close_deadline.timeout(raise_if_elapsed=False)
+ self.recv_events_thread.join(timeout)
+
+ if self.recv_events_thread.is_alive():
+ # There's no risk to overwrite another error because
+ # original_exc is never set when wait_for_close is True.
+ assert original_exc is None
+ original_exc = TimeoutError("timed out while closing connection")
+ # Set recv_events_exc before closing the socket in order to get
+ # proper exception reporting.
+ raise_close_exc = True
+ with self.protocol_mutex:
+ self.set_recv_events_exc(original_exc)
+
+ # If an error occurred, close the socket to terminate the connection and
+ # raise an exception.
+ if raise_close_exc:
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise self.protocol.close_exc from original_exc
+
+ def send_data(self) -> None:
+ """
+ Send outgoing data.
+
+ This method requires holding protocol_mutex.
+
+ Raises:
+ OSError: When a socket operations fails.
+
+ """
+ assert self.protocol_mutex.locked()
+ for data in self.protocol.data_to_send():
+ if data:
+ if self.close_deadline is not None:
+ self.socket.settimeout(self.close_deadline.timeout())
+ self.socket.sendall(data)
+ else:
+ try:
+ self.socket.shutdown(socket.SHUT_WR)
+ except OSError: # socket already closed
+ pass
+
+ def set_recv_events_exc(self, exc: Optional[BaseException]) -> None:
+ """
+ Set recv_events_exc, if not set yet.
+
+ This method requires holding protocol_mutex.
+
+ """
+ assert self.protocol_mutex.locked()
+ if self.recv_events_exc is None:
+ self.recv_events_exc = exc
+
+ def close_socket(self) -> None:
+ """
+ Shutdown and close socket. Close message assembler.
+
+ Calling close_socket() guarantees that recv_events() terminates. Indeed,
+ recv_events() may block only on socket.recv() or on recv_messages.put().
+
+ """
+ # shutdown() is required to interrupt recv() on Linux.
+ try:
+ self.socket.shutdown(socket.SHUT_RDWR)
+ except OSError:
+ pass # socket is already closed
+ self.socket.close()
+ self.recv_messages.close()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py
new file mode 100644
index 0000000000..67a22313ca
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/messages.py
@@ -0,0 +1,281 @@
+from __future__ import annotations
+
+import codecs
+import queue
+import threading
+from typing import Iterator, List, Optional, cast
+
+from ..frames import Frame, Opcode
+from ..typing import Data
+
+
+__all__ = ["Assembler"]
+
+UTF8Decoder = codecs.getincrementaldecoder("utf-8")
+
+
+class Assembler:
+ """
+ Assemble messages from frames.
+
+ """
+
+ def __init__(self) -> None:
+ # Serialize reads and writes -- except for reads via synchronization
+ # primitives provided by the threading and queue modules.
+ self.mutex = threading.Lock()
+
+ # We create a latch with two events to ensure proper interleaving of
+ # writing and reading messages.
+ # put() sets this event to tell get() that a message can be fetched.
+ self.message_complete = threading.Event()
+ # get() sets this event to let put() that the message was fetched.
+ self.message_fetched = threading.Event()
+
+ # This flag prevents concurrent calls to get() by user code.
+ self.get_in_progress = False
+ # This flag prevents concurrent calls to put() by library code.
+ self.put_in_progress = False
+
+ # Decoder for text frames, None for binary frames.
+ self.decoder: Optional[codecs.IncrementalDecoder] = None
+
+ # Buffer of frames belonging to the same message.
+ self.chunks: List[Data] = []
+
+ # When switching from "buffering" to "streaming", we use a thread-safe
+ # queue for transferring frames from the writing thread (library code)
+ # to the reading thread (user code). We're buffering when chunks_queue
+ # is None and streaming when it's a SimpleQueue. None is a sentinel
+ # value marking the end of the stream, superseding message_complete.
+
+ # Stream data from frames belonging to the same message.
+ # Remove quotes around type when dropping Python < 3.9.
+ self.chunks_queue: Optional["queue.SimpleQueue[Optional[Data]]"] = None
+
+ # This flag marks the end of the stream.
+ self.closed = False
+
+ def get(self, timeout: Optional[float] = None) -> Data:
+ """
+ Read the next message.
+
+ :meth:`get` returns a single :class:`str` or :class:`bytes`.
+
+ If the message is fragmented, :meth:`get` waits until the last frame is
+ received, then it reassembles the message and returns it. To receive
+ messages frame by frame, use :meth:`get_iter` instead.
+
+ Args:
+ timeout: If a timeout is provided and elapses before a complete
+ message is received, :meth:`get` raises :exc:`TimeoutError`.
+
+ Raises:
+ EOFError: If the stream of frames has ended.
+ RuntimeError: If two threads run :meth:`get` or :meth:``get_iter`
+ concurrently.
+
+ """
+ with self.mutex:
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ if self.get_in_progress:
+ raise RuntimeError("get or get_iter is already running")
+
+ self.get_in_progress = True
+
+ # If the message_complete event isn't set yet, release the lock to
+ # allow put() to run and eventually set it.
+ # Locking with get_in_progress ensures only one thread can get here.
+ completed = self.message_complete.wait(timeout)
+
+ with self.mutex:
+ self.get_in_progress = False
+
+ # Waiting for a complete message timed out.
+ if not completed:
+ raise TimeoutError(f"timed out in {timeout:.1f}s")
+
+ # get() was unblocked by close() rather than put().
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ assert self.message_complete.is_set()
+ self.message_complete.clear()
+
+ joiner: Data = b"" if self.decoder is None else ""
+ # mypy cannot figure out that chunks have the proper type.
+ message: Data = joiner.join(self.chunks) # type: ignore
+
+ assert not self.message_fetched.is_set()
+ self.message_fetched.set()
+
+ self.chunks = []
+ assert self.chunks_queue is None
+
+ return message
+
+ def get_iter(self) -> Iterator[Data]:
+ """
+ Stream the next message.
+
+ Iterating the return value of :meth:`get_iter` yields a :class:`str` or
+ :class:`bytes` for each frame in the message.
+
+ The iterator must be fully consumed before calling :meth:`get_iter` or
+ :meth:`get` again. Else, :exc:`RuntimeError` is raised.
+
+ This method only makes sense for fragmented messages. If messages aren't
+ fragmented, use :meth:`get` instead.
+
+ Raises:
+ EOFError: If the stream of frames has ended.
+ RuntimeError: If two threads run :meth:`get` or :meth:``get_iter`
+ concurrently.
+
+ """
+ with self.mutex:
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ if self.get_in_progress:
+ raise RuntimeError("get or get_iter is already running")
+
+ chunks = self.chunks
+ self.chunks = []
+ self.chunks_queue = cast(
+ # Remove quotes around type when dropping Python < 3.9.
+ "queue.SimpleQueue[Optional[Data]]",
+ queue.SimpleQueue(),
+ )
+
+ # Sending None in chunk_queue supersedes setting message_complete
+ # when switching to "streaming". If message is already complete
+ # when the switch happens, put() didn't send None, so we have to.
+ if self.message_complete.is_set():
+ self.chunks_queue.put(None)
+
+ self.get_in_progress = True
+
+ # Locking with get_in_progress ensures only one thread can get here.
+ yield from chunks
+ while True:
+ chunk = self.chunks_queue.get()
+ if chunk is None:
+ break
+ yield chunk
+
+ with self.mutex:
+ self.get_in_progress = False
+
+ assert self.message_complete.is_set()
+ self.message_complete.clear()
+
+ # get_iter() was unblocked by close() rather than put().
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ assert not self.message_fetched.is_set()
+ self.message_fetched.set()
+
+ assert self.chunks == []
+ self.chunks_queue = None
+
+ def put(self, frame: Frame) -> None:
+ """
+ Add ``frame`` to the next message.
+
+ When ``frame`` is the final frame in a message, :meth:`put` waits until
+ the message is fetched, either by calling :meth:`get` or by fully
+ consuming the return value of :meth:`get_iter`.
+
+ :meth:`put` assumes that the stream of frames respects the protocol. If
+ it doesn't, the behavior is undefined.
+
+ Raises:
+ EOFError: If the stream of frames has ended.
+ RuntimeError: If two threads run :meth:`put` concurrently.
+
+ """
+ with self.mutex:
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ if self.put_in_progress:
+ raise RuntimeError("put is already running")
+
+ if frame.opcode is Opcode.TEXT:
+ self.decoder = UTF8Decoder(errors="strict")
+ elif frame.opcode is Opcode.BINARY:
+ self.decoder = None
+ elif frame.opcode is Opcode.CONT:
+ pass
+ else:
+ # Ignore control frames.
+ return
+
+ data: Data
+ if self.decoder is not None:
+ data = self.decoder.decode(frame.data, frame.fin)
+ else:
+ data = frame.data
+
+ if self.chunks_queue is None:
+ self.chunks.append(data)
+ else:
+ self.chunks_queue.put(data)
+
+ if not frame.fin:
+ return
+
+ # Message is complete. Wait until it's fetched to return.
+
+ assert not self.message_complete.is_set()
+ self.message_complete.set()
+
+ if self.chunks_queue is not None:
+ self.chunks_queue.put(None)
+
+ assert not self.message_fetched.is_set()
+
+ self.put_in_progress = True
+
+ # Release the lock to allow get() to run and eventually set the event.
+ self.message_fetched.wait()
+
+ with self.mutex:
+ self.put_in_progress = False
+
+ assert self.message_fetched.is_set()
+ self.message_fetched.clear()
+
+ # put() was unblocked by close() rather than get() or get_iter().
+ if self.closed:
+ raise EOFError("stream of frames ended")
+
+ self.decoder = None
+
+ def close(self) -> None:
+ """
+ End the stream of frames.
+
+ Callling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`,
+ or :meth:`put` is safe. They will raise :exc:`EOFError`.
+
+ """
+ with self.mutex:
+ if self.closed:
+ return
+
+ self.closed = True
+
+ # Unblock get or get_iter.
+ if self.get_in_progress:
+ self.message_complete.set()
+ if self.chunks_queue is not None:
+ self.chunks_queue.put(None)
+
+ # Unblock put().
+ if self.put_in_progress:
+ self.message_fetched.set()
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py
new file mode 100644
index 0000000000..14767968c9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/server.py
@@ -0,0 +1,530 @@
+from __future__ import annotations
+
+import http
+import logging
+import os
+import selectors
+import socket
+import ssl
+import sys
+import threading
+from types import TracebackType
+from typing import Any, Callable, Optional, Sequence, Type
+
+from websockets.frames import CloseCode
+
+from ..extensions.base import ServerExtensionFactory
+from ..extensions.permessage_deflate import enable_server_permessage_deflate
+from ..headers import validate_subprotocols
+from ..http import USER_AGENT
+from ..http11 import Request, Response
+from ..protocol import CONNECTING, OPEN, Event
+from ..server import ServerProtocol
+from ..typing import LoggerLike, Origin, Subprotocol
+from .connection import Connection
+from .utils import Deadline
+
+
+__all__ = ["serve", "unix_serve", "ServerConnection", "WebSocketServer"]
+
+
+class ServerConnection(Connection):
+ """
+ Threaded implementation of a WebSocket server connection.
+
+ :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for
+ receiving and sending messages.
+
+ It supports iteration to receive messages::
+
+ for message in websocket:
+ process(message)
+
+ The iterator exits normally when the connection is closed with close code
+ 1000 (OK) or 1001 (going away) or without a close code. It raises a
+ :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is
+ closed with any other code.
+
+ Args:
+ socket: Socket connected to a WebSocket client.
+ protocol: Sans-I/O connection.
+ close_timeout: Timeout for closing the connection in seconds.
+
+ """
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ protocol: ServerProtocol,
+ *,
+ close_timeout: Optional[float] = 10,
+ ) -> None:
+ self.protocol: ServerProtocol
+ self.request_rcvd = threading.Event()
+ super().__init__(
+ socket,
+ protocol,
+ close_timeout=close_timeout,
+ )
+
+ def handshake(
+ self,
+ process_request: Optional[
+ Callable[
+ [ServerConnection, Request],
+ Optional[Response],
+ ]
+ ] = None,
+ process_response: Optional[
+ Callable[
+ [ServerConnection, Request, Response],
+ Optional[Response],
+ ]
+ ] = None,
+ server_header: Optional[str] = USER_AGENT,
+ timeout: Optional[float] = None,
+ ) -> None:
+ """
+ Perform the opening handshake.
+
+ """
+ if not self.request_rcvd.wait(timeout):
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise TimeoutError("timed out during handshake")
+
+ if self.request is None:
+ self.close_socket()
+ self.recv_events_thread.join()
+ raise ConnectionError("connection closed during handshake")
+
+ with self.send_context(expected_state=CONNECTING):
+ self.response = None
+
+ if process_request is not None:
+ try:
+ self.response = process_request(self, self.request)
+ except Exception as exc:
+ self.protocol.handshake_exc = exc
+ self.logger.error("opening handshake failed", exc_info=True)
+ self.response = self.protocol.reject(
+ http.HTTPStatus.INTERNAL_SERVER_ERROR,
+ (
+ "Failed to open a WebSocket connection.\n"
+ "See server log for more information.\n"
+ ),
+ )
+
+ if self.response is None:
+ self.response = self.protocol.accept(self.request)
+
+ if server_header is not None:
+ self.response.headers["Server"] = server_header
+
+ if process_response is not None:
+ try:
+ response = process_response(self, self.request, self.response)
+ except Exception as exc:
+ self.protocol.handshake_exc = exc
+ self.logger.error("opening handshake failed", exc_info=True)
+ self.response = self.protocol.reject(
+ http.HTTPStatus.INTERNAL_SERVER_ERROR,
+ (
+ "Failed to open a WebSocket connection.\n"
+ "See server log for more information.\n"
+ ),
+ )
+ else:
+ if response is not None:
+ self.response = response
+
+ self.protocol.send_response(self.response)
+
+ if self.protocol.state is not OPEN:
+ self.recv_events_thread.join(self.close_timeout)
+ self.close_socket()
+ self.recv_events_thread.join()
+
+ if self.protocol.handshake_exc is not None:
+ raise self.protocol.handshake_exc
+
+ def process_event(self, event: Event) -> None:
+ """
+ Process one incoming event.
+
+ """
+ # First event - handshake request.
+ if self.request is None:
+ assert isinstance(event, Request)
+ self.request = event
+ self.request_rcvd.set()
+ # Later events - frames.
+ else:
+ super().process_event(event)
+
+ def recv_events(self) -> None:
+ """
+ Read incoming data from the socket and process events.
+
+ """
+ try:
+ super().recv_events()
+ finally:
+ # If the connection is closed during the handshake, unblock it.
+ self.request_rcvd.set()
+
+
+class WebSocketServer:
+ """
+ WebSocket server returned by :func:`serve`.
+
+ This class mirrors the API of :class:`~socketserver.BaseServer`, notably the
+ :meth:`~socketserver.BaseServer.serve_forever` and
+ :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context
+ manager protocol.
+
+ Args:
+ socket: Server socket listening for new connections.
+ handler: Handler for one connection. Receives the socket and address
+ returned by :meth:`~socket.socket.accept`.
+ logger: Logger for this server.
+
+ """
+
+ def __init__(
+ self,
+ socket: socket.socket,
+ handler: Callable[[socket.socket, Any], None],
+ logger: Optional[LoggerLike] = None,
+ ):
+ self.socket = socket
+ self.handler = handler
+ if logger is None:
+ logger = logging.getLogger("websockets.server")
+ self.logger = logger
+ if sys.platform != "win32":
+ self.shutdown_watcher, self.shutdown_notifier = os.pipe()
+
+ def serve_forever(self) -> None:
+ """
+ See :meth:`socketserver.BaseServer.serve_forever`.
+
+ This method doesn't return. Calling :meth:`shutdown` from another thread
+ stops the server.
+
+ Typical use::
+
+ with serve(...) as server:
+ server.serve_forever()
+
+ """
+ poller = selectors.DefaultSelector()
+ poller.register(self.socket, selectors.EVENT_READ)
+ if sys.platform != "win32":
+ poller.register(self.shutdown_watcher, selectors.EVENT_READ)
+
+ while True:
+ poller.select()
+ try:
+ # If the socket is closed, this will raise an exception and exit
+ # the loop. So we don't need to check the return value of select().
+ sock, addr = self.socket.accept()
+ except OSError:
+ break
+ thread = threading.Thread(target=self.handler, args=(sock, addr))
+ thread.start()
+
+ def shutdown(self) -> None:
+ """
+ See :meth:`socketserver.BaseServer.shutdown`.
+
+ """
+ self.socket.close()
+ if sys.platform != "win32":
+ os.write(self.shutdown_notifier, b"x")
+
+ def fileno(self) -> int:
+ """
+ See :meth:`socketserver.BaseServer.fileno`.
+
+ """
+ return self.socket.fileno()
+
+ def __enter__(self) -> WebSocketServer:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_value: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ self.shutdown()
+
+
+def serve(
+ handler: Callable[[ServerConnection], None],
+ host: Optional[str] = None,
+ port: Optional[int] = None,
+ *,
+ # TCP/TLS — unix and path are only for unix_serve()
+ sock: Optional[socket.socket] = None,
+ ssl_context: Optional[ssl.SSLContext] = None,
+ unix: bool = False,
+ path: Optional[str] = None,
+ # WebSocket
+ origins: Optional[Sequence[Optional[Origin]]] = None,
+ extensions: Optional[Sequence[ServerExtensionFactory]] = None,
+ subprotocols: Optional[Sequence[Subprotocol]] = None,
+ select_subprotocol: Optional[
+ Callable[
+ [ServerConnection, Sequence[Subprotocol]],
+ Optional[Subprotocol],
+ ]
+ ] = None,
+ process_request: Optional[
+ Callable[
+ [ServerConnection, Request],
+ Optional[Response],
+ ]
+ ] = None,
+ process_response: Optional[
+ Callable[
+ [ServerConnection, Request, Response],
+ Optional[Response],
+ ]
+ ] = None,
+ server_header: Optional[str] = USER_AGENT,
+ compression: Optional[str] = "deflate",
+ # Timeouts
+ open_timeout: Optional[float] = 10,
+ close_timeout: Optional[float] = 10,
+ # Limits
+ max_size: Optional[int] = 2**20,
+ # Logging
+ logger: Optional[LoggerLike] = None,
+ # Escape hatch for advanced customization
+ create_connection: Optional[Type[ServerConnection]] = None,
+) -> WebSocketServer:
+ """
+ Create a WebSocket server listening on ``host`` and ``port``.
+
+ Whenever a client connects, the server creates a :class:`ServerConnection`,
+ performs the opening handshake, and delegates to the ``handler``.
+
+ The handler receives a :class:`ServerConnection` instance, which you can use
+ to send and receive messages.
+
+ Once the handler completes, either normally or with an exception, the server
+ performs the closing handshake and closes the connection.
+
+ :class:`WebSocketServer` mirrors the API of
+ :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure
+ that it will be closed and call the :meth:`~WebSocketServer.serve_forever`
+ method to serve requests::
+
+ def handler(websocket):
+ ...
+
+ with websockets.sync.server.serve(handler, ...) as server:
+ server.serve_forever()
+
+ Args:
+ handler: Connection handler. It receives the WebSocket connection,
+ which is a :class:`ServerConnection`, in argument.
+ host: Network interfaces the server binds to.
+ See :func:`~socket.create_server` for details.
+ port: TCP port the server listens on.
+ See :func:`~socket.create_server` for details.
+ sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``.
+ You may call :func:`socket.create_server` to create a suitable TCP
+ socket.
+ ssl_context: Configuration for enabling TLS on the connection.
+ origins: Acceptable values of the ``Origin`` header, for defending
+ against Cross-Site WebSocket Hijacking attacks. Include :obj:`None`
+ in the list if the lack of an origin is acceptable.
+ extensions: List of supported extensions, in order in which they
+ should be negotiated and run.
+ subprotocols: List of supported subprotocols, in order of decreasing
+ preference.
+ select_subprotocol: Callback for selecting a subprotocol among
+ those supported by the client and the server. It receives a
+ :class:`ServerConnection` (not a
+ :class:`~websockets.server.ServerProtocol`!) instance and a list of
+ subprotocols offered by the client. Other than the first argument,
+ it has the same behavior as the
+ :meth:`ServerProtocol.select_subprotocol
+ <websockets.server.ServerProtocol.select_subprotocol>` method.
+ process_request: Intercept the request during the opening handshake.
+ Return an HTTP response to force the response or :obj:`None` to
+ continue normally. When you force an HTTP 101 Continue response,
+ the handshake is successful. Else, the connection is aborted.
+ process_response: Intercept the response during the opening handshake.
+ Return an HTTP response to force the response or :obj:`None` to
+ continue normally. When you force an HTTP 101 Continue response,
+ the handshake is successful. Else, the connection is aborted.
+ server_header: Value of the ``Server`` response header.
+ It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to
+ :obj:`None` removes the header.
+ compression: The "permessage-deflate" extension is enabled by default.
+ Set ``compression`` to :obj:`None` to disable it. See the
+ :doc:`compression guide <../../topics/compression>` for details.
+ open_timeout: Timeout for opening connections in seconds.
+ :obj:`None` disables the timeout.
+ close_timeout: Timeout for closing connections in seconds.
+ :obj:`None` disables the timeout.
+ max_size: Maximum size of incoming messages in bytes.
+ :obj:`None` disables the limit.
+ logger: Logger for this server.
+ It defaults to ``logging.getLogger("websockets.server")``. See the
+ :doc:`logging guide <../../topics/logging>` for details.
+ create_connection: Factory for the :class:`ServerConnection` managing
+ the connection. Set it to a wrapper or a subclass to customize
+ connection handling.
+ """
+
+ # Process parameters
+
+ if subprotocols is not None:
+ validate_subprotocols(subprotocols)
+
+ if compression == "deflate":
+ extensions = enable_server_permessage_deflate(extensions)
+ elif compression is not None:
+ raise ValueError(f"unsupported compression: {compression}")
+
+ if create_connection is None:
+ create_connection = ServerConnection
+
+ # Bind socket and listen
+
+ if sock is None:
+ if unix:
+ if path is None:
+ raise TypeError("missing path argument")
+ sock = socket.create_server(path, family=socket.AF_UNIX)
+ else:
+ sock = socket.create_server((host, port))
+ else:
+ if path is not None:
+ raise TypeError("path and sock arguments are incompatible")
+
+ # Initialize TLS wrapper
+
+ if ssl_context is not None:
+ sock = ssl_context.wrap_socket(
+ sock,
+ server_side=True,
+ # Delay TLS handshake until after we set a timeout on the socket.
+ do_handshake_on_connect=False,
+ )
+
+ # Define request handler
+
+ def conn_handler(sock: socket.socket, addr: Any) -> None:
+ # Calculate timeouts on the TLS and WebSocket handshakes.
+ # The TLS timeout must be set on the socket, then removed
+ # to avoid conflicting with the WebSocket timeout in handshake().
+ deadline = Deadline(open_timeout)
+
+ try:
+ # Disable Nagle algorithm
+
+ if not unix:
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
+
+ # Perform TLS handshake
+
+ if ssl_context is not None:
+ sock.settimeout(deadline.timeout())
+ assert isinstance(sock, ssl.SSLSocket) # mypy cannot figure this out
+ sock.do_handshake()
+ sock.settimeout(None)
+
+ # Create a closure so that select_subprotocol has access to self.
+
+ protocol_select_subprotocol: Optional[
+ Callable[
+ [ServerProtocol, Sequence[Subprotocol]],
+ Optional[Subprotocol],
+ ]
+ ] = None
+
+ if select_subprotocol is not None:
+
+ def protocol_select_subprotocol(
+ protocol: ServerProtocol,
+ subprotocols: Sequence[Subprotocol],
+ ) -> Optional[Subprotocol]:
+ # mypy doesn't know that select_subprotocol is immutable.
+ assert select_subprotocol is not None
+ # Ensure this function is only used in the intended context.
+ assert protocol is connection.protocol
+ return select_subprotocol(connection, subprotocols)
+
+ # Initialize WebSocket connection
+
+ protocol = ServerProtocol(
+ origins=origins,
+ extensions=extensions,
+ subprotocols=subprotocols,
+ select_subprotocol=protocol_select_subprotocol,
+ state=CONNECTING,
+ max_size=max_size,
+ logger=logger,
+ )
+
+ # Initialize WebSocket protocol
+
+ assert create_connection is not None # help mypy
+ connection = create_connection(
+ sock,
+ protocol,
+ close_timeout=close_timeout,
+ )
+ # On failure, handshake() closes the socket, raises an exception, and
+ # logs it.
+ connection.handshake(
+ process_request,
+ process_response,
+ server_header,
+ deadline.timeout(),
+ )
+
+ except Exception:
+ sock.close()
+ return
+
+ try:
+ handler(connection)
+ except Exception:
+ protocol.logger.error("connection handler failed", exc_info=True)
+ connection.close(CloseCode.INTERNAL_ERROR)
+ else:
+ connection.close()
+
+ # Initialize server
+
+ return WebSocketServer(sock, conn_handler, logger)
+
+
+def unix_serve(
+ handler: Callable[[ServerConnection], Any],
+ path: Optional[str] = None,
+ **kwargs: Any,
+) -> WebSocketServer:
+ """
+ Create a WebSocket server listening on a Unix socket.
+
+ This function is identical to :func:`serve`, except the ``host`` and
+ ``port`` arguments are replaced by ``path``. It's only available on Unix.
+
+ It's useful for deploying a server behind a reverse proxy such as nginx.
+
+ Args:
+ handler: Connection handler. It receives the WebSocket connection,
+ which is a :class:`ServerConnection`, in argument.
+ path: File system path to the Unix socket.
+
+ """
+ return serve(handler, path=path, unix=True, **kwargs)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py
new file mode 100644
index 0000000000..471f32e19d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/sync/utils.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+import time
+from typing import Optional
+
+
+__all__ = ["Deadline"]
+
+
+class Deadline:
+ """
+ Manage timeouts across multiple steps.
+
+ Args:
+ timeout: Time available in seconds or :obj:`None` if there is no limit.
+
+ """
+
+ def __init__(self, timeout: Optional[float]) -> None:
+ self.deadline: Optional[float]
+ if timeout is None:
+ self.deadline = None
+ else:
+ self.deadline = time.monotonic() + timeout
+
+ def timeout(self, *, raise_if_elapsed: bool = True) -> Optional[float]:
+ """
+ Calculate a timeout from a deadline.
+
+ Args:
+ raise_if_elapsed (bool): Whether to raise :exc:`TimeoutError`
+ if the deadline lapsed.
+
+ Raises:
+ TimeoutError: If the deadline lapsed.
+
+ Returns:
+ Time left in seconds or :obj:`None` if there is no limit.
+
+ """
+ if self.deadline is None:
+ return None
+ timeout = self.deadline - time.monotonic()
+ if raise_if_elapsed and timeout <= 0:
+ raise TimeoutError("timed out")
+ return timeout
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
index e672ba0069..cc3e3ec0d9 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/typing.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import http
import logging
from typing import List, NewType, Optional, Tuple, Union
@@ -7,6 +8,7 @@ from typing import List, NewType, Optional, Tuple, Union
__all__ = [
"Data",
"LoggerLike",
+ "StatusLike",
"Origin",
"Subprotocol",
"ExtensionName",
@@ -30,6 +32,11 @@ LoggerLike = Union[logging.Logger, logging.LoggerAdapter]
"""Types accepted where a :class:`~logging.Logger` is expected."""
+StatusLike = Union[http.HTTPStatus, int]
+"""
+Types accepted where an :class:`~http.HTTPStatus` is expected."""
+
+
Origin = NewType("Origin", str)
"""Value of a ``Origin`` header."""
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
index fff0c38064..385090f66a 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/uri.py
@@ -33,8 +33,8 @@ class WebSocketURI:
port: int
path: str
query: str
- username: Optional[str]
- password: Optional[str]
+ username: Optional[str] = None
+ password: Optional[str] = None
@property
def resource_name(self) -> str:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
index c30bfd68f3..d1c99458e2 100644
--- a/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
+++ b/testing/web-platform/tests/tools/third_party/websockets/src/websockets/version.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import importlib.metadata
+
__all__ = ["tag", "version", "commit"]
@@ -18,7 +20,7 @@ __all__ = ["tag", "version", "commit"]
released = True
-tag = version = commit = "10.3"
+tag = version = commit = "12.0"
if not released: # pragma: no cover
@@ -44,7 +46,11 @@ if not released: # pragma: no cover
text=True,
).stdout.strip()
# subprocess.run raises FileNotFoundError if git isn't on $PATH.
- except (FileNotFoundError, subprocess.CalledProcessError):
+ except (
+ FileNotFoundError,
+ subprocess.CalledProcessError,
+ subprocess.TimeoutExpired,
+ ):
pass
else:
description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)"
@@ -56,8 +62,6 @@ if not released: # pragma: no cover
# Read version from package metadata if it is installed.
try:
- import importlib.metadata # move up when dropping Python 3.7
-
return importlib.metadata.version("websockets")
except ImportError:
pass