summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party/websockets/docs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/websockets/docs')
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/Makefile23
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.pngbin4069 -> 0 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/conf.py171
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst69
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst101
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst161
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst21
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst336
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst31
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst87
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst294
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst30
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst177
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst61
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst183
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst56
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst215
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst84
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst110
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst170
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst172
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst322
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst131
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/index.rst75
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst46
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst591
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst565
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst290
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/make.bat35
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst1230
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst12
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst4
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst112
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst64
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst54
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst113
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst66
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst6
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst60
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst187
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst90
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst58
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst64
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst62
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst49
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst60
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst24
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt8
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt85
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst348
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst348
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst222
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst181
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg63
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst572
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst18
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle25
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst245
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst48
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst20
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle37
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg3
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst41
-rw-r--r--testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst116
70 files changed, 9587 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile
new file mode 100644
index 0000000000..0458706458
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/Makefile
@@ -0,0 +1,23 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+livehtml:
+ sphinx-autobuild --watch "$(SOURCEDIR)/../src" "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png b/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png
deleted file mode 100644
index 317dc4d985..0000000000
--- a/testing/web-platform/tests/tools/third_party/websockets/docs/_static/tidelift.png
+++ /dev/null
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py
new file mode 100644
index 0000000000..9d61dc7173
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/conf.py
@@ -0,0 +1,171 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import datetime
+import importlib
+import inspect
+import os
+import subprocess
+import sys
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.join(os.path.abspath(".."), "src"))
+
+
+# -- Project information -----------------------------------------------------
+
+project = "websockets"
+copyright = f"2013-{datetime.date.today().year}, Aymeric Augustin and contributors"
+author = "Aymeric Augustin"
+
+from websockets.version import tag as version, version as release
+
+
+# -- General configuration ---------------------------------------------------
+
+nitpicky = True
+
+nitpick_ignore = [
+ # topics/design.rst discusses undocumented APIs
+ ("py:meth", "client.WebSocketClientProtocol.handshake"),
+ ("py:meth", "server.WebSocketServerProtocol.handshake"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.is_client"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.messages"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.close_connection"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.close_connection_task"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.transfer_data"),
+ ("py:attr", "legacy.protocol.WebSocketCommonProtocol.transfer_data_task"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_open"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.ensure_open"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.fail_connection"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_lost"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.read_message"),
+ ("py:meth", "legacy.protocol.WebSocketCommonProtocol.write_frame"),
+]
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.linkcode",
+ "sphinx.ext.napoleon",
+ "sphinx_copybutton",
+ "sphinx_inline_tabs",
+ "sphinxcontrib.spelling",
+ "sphinxcontrib_trio",
+ "sphinxext.opengraph",
+]
+# It is currently inconvenient to install PyEnchant on Apple Silicon.
+try:
+ import sphinxcontrib.spelling
+except ImportError:
+ extensions.remove("sphinxcontrib.spelling")
+
+autodoc_typehints = "description"
+
+autodoc_typehints_description_target = "documented"
+
+# Workaround for https://github.com/sphinx-doc/sphinx/issues/9560
+from sphinx.domains.python import PythonDomain
+
+assert PythonDomain.object_types["data"].roles == ("data", "obj")
+PythonDomain.object_types["data"].roles = ("data", "class", "obj")
+
+intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
+
+spelling_show_suggestions = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# Configure viewcode extension.
+from websockets.version import commit
+
+code_url = f"https://github.com/python-websockets/websockets/blob/{commit}"
+
+def linkcode_resolve(domain, info):
+ # Non-linkable objects from the starter kit in the tutorial.
+ if domain == "js" or info["module"] == "connect4":
+ return
+
+ assert domain == "py", "expected only Python objects"
+
+ mod = importlib.import_module(info["module"])
+ if "." in info["fullname"]:
+ objname, attrname = info["fullname"].split(".")
+ obj = getattr(mod, objname)
+ try:
+ # object is a method of a class
+ obj = getattr(obj, attrname)
+ except AttributeError:
+ # object is an attribute of a class
+ return None
+ else:
+ obj = getattr(mod, info["fullname"])
+
+ try:
+ file = inspect.getsourcefile(obj)
+ lines = inspect.getsourcelines(obj)
+ except TypeError:
+ # e.g. object is a typing.Union
+ return None
+ file = os.path.relpath(file, os.path.abspath(".."))
+ if not file.startswith("src/websockets"):
+ # e.g. object is a typing.NewType
+ return None
+ start, end = lines[1], lines[1] + len(lines[0]) - 1
+
+ return f"{code_url}/{file}#L{start}-L{end}"
+
+# Configure opengraph extension
+
+# Social cards don't support the SVG logo. Also, the text preview looks bad.
+ogp_social_cards = {"enable": False}
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = "furo"
+
+html_theme_options = {
+ "light_css_variables": {
+ "color-brand-primary": "#306998", # blue from logo
+ "color-brand-content": "#0b487a", # blue more saturated and less dark
+ },
+ "dark_css_variables": {
+ "color-brand-primary": "#ffd43bcc", # yellow from logo, more muted than content
+ "color-brand-content": "#ffd43bd9", # yellow from logo, transparent like text
+ },
+ "sidebar_hide_name": True,
+}
+
+html_logo = "_static/websockets.svg"
+
+html_favicon = "_static/favicon.ico"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ["_static"]
+
+html_copy_source = False
+
+html_show_sphinx = False
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst
new file mode 100644
index 0000000000..e77f50addd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/asyncio.rst
@@ -0,0 +1,69 @@
+Using asyncio
+=============
+
+.. currentmodule:: websockets
+
+How do I run two coroutines in parallel?
+----------------------------------------
+
+You must start two tasks, which the event loop will run concurrently. You can
+achieve this with :func:`asyncio.gather` or :func:`asyncio.create_task`.
+
+Keep track of the tasks and make sure they terminate or you cancel them when
+the connection terminates.
+
+Why does my program never receive any messages?
+-----------------------------------------------
+
+Your program runs a coroutine that never yields control to the event loop. The
+coroutine that receives messages never gets a chance to run.
+
+Putting an ``await`` statement in a ``for`` or a ``while`` loop isn't enough
+to yield control. Awaiting a coroutine may yield control, but there's no
+guarantee that it will.
+
+For example, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` only yields
+control when send buffers are full, which never happens in most practical
+cases.
+
+If you run a loop that contains only synchronous operations and
+a :meth:`~legacy.protocol.WebSocketCommonProtocol.send` call, you must yield
+control explicitly with :func:`asyncio.sleep`::
+
+ async def producer(websocket):
+ message = generate_next_message()
+ await websocket.send(message)
+ await asyncio.sleep(0) # yield control to the event loop
+
+:func:`asyncio.sleep` always suspends the current task, allowing other tasks
+to run. This behavior is documented precisely because it isn't expected from
+every coroutine.
+
+See `issue 867`_.
+
+.. _issue 867: https://github.com/python-websockets/websockets/issues/867
+
+Why am I having problems with threads?
+--------------------------------------
+
+If you choose websockets' default implementation based on :mod:`asyncio`, then
+you shouldn't use threads. Indeed, choosing :mod:`asyncio` to handle concurrency
+is mutually exclusive with :mod:`threading`.
+
+If you believe that you need to run websockets in a thread and some logic in
+another thread, you should run that logic in a :class:`~asyncio.Task` instead.
+If it blocks the event loop, :meth:`~asyncio.loop.run_in_executor` will help.
+
+This question is really about :mod:`asyncio`. Please review the advice about
+:ref:`asyncio-multithreading` in the Python documentation.
+
+Why does my simple program misbehave mysteriously?
+--------------------------------------------------
+
+You are using :func:`time.sleep` instead of :func:`asyncio.sleep`, which
+blocks the event loop and prevents asyncio from operating normally.
+
+This may lead to messages getting send but not received, to connection
+timeouts, and to unexpected results of shotgun debugging e.g. adding an
+unnecessary call to :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+makes the program functional.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst
new file mode 100644
index 0000000000..c590ac107d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/client.rst
@@ -0,0 +1,101 @@
+Client
+======
+
+.. currentmodule:: websockets
+
+Why does the client close the connection prematurely?
+-----------------------------------------------------
+
+You're exiting the context manager prematurely. Wait for the work to be
+finished before exiting.
+
+For example, if your code has a structure similar to::
+
+ async with connect(...) as websocket:
+ asyncio.create_task(do_some_work())
+
+change it to::
+
+ async with connect(...) as websocket:
+ await do_some_work()
+
+How do I access HTTP headers?
+-----------------------------
+
+Once the connection is established, HTTP headers are available in
+:attr:`~client.WebSocketClientProtocol.request_headers` and
+:attr:`~client.WebSocketClientProtocol.response_headers`.
+
+How do I set HTTP headers?
+--------------------------
+
+To set the ``Origin``, ``Sec-WebSocket-Extensions``, or
+``Sec-WebSocket-Protocol`` headers in the WebSocket handshake request, use the
+``origin``, ``extensions``, or ``subprotocols`` arguments of
+:func:`~client.connect`.
+
+To override the ``User-Agent`` header, use the ``user_agent_header`` argument.
+Set it to :obj:`None` to remove the header.
+
+To set other HTTP headers, for example the ``Authorization`` header, use the
+``extra_headers`` argument::
+
+ async with connect(..., extra_headers={"Authorization": ...}) as websocket:
+ ...
+
+In the :mod:`threading` API, this argument is named ``additional_headers``::
+
+ with connect(..., additional_headers={"Authorization": ...}) as websocket:
+ ...
+
+How do I force the IP address that the client connects to?
+----------------------------------------------------------
+
+Use the ``host`` argument of :meth:`~asyncio.loop.create_connection`::
+
+ await websockets.connect("ws://example.com", host="192.168.0.1")
+
+:func:`~client.connect` accepts the same arguments as
+:meth:`~asyncio.loop.create_connection`.
+
+How do I close a connection?
+----------------------------
+
+The easiest is to use :func:`~client.connect` as a context manager::
+
+ async with connect(...) as websocket:
+ ...
+
+The connection is closed when exiting the context manager.
+
+How do I reconnect when the connection drops?
+---------------------------------------------
+
+Use :func:`~client.connect` as an asynchronous iterator::
+
+ async for websocket in websockets.connect(...):
+ try:
+ ...
+ except websockets.ConnectionClosed:
+ continue
+
+Make sure you handle exceptions in the ``async for`` loop. Uncaught exceptions
+will break out of the loop.
+
+How do I stop a client that is processing messages in a loop?
+-------------------------------------------------------------
+
+You can close the connection.
+
+Here's an example that terminates cleanly when it receives SIGTERM on Unix:
+
+.. literalinclude:: ../../example/faq/shutdown_client.py
+ :emphasize-lines: 10-13
+
+How do I disable TLS/SSL certificate verification?
+--------------------------------------------------
+
+Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`.
+
+:func:`~client.connect` accepts the same arguments as
+:meth:`~asyncio.loop.create_connection`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst
new file mode 100644
index 0000000000..2c63c4f36f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/common.rst
@@ -0,0 +1,161 @@
+Both sides
+==========
+
+.. currentmodule:: websockets
+
+What does ``ConnectionClosedError: no close frame received or sent`` mean?
+--------------------------------------------------------------------------
+
+If you're seeing this traceback in the logs of a server:
+
+.. code-block:: pytb
+
+ connection handler failed
+ Traceback (most recent call last):
+ ...
+ asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of 2 expected bytes
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: no close frame received or sent
+
+or if a client crashes with this traceback:
+
+.. code-block:: pytb
+
+ Traceback (most recent call last):
+ ...
+ ConnectionResetError: [Errno 54] Connection reset by peer
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: no close frame received or sent
+
+it means that the TCP connection was lost. As a consequence, the WebSocket
+connection was closed without receiving and sending a close frame, which is
+abnormal.
+
+You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it
+from being logged.
+
+There are several reasons why long-lived connections may be lost:
+
+* End-user devices tend to lose network connectivity often and unpredictably
+ because they can move out of wireless network coverage, get unplugged from
+ a wired network, enter airplane mode, be put to sleep, etc.
+* HTTP load balancers or proxies that aren't configured for long-lived
+ connections may terminate connections after a short amount of time, usually
+ 30 seconds, despite websockets' keepalive mechanism.
+
+If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to
+see when and how connections are closed.
+
+What does ``ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received`` mean?
+---------------------------------------------------------------------------------------------------------------------
+
+If you're seeing this traceback in the logs of a server:
+
+.. code-block:: pytb
+
+ connection handler failed
+ Traceback (most recent call last):
+ ...
+ asyncio.exceptions.CancelledError
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
+
+or if a client crashes with this traceback:
+
+.. code-block:: pytb
+
+ Traceback (most recent call last):
+ ...
+ asyncio.exceptions.CancelledError
+
+ The above exception was the direct cause of the following exception:
+
+ Traceback (most recent call last):
+ ...
+ websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received
+
+it means that the WebSocket connection suffered from excessive latency and was
+closed after reaching the timeout of websockets' keepalive mechanism.
+
+You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it
+from being logged.
+
+There are two main reasons why latency may increase:
+
+* Poor network connectivity.
+* More traffic than the recipient can handle.
+
+See the discussion of :doc:`timeouts <../topics/timeouts>` for details.
+
+If websockets' default timeout of 20 seconds is too short for your use case,
+you can adjust it with the ``ping_timeout`` argument.
+
+How do I set a timeout on :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`?
+--------------------------------------------------------------------------------
+
+On Python ≥ 3.11, use :func:`asyncio.timeout`::
+
+ async with asyncio.timeout(timeout=10):
+ message = await websocket.recv()
+
+On older versions of Python, use :func:`asyncio.wait_for`::
+
+ message = await asyncio.wait_for(websocket.recv(), timeout=10)
+
+This technique works for most APIs. When it doesn't, for example with
+asynchronous context managers, websockets provides an ``open_timeout`` argument.
+
+How can I pass arguments to a custom protocol subclass?
+-------------------------------------------------------
+
+You can bind additional arguments to the protocol factory with
+:func:`functools.partial`::
+
+ import asyncio
+ import functools
+ import websockets
+
+ class MyServerProtocol(websockets.WebSocketServerProtocol):
+ def __init__(self, *args, extra_argument=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ # do something with extra_argument
+
+ create_protocol = functools.partial(MyServerProtocol, extra_argument=42)
+ start_server = websockets.serve(..., create_protocol=create_protocol)
+
+This example was for a server. The same pattern applies on a client.
+
+How do I keep idle connections open?
+------------------------------------
+
+websockets sends pings at 20 seconds intervals to keep the connection open.
+
+It closes the connection if it doesn't get a pong within 20 seconds.
+
+You can adjust this behavior with ``ping_interval`` and ``ping_timeout``.
+
+See :doc:`../topics/timeouts` for details.
+
+How do I respond to pings?
+--------------------------
+
+If you are referring to Ping_ and Pong_ frames defined in the WebSocket
+protocol, don't bother, because websockets handles them for you.
+
+.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
+.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+
+If you are connecting to a server that defines its own heartbeat at the
+application level, then you need to build that logic into your application.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst
new file mode 100644
index 0000000000..9d5b0d538a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/index.rst
@@ -0,0 +1,21 @@
+Frequently asked questions
+==========================
+
+.. currentmodule:: websockets
+
+.. admonition:: Many questions asked in websockets' issue tracker are really
+ about :mod:`asyncio`.
+ :class: seealso
+
+ Python's documentation about `developing with asyncio`_ is a good
+ complement.
+
+ .. _developing with asyncio: https://docs.python.org/3/library/asyncio-dev.html
+
+.. toctree::
+
+ server
+ client
+ common
+ asyncio
+ misc
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst
new file mode 100644
index 0000000000..ee5ad23728
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/misc.rst
@@ -0,0 +1,49 @@
+Miscellaneous
+=============
+
+.. currentmodule:: websockets
+
+Why do I get the error: ``module 'websockets' has no attribute '...'``?
+.......................................................................
+
+Often, this is because you created a script called ``websockets.py`` in your
+current working directory. Then ``import websockets`` imports this module
+instead of the websockets library.
+
+.. _real-import-paths:
+
+Why is the default implementation located in ``websockets.legacy``?
+...................................................................
+
+This is an artifact of websockets' history. For its first eight years, only the
+:mod:`asyncio` implementation existed. Then, the Sans-I/O implementation was
+added. Moving the code in a ``legacy`` submodule eased this refactoring and
+optimized maintainability.
+
+All public APIs were kept at their original locations. ``websockets.legacy``
+isn't a public API. It's only visible in the source code and in stack traces.
+There is no intent to deprecate this implementation — at least until a superior
+alternative exists.
+
+Why is websockets slower than another library in my benchmark?
+..............................................................
+
+Not all libraries are as feature-complete as websockets. For a fair benchmark,
+you should disable features that the other library doesn't provide. Typically,
+you may need to disable:
+
+* Compression: set ``compression=None``
+* Keepalive: set ``ping_interval=None``
+* UTF-8 decoding: send ``bytes`` rather than ``str``
+
+If websockets is still slower than another Python library, please file a bug.
+
+Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks?
+............................................................................
+
+No, there aren't.
+
+websockets provides high-level, coroutine-based APIs. Compared to callbacks,
+coroutines make it easier to manage control flow in concurrent code.
+
+If you prefer callback-based APIs, you should use another library.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst
new file mode 100644
index 0000000000..08b412d306
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/faq/server.rst
@@ -0,0 +1,336 @@
+Server
+======
+
+.. currentmodule:: websockets
+
+Why does the server close the connection prematurely?
+-----------------------------------------------------
+
+Your connection handler exits prematurely. Wait for the work to be finished
+before returning.
+
+For example, if your handler has a structure similar to::
+
+ async def handler(websocket):
+ asyncio.create_task(do_some_work())
+
+change it to::
+
+ async def handler(websocket):
+ await do_some_work()
+
+Why does the server close the connection after one message?
+-----------------------------------------------------------
+
+Your connection handler exits after processing one message. Write a loop to
+process multiple messages.
+
+For example, if your handler looks like this::
+
+ async def handler(websocket):
+ print(websocket.recv())
+
+change it like this::
+
+ async def handler(websocket):
+ async for message in websocket:
+ print(message)
+
+*Don't feel bad if this happens to you — it's the most common question in
+websockets' issue tracker :-)*
+
+Why can only one client connect at a time?
+------------------------------------------
+
+Your connection handler blocks the event loop. Look for blocking calls.
+
+Any call that may take some time must be asynchronous.
+
+For example, this connection handler prevents the event loop from running during
+one second::
+
+ async def handler(websocket):
+ time.sleep(1)
+ ...
+
+Change it to::
+
+ async def handler(websocket):
+ await asyncio.sleep(1)
+ ...
+
+In addition, calling a coroutine doesn't guarantee that it will yield control to
+the event loop.
+
+For example, this connection handler blocks the event loop by sending messages
+continuously::
+
+ async def handler(websocket):
+ while True:
+ await websocket.send("firehose!")
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send` completes synchronously as
+long as there's space in send buffers. The event loop never runs. (This pattern
+is uncommon in real-world applications. It occurs mostly in toy programs.)
+
+You can avoid the issue by yielding control to the event loop explicitly::
+
+ async def handler(websocket):
+ while True:
+ await websocket.send("firehose!")
+ await asyncio.sleep(0)
+
+All this is part of learning asyncio. It isn't specific to websockets.
+
+See also Python's documentation about `running blocking code`_.
+
+.. _running blocking code: https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code
+
+.. _send-message-to-all-users:
+
+How do I send a message to all users?
+-------------------------------------
+
+Record all connections in a global variable::
+
+ CONNECTIONS = set()
+
+ async def handler(websocket):
+ CONNECTIONS.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CONNECTIONS.remove(websocket)
+
+Then, call :func:`~websockets.broadcast`::
+
+ import websockets
+
+ def message_all(message):
+ websockets.broadcast(CONNECTIONS, message)
+
+If you're running multiple server processes, make sure you call ``message_all``
+in each process.
+
+.. _send-message-to-single-user:
+
+How do I send a message to a single user?
+-----------------------------------------
+
+Record connections in a global variable, keyed by user identifier::
+
+ CONNECTIONS = {}
+
+ async def handler(websocket):
+ user_id = ... # identify user in your app's context
+ CONNECTIONS[user_id] = websocket
+ try:
+ await websocket.wait_closed()
+ finally:
+ del CONNECTIONS[user_id]
+
+Then, call :meth:`~legacy.protocol.WebSocketCommonProtocol.send`::
+
+ async def message_user(user_id, message):
+ websocket = CONNECTIONS[user_id] # raises KeyError if user disconnected
+ await websocket.send(message) # may raise websockets.ConnectionClosed
+
+Add error handling according to the behavior you want if the user disconnected
+before the message could be sent.
+
+This example supports only one connection per user. To support concurrent
+connections by the same user, you can change ``CONNECTIONS`` to store a set of
+connections for each user.
+
+If you're running multiple server processes, call ``message_user`` in each
+process. The process managing the user's connection sends the message; other
+processes do nothing.
+
+When you reach a scale where server processes cannot keep up with the stream of
+all messages, you need a better architecture. For example, you could deploy an
+external publish / subscribe system such as Redis_. Server processes would
+subscribe their clients. Then, they would receive messages only for the
+connections that they're managing.
+
+.. _Redis: https://redis.io/
+
+How do I send a message to a channel, a topic, or some users?
+-------------------------------------------------------------
+
+websockets doesn't provide built-in publish / subscribe functionality.
+
+Record connections in a global variable, keyed by user identifier, as shown in
+:ref:`How do I send a message to a single user?<send-message-to-single-user>`
+
+Then, build the set of recipients and broadcast the message to them, as shown in
+:ref:`How do I send a message to all users?<send-message-to-all-users>`
+
+:doc:`../howto/django` contains a complete implementation of this pattern.
+
+Again, as you scale, you may reach the performance limits of a basic in-process
+implementation. You may need an external publish / subscribe system like Redis_.
+
+.. _Redis: https://redis.io/
+
+How do I pass arguments to the connection handler?
+--------------------------------------------------
+
+You can bind additional arguments to the connection handler with
+:func:`functools.partial`::
+
+ import asyncio
+ import functools
+ import websockets
+
+ async def handler(websocket, extra_argument):
+ ...
+
+ bound_handler = functools.partial(handler, extra_argument=42)
+ start_server = websockets.serve(bound_handler, ...)
+
+Another way to achieve this result is to define the ``handler`` coroutine in
+a scope where the ``extra_argument`` variable exists instead of injecting it
+through an argument.
+
+How do I access the request path?
+---------------------------------
+
+It is available in the :attr:`~server.WebSocketServerProtocol.path` attribute.
+
+You may route a connection to different handlers depending on the request path::
+
+ async def handler(websocket):
+ if websocket.path == "/blue":
+ await blue_handler(websocket)
+ elif websocket.path == "/green":
+ await green_handler(websocket)
+ else:
+ # No handler for this path; close the connection.
+ return
+
+You may also route the connection based on the first message received from the
+client, as shown in the :doc:`tutorial <../intro/tutorial2>`. When you want to
+authenticate the connection before routing it, this is usually more convenient.
+
+Generally speaking, there is far less emphasis on the request path in WebSocket
+servers than in HTTP servers. When a WebSocket server provides a single endpoint,
+it may ignore the request path entirely.
+
+How do I access HTTP headers?
+-----------------------------
+
+To access HTTP headers during the WebSocket handshake, you can override
+:attr:`~server.WebSocketServerProtocol.process_request`::
+
+ async def process_request(self, path, request_headers):
+ authorization = request_headers["Authorization"]
+
+Once the connection is established, HTTP headers are available in
+:attr:`~server.WebSocketServerProtocol.request_headers` and
+:attr:`~server.WebSocketServerProtocol.response_headers`::
+
+ async def handler(websocket):
+ authorization = websocket.request_headers["Authorization"]
+
+How do I set HTTP headers?
+--------------------------
+
+To set the ``Sec-WebSocket-Extensions`` or ``Sec-WebSocket-Protocol`` headers in
+the WebSocket handshake response, use the ``extensions`` or ``subprotocols``
+arguments of :func:`~server.serve`.
+
+To override the ``Server`` header, use the ``server_header`` argument. Set it to
+:obj:`None` to remove the header.
+
+To set other HTTP headers, use the ``extra_headers`` argument.
+
+How do I get the IP address of the client?
+------------------------------------------
+
+It's available in :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address`::
+
+ async def handler(websocket):
+ remote_ip = websocket.remote_address[0]
+
+How do I set the IP addresses that my server listens on?
+--------------------------------------------------------
+
+Use the ``host`` argument of :meth:`~asyncio.loop.create_server`::
+
+ await websockets.serve(handler, host="192.168.0.1", port=8080)
+
+:func:`~server.serve` accepts the same arguments as
+:meth:`~asyncio.loop.create_server`.
+
+What does ``OSError: [Errno 99] error while attempting to bind on address ('::1', 80, 0, 0): address not available`` mean?
+--------------------------------------------------------------------------------------------------------------------------
+
+You are calling :func:`~server.serve` without a ``host`` argument in a context
+where IPv6 isn't available.
+
+To listen only on IPv4, specify ``host="0.0.0.0"`` or ``family=socket.AF_INET``.
+
+Refer to the documentation of :meth:`~asyncio.loop.create_server` for details.
+
+How do I close a connection?
+----------------------------
+
+websockets takes care of closing the connection when the handler exits.
+
+How do I stop a server?
+-----------------------
+
+Exit the :func:`~server.serve` context manager.
+
+Here's an example that terminates cleanly when it receives SIGTERM on Unix:
+
+.. literalinclude:: ../../example/faq/shutdown_server.py
+ :emphasize-lines: 12-15,18
+
+How do I stop a server while keeping existing connections open?
+---------------------------------------------------------------
+
+Call the server's :meth:`~server.WebSocketServer.close` method with
+``close_connections=False``.
+
+Here's how to adapt the example just above::
+
+ async def server():
+ ...
+
+ server = await websockets.serve(echo, "localhost", 8765)
+ await stop
+ await server.close(close_connections=False)
+
+How do I implement a health check?
+----------------------------------
+
+Intercept WebSocket handshake requests with the
+:meth:`~server.WebSocketServerProtocol.process_request` hook.
+
+When a request is sent to the health check endpoint, treat is as an HTTP request
+and return a ``(status, headers, body)`` tuple, as in this example:
+
+.. literalinclude:: ../../example/faq/health_check_server.py
+ :emphasize-lines: 7-9,18
+
+How do I run HTTP and WebSocket servers on the same port?
+---------------------------------------------------------
+
+You don't.
+
+HTTP and WebSocket have widely different operational characteristics. Running
+them with the same server becomes inconvenient when you scale.
+
+Providing an HTTP server is out of scope for websockets. It only aims at
+providing a WebSocket server.
+
+There's limited support for returning HTTP responses with the
+:attr:`~server.WebSocketServerProtocol.process_request` hook.
+
+If you need more, pick an HTTP server and run it separately.
+
+Alternatively, pick an HTTP framework that builds on top of ``websockets`` to
+support WebSocket connections, like Sanic_.
+
+.. _Sanic: https://sanicframework.org/en/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst
new file mode 100644
index 0000000000..fc736a5918
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/autoreload.rst
@@ -0,0 +1,31 @@
+Reload on code changes
+======================
+
+When developing a websockets server, you may run it locally to test changes.
+Unfortunately, whenever you want to try a new version of the code, you must
+stop the server and restart it, which slows down your development process.
+
+Web frameworks such as Django or Flask provide a development server that
+reloads the application automatically when you make code changes. There is no
+such functionality in websockets because it's designed for production rather
+than development.
+
+However, you can achieve the same result easily.
+
+Install watchdog_ with the ``watchmedo`` shell utility:
+
+.. code-block:: console
+
+ $ pip install 'watchdog[watchmedo]'
+
+.. _watchdog: https://pypi.org/project/watchdog/
+
+Run your server with ``watchmedo auto-restart``:
+
+.. code-block:: console
+
+ $ watchmedo auto-restart --pattern "*.py" --recursive --signal SIGTERM \
+ python app.py
+
+This example assumes that the server is defined in a script called ``app.py``.
+Adapt it as necessary.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst
new file mode 100644
index 0000000000..95b551f673
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst
@@ -0,0 +1,87 @@
+Cheat sheet
+===========
+
+.. currentmodule:: websockets
+
+Server
+------
+
+* Write a coroutine that handles a single connection. It receives a WebSocket
+ protocol instance and the URI path in argument.
+
+ * Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send
+ messages at any time.
+
+ * When :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` or
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` raises
+ :exc:`~exceptions.ConnectionClosed`, clean up and exit. If you started
+ other :class:`asyncio.Task`, terminate them before exiting.
+
+ * If you aren't awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`,
+ consider awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`
+ to detect quickly when the connection is closed.
+
+ * You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't
+ needed in general.
+
+* Create a server with :func:`~server.serve` which is similar to asyncio's
+ :meth:`~asyncio.loop.create_server`. You can also use it as an asynchronous
+ context manager.
+
+ * The server takes care of establishing connections, then lets the handler
+ execute the application logic, and finally closes the connection after the
+ handler exits normally or with an exception.
+
+ * For advanced customization, you may subclass
+ :class:`~server.WebSocketServerProtocol` and pass either this subclass or
+ a factory function as the ``create_protocol`` argument.
+
+Client
+------
+
+* Create a client with :func:`~client.connect` which is similar to asyncio's
+ :meth:`~asyncio.loop.create_connection`. You can also use it as an
+ asynchronous context manager.
+
+ * For advanced customization, you may subclass
+ :class:`~client.WebSocketClientProtocol` and pass either this subclass or
+ a factory function as the ``create_protocol`` argument.
+
+* Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send messages
+ at any time.
+
+* You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't
+ needed in general.
+
+* If you aren't using :func:`~client.connect` as a context manager, call
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.close` to terminate the connection.
+
+.. _debugging:
+
+Debugging
+---------
+
+If you don't understand what websockets is doing, enable logging::
+
+ import logging
+ logger = logging.getLogger('websockets')
+ logger.setLevel(logging.DEBUG)
+ logger.addHandler(logging.StreamHandler())
+
+The logs contain:
+
+* Exceptions in the connection handler at the ``ERROR`` level
+* Exceptions in the opening or closing handshake at the ``INFO`` level
+* All frames at the ``DEBUG`` level — this can be very verbose
+
+If you're new to ``asyncio``, you will certainly encounter issues that are
+related to asynchronous programming in general rather than to websockets in
+particular. Fortunately Python's official documentation provides advice to
+`develop with asyncio`_. Check it out: it's invaluable!
+
+.. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst
new file mode 100644
index 0000000000..e3da0a878b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/django.rst
@@ -0,0 +1,294 @@
+Integrate with Django
+=====================
+
+If you're looking at adding real-time capabilities to a Django project with
+WebSocket, you have two main options.
+
+1. Using Django Channels_, a project adding WebSocket to Django, among other
+ features. This approach is fully supported by Django. However, it requires
+ switching to a new deployment architecture.
+
+2. Deploying a separate WebSocket server next to your Django project. This
+ technique is well suited when you need to add a small set of real-time
+ features — maybe a notification service — to an HTTP application.
+
+.. _Channels: https://channels.readthedocs.io/
+
+This guide shows how to implement the second technique with websockets. It
+assumes familiarity with Django.
+
+Authenticate connections
+------------------------
+
+Since the websockets server runs outside of Django, we need to integrate it
+with ``django.contrib.auth``.
+
+We will generate authentication tokens in the Django project. Then we will
+send them to the websockets server, where they will authenticate the user.
+
+Generating a token for the current user and making it available in the browser
+is up to you. You could render the token in a template or fetch it with an API
+call.
+
+Refer to the topic guide on :doc:`authentication <../topics/authentication>`
+for details on this design.
+
+Generate tokens
+...............
+
+We want secure, short-lived tokens containing the user ID. We'll rely on
+`django-sesame`_, a small library designed exactly for this purpose.
+
+.. _django-sesame: https://github.com/aaugustin/django-sesame
+
+Add django-sesame to the dependencies of your Django project, install it, and
+configure it in the settings of the project:
+
+.. code-block:: python
+
+ AUTHENTICATION_BACKENDS = [
+ "django.contrib.auth.backends.ModelBackend",
+ "sesame.backends.ModelBackend",
+ ]
+
+(If your project already uses another authentication backend than the default
+``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.)
+
+You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for
+authenticating users in the Django server, while we're authenticating them in
+the websockets server.
+
+We'd like our tokens to be valid for 30 seconds. We expect web pages to load
+and to establish the WebSocket connection within this delay. Configure
+django-sesame accordingly in the settings of your Django project:
+
+.. code-block:: python
+
+ SESAME_MAX_AGE = 30
+
+If you expect your web site to load faster for all clients, a shorter lifespan
+is possible. However, in the context of this document, it would make manual
+testing more difficult.
+
+You could also enable single-use tokens. However, this would update the last
+login date of the user every time a WebSocket connection is established. This
+doesn't seem like a good idea, both in terms of behavior and in terms of
+performance.
+
+Now you can generate tokens in a ``django-admin shell`` as follows:
+
+.. code-block:: pycon
+
+ >>> from django.contrib.auth import get_user_model
+ >>> User = get_user_model()
+ >>> user = User.objects.get(username="<your username>")
+ >>> from sesame.utils import get_token
+ >>> get_token(user)
+ '<your token>'
+
+Keep this console open: since tokens expire after 30 seconds, you'll have to
+generate a new token every time you want to test connecting to the server.
+
+Validate tokens
+...............
+
+Let's move on to the websockets server.
+
+Add websockets to the dependencies of your Django project and install it.
+Indeed, we're going to reuse the environment of the Django project, so we can
+call its APIs in the websockets server.
+
+Now here's how to implement authentication.
+
+.. literalinclude:: ../../example/django/authentication.py
+
+Let's unpack this code.
+
+We're calling ``django.setup()`` before doing anything with Django because
+we're using Django in a `standalone script`_. This assumes that the
+``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to
+your settings module.
+
+.. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage
+
+The connection handler reads the first message received from the client, which
+is expected to contain a django-sesame token. Then it authenticates the user
+with ``get_user()``, the API for `authentication outside a view`_. If
+authentication fails, it closes the connection and exits.
+
+.. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view
+
+When we call an API that makes a database query such as ``get_user()``, we
+wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't
+support asynchronous I/O. It would block the event loop if it didn't run in a
+separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In
+earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead.
+
+Finally, we start a server with :func:`~websockets.server.serve`.
+
+We're ready to test!
+
+Save this code to a file called ``authentication.py``, make sure the
+``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the
+websockets server:
+
+.. code-block:: console
+
+ $ python authentication.py
+
+Generate a new token — remember, they're only valid for 30 seconds — and use
+it to connect to your server. Paste your token and press Enter when you get a
+prompt:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8888/
+ Connected to ws://localhost:8888/
+ > <your token>
+ < Hello <your username>!
+ Connection closed: 1000 (OK).
+
+It works!
+
+If you enter an expired or invalid token, authentication fails and the server
+closes the connection:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8888/
+ Connected to ws://localhost:8888.
+ > not a token
+ Connection closed: 1011 (internal error) authentication failed.
+
+You can also test from a browser by generating a new token and running the
+following code in the JavaScript console of the browser:
+
+.. code-block:: javascript
+
+ websocket = new WebSocket("ws://localhost:8888/");
+ websocket.onopen = (event) => websocket.send("<your token>");
+ websocket.onmessage = (event) => console.log(event.data);
+
+If you don't want to import your entire Django project into the websockets
+server, you can build a separate Django project with ``django.contrib.auth``,
+``django-sesame``, a suitable ``User`` model, and a subset of the settings of
+the main project.
+
+Stream events
+-------------
+
+We can connect and authenticate but our server doesn't do anything useful yet!
+
+Let's send a message every time a user makes an action in the admin. This
+message will be broadcast to all users who can access the model on which the
+action was made. This may be used for showing notifications to other users.
+
+Many use cases for WebSocket with Django follow a similar pattern.
+
+Set up event bus
+................
+
+We need a event bus to enable communications between Django and websockets.
+Both sides connect permanently to the bus. Then Django writes events and
+websockets reads them. For the sake of simplicity, we'll rely on `Redis
+Pub/Sub`_.
+
+.. _Redis Pub/Sub: https://redis.io/topics/pubsub
+
+The easiest way to add Redis to a Django project is by configuring a cache
+backend with `django-redis`_. This library manages connections to Redis
+efficiently, persisting them between requests, and provides an API to access
+the Redis connection directly.
+
+.. _django-redis: https://github.com/jazzband/django-redis
+
+Install Redis, add django-redis to the dependencies of your Django project,
+install it, and configure it in the settings of the project:
+
+.. code-block:: python
+
+ CACHES = {
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://127.0.0.1:6379/1",
+ },
+ }
+
+If you already have a default cache, add a new one with a different name and
+change ``get_redis_connection("default")`` in the code below to the same name.
+
+Publish events
+..............
+
+Now let's write events to the bus.
+
+Add the following code to a module that is imported when your Django project
+starts. Typically, you would put it in a ``signals.py`` module, which you
+would import in the ``AppConfig.ready()`` method of one of your apps:
+
+.. literalinclude:: ../../example/django/signals.py
+
+This code runs every time the admin saves a ``LogEntry`` object to keep track
+of a change. It extracts interesting data, serializes it to JSON, and writes
+an event to Redis.
+
+Let's check that it works:
+
+.. code-block:: console
+
+ $ redis-cli
+ 127.0.0.1:6379> SELECT 1
+ OK
+ 127.0.0.1:6379[1]> SUBSCRIBE events
+ Reading messages... (press Ctrl-C to quit)
+ 1) "subscribe"
+ 2) "events"
+ 3) (integer) 1
+
+Leave this command running, start the Django development server and make
+changes in the admin: add, modify, or delete objects. You should see
+corresponding events published to the ``"events"`` stream.
+
+Broadcast events
+................
+
+Now let's turn to reading events and broadcasting them to connected clients.
+We need to add several features:
+
+* Keep track of connected clients so we can broadcast messages.
+* Tell which content types the user has permission to view or to change.
+* Connect to the message bus and read events.
+* Broadcast these events to users who have corresponding permissions.
+
+Here's a complete implementation.
+
+.. literalinclude:: ../../example/django/notifications.py
+
+Since the ``get_content_types()`` function makes a database query, it is
+wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket
+connection is open; then its result is cached for the lifetime of the
+connection. Indeed, running it for each message would trigger database queries
+for all connected users at the same time, which would hurt the database.
+
+The connection handler merely registers the connection in a global variable,
+associated to the list of content types for which events should be sent to
+that connection, and waits until the client disconnects.
+
+The ``process_events()`` function reads events from Redis and broadcasts them
+to all connections that should receive them. We don't care much if a sending a
+notification fails — this happens when a connection drops between the moment
+we iterate on connections and the moment the corresponding message is sent —
+so we start a task with for each message and forget about it. Also, this means
+we're immediately ready to process the next event, even if it takes time to
+send a message to a slow client.
+
+Since Redis can publish a message to multiple subscribers, multiple instances
+of this server can safely run in parallel.
+
+Does it scale?
+--------------
+
+In theory, given enough servers, this design can scale to a hundred million
+clients, since Redis can handle ten thousand servers and each server can
+handle ten thousand clients. In practice, you would need a more scalable
+message bus before reaching that scale, due to the volume of messages.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst
new file mode 100644
index 0000000000..3c8a7d72a6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/extensions.rst
@@ -0,0 +1,30 @@
+Write an extension
+==================
+
+.. currentmodule:: websockets.extensions
+
+During the opening handshake, WebSocket clients and servers negotiate which
+extensions_ will be used with which parameters. Then each frame is processed
+by extensions before being sent or after being received.
+
+.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9
+
+As a consequence, writing an extension requires implementing several classes:
+
+* Extension Factory: it negotiates parameters and instantiates the extension.
+
+ Clients and servers require separate extension factories with distinct APIs.
+
+ Extension factories are the public API of an extension.
+
+* Extension: it decodes incoming frames and encodes outgoing frames.
+
+ If the extension is symmetrical, clients and servers can use the same
+ class.
+
+ Extensions are initialized by extension factories, so they don't need to be
+ part of the public API of an extension.
+
+websockets provides base classes for extension factories and extensions.
+See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`,
+and :class:`Extension` for details.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst
new file mode 100644
index 0000000000..ed001a2aee
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/fly.rst
@@ -0,0 +1,177 @@
+Deploy to Fly
+================
+
+This guide describes how to deploy a websockets server to Fly_.
+
+.. _Fly: https://fly.io/
+
+.. admonition:: The free tier of Fly is sufficient for trying this guide.
+ :class: tip
+
+ The `free tier`__ include up to three small VMs. This guide uses only one.
+
+ __ https://fly.io/docs/about/pricing/
+
+We're going to deploy a very simple app. The process would be identical for a
+more realistic app.
+
+Create application
+------------------
+
+Here's the implementation of the app, an echo server. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/fly/app.py
+ :language: python
+
+This app implements typical requirements for running on a Platform as a Service:
+
+* it provides a health check at ``/healthz``;
+* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal.
+
+Create a ``requirements.txt`` file containing this line to declare a dependency
+on websockets:
+
+.. literalinclude:: ../../example/deployment/fly/requirements.txt
+ :language: text
+
+The app is ready. Let's deploy it!
+
+Deploy application
+------------------
+
+Follow the instructions__ to install the Fly CLI, if you haven't done that yet.
+
+__ https://fly.io/docs/hands-on/install-flyctl/
+
+Sign up or log in to Fly.
+
+Launch the app — you'll have to pick a different name because I'm already using
+``websockets-echo``:
+
+.. code-block:: console
+
+ $ fly launch
+ Creating app in ...
+ Scanning source code
+ Detected a Python app
+ Using the following build configuration:
+ Builder: paketobuildpacks/builder:base
+ ? App Name (leave blank to use an auto-generated name): websockets-echo
+ ? Select organization: ...
+ ? Select region: ...
+ Created app websockets-echo in organization ...
+ Wrote config file fly.toml
+ ? Would you like to set up a Postgresql database now? No
+ We have generated a simple Procfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application.
+
+.. admonition:: This will build the image with a generic buildpack.
+ :class: tip
+
+ Fly can `build images`__ with a Dockerfile or a buildpack. Here, ``fly
+ launch`` configures a generic Paketo buildpack.
+
+ If you'd rather package the app with a Dockerfile, check out the guide to
+ :ref:`containerize an application <containerize-application>`.
+
+ __ https://fly.io/docs/reference/builders/
+
+Replace the auto-generated ``fly.toml`` with:
+
+.. literalinclude:: ../../example/deployment/fly/fly.toml
+ :language: toml
+
+This configuration:
+
+* listens on port 443, terminates TLS, and forwards to the app on port 8080;
+* declares a health check at ``/healthz``;
+* requests a ``SIGTERM`` for terminating the app.
+
+Replace the auto-generated ``Procfile`` with:
+
+.. literalinclude:: ../../example/deployment/fly/Procfile
+ :language: text
+
+This tells Fly how to run the app.
+
+Now you can deploy it:
+
+.. code-block:: console
+
+ $ fly deploy
+
+ ... lots of output...
+
+ ==> Monitoring deployment
+
+ 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
+ --> v0 deployed successfully
+
+Validate deployment
+-------------------
+
+Let's confirm that your application is running as expected.
+
+Since it's a WebSocket server, you need a WebSocket client, such as the
+interactive client that comes with websockets.
+
+If you're currently building a websockets server, perhaps you're already in a
+virtualenv where websockets is installed. If not, you can install it in a new
+virtualenv as follows:
+
+.. code-block:: console
+
+ $ python -m venv websockets-client
+ $ . websockets-client/bin/activate
+ $ pip install websockets
+
+Connect the interactive client — you must replace ``websockets-echo`` with the
+name of your Fly app in this command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.fly.dev/
+ Connected to wss://websockets-echo.fly.dev/.
+ >
+
+Great! Your app is running!
+
+Once you're connected, you can send any message and the server will echo it,
+or press Ctrl-D to terminate the connection:
+
+.. code-block:: console
+
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+You can also confirm that your application shuts down gracefully.
+
+Connect an interactive client again — remember to replace ``websockets-echo``
+with your app:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.fly.dev/
+ Connected to wss://websockets-echo.fly.dev/.
+ >
+
+In another shell, restart the app — again, replace ``websockets-echo`` with your
+app:
+
+.. code-block:: console
+
+ $ fly restart websockets-echo
+ websockets-echo is being restarted
+
+Go back to the first shell. The connection is closed with code 1001 (going
+away).
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.fly.dev/
+ Connected to wss://websockets-echo.fly.dev/.
+ Connection closed: 1001 (going away).
+
+If graceful shutdown wasn't working, the server wouldn't perform a closing
+handshake and the connection would be closed with code 1006 (abnormal closure).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst
new file mode 100644
index 0000000000..fdaab04011
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/haproxy.rst
@@ -0,0 +1,61 @@
+Deploy behind HAProxy
+=====================
+
+This guide demonstrates a way to load balance connections across multiple
+websockets server processes running on the same machine with HAProxy_.
+
+We'll run server processes with Supervisor as described in :doc:`this guide
+<supervisor>`.
+
+.. _HAProxy: https://www.haproxy.org/
+
+Run server processes
+--------------------
+
+Save this app to ``app.py``:
+
+.. literalinclude:: ../../example/deployment/haproxy/app.py
+ :emphasize-lines: 24
+
+Each server process listens on a different port by extracting an incremental
+index from an environment variable set by Supervisor.
+
+Save this configuration to ``supervisord.conf``:
+
+.. literalinclude:: ../../example/deployment/haproxy/supervisord.conf
+
+This configuration runs four instances of the app.
+
+Install Supervisor and run it:
+
+.. code-block:: console
+
+ $ supervisord -c supervisord.conf -n
+
+Configure and run HAProxy
+-------------------------
+
+Here's a simple HAProxy configuration to load balance connections across four
+processes:
+
+.. literalinclude:: ../../example/deployment/haproxy/haproxy.cfg
+
+In the backend configuration, we set the load balancing method to
+``leastconn`` in order to balance the number of active connections across
+servers. This is best for long running connections.
+
+Save the configuration to ``haproxy.cfg``, install HAProxy, and run it:
+
+.. code-block:: console
+
+ $ haproxy -f haproxy.cfg
+
+You can confirm that HAProxy proxies connections properly:
+
+.. code-block:: console
+
+ $ PYTHONPATH=src python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst
new file mode 100644
index 0000000000..a97d2e7ce0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/heroku.rst
@@ -0,0 +1,183 @@
+Deploy to Heroku
+================
+
+This guide describes how to deploy a websockets server to Heroku_. The same
+principles should apply to other Platform as a Service providers.
+
+.. _Heroku: https://www.heroku.com/
+
+.. admonition:: Heroku no longer offers a free tier.
+ :class: attention
+
+ When this tutorial was written, in September 2021, Heroku offered a free
+ tier where a websockets app could run at no cost. In November 2022, Heroku
+ removed the free tier, making it impossible to maintain this document. As a
+ consequence, it isn't updated anymore and may be removed in the future.
+
+We're going to deploy a very simple app. The process would be identical for a
+more realistic app.
+
+Create repository
+-----------------
+
+Deploying to Heroku requires a git repository. Let's initialize one:
+
+.. code-block:: console
+
+ $ mkdir websockets-echo
+ $ cd websockets-echo
+ $ git init -b main
+ Initialized empty Git repository in websockets-echo/.git/
+ $ git commit --allow-empty -m "Initial commit."
+ [main (root-commit) 1e7947d] Initial commit.
+
+Create application
+------------------
+
+Here's the implementation of the app, an echo server. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/heroku/app.py
+ :language: python
+
+Heroku expects the server to `listen on a specific port`_, which is provided
+in the ``$PORT`` environment variable. The app reads it and passes it to
+:func:`~websockets.server.serve`.
+
+.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port
+
+Heroku sends a ``SIGTERM`` signal to all processes when `shutting down a
+dyno`_. When the app receives this signal, it closes connections and exits
+cleanly.
+
+.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown
+
+Create a ``requirements.txt`` file containing this line to declare a dependency
+on websockets:
+
+.. literalinclude:: ../../example/deployment/heroku/requirements.txt
+ :language: text
+
+Create a ``Procfile``.
+
+.. literalinclude:: ../../example/deployment/heroku/Procfile
+
+This tells Heroku how to run the app.
+
+Confirm that you created the correct files and commit them to git:
+
+.. code-block:: console
+
+ $ ls
+ Procfile app.py requirements.txt
+ $ git add .
+ $ git commit -m "Initial implementation."
+ [main 8418c62] Initial implementation.
+  3 files changed, 32 insertions(+)
+  create mode 100644 Procfile
+  create mode 100644 app.py
+  create mode 100644 requirements.txt
+
+The app is ready. Let's deploy it!
+
+Deploy application
+------------------
+
+Follow the instructions_ to install the Heroku CLI, if you haven't done that
+yet.
+
+.. _instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up
+
+Sign up or log in to Heroku.
+
+Create a Heroku app — you'll have to pick a different name because I'm already
+using ``websockets-echo``:
+
+.. code-block:: console
+
+ $ heroku create websockets-echo
+ Creating ⬢ websockets-echo... done
+ https://websockets-echo.herokuapp.com/ | https://git.heroku.com/websockets-echo.git
+
+.. code-block:: console
+
+ $ git push heroku
+
+ ... lots of output...
+
+ remote: -----> Launching...
+ remote: Released v1
+ remote: https://websockets-echo.herokuapp.com/ deployed to Heroku
+ remote:
+ remote: Verifying deploy... done.
+ To https://git.heroku.com/websockets-echo.git
+  * [new branch] main -> main
+
+Validate deployment
+-------------------
+
+Let's confirm that your application is running as expected.
+
+Since it's a WebSocket server, you need a WebSocket client, such as the
+interactive client that comes with websockets.
+
+If you're currently building a websockets server, perhaps you're already in a
+virtualenv where websockets is installed. If not, you can install it in a new
+virtualenv as follows:
+
+.. code-block:: console
+
+ $ python -m venv websockets-client
+ $ . websockets-client/bin/activate
+ $ pip install websockets
+
+Connect the interactive client — you must replace ``websockets-echo`` with the
+name of your Heroku app in this command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.herokuapp.com/
+ Connected to wss://websockets-echo.herokuapp.com/.
+ >
+
+Great! Your app is running!
+
+Once you're connected, you can send any message and the server will echo it,
+or press Ctrl-D to terminate the connection:
+
+.. code-block:: console
+
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+You can also confirm that your application shuts down gracefully.
+
+Connect an interactive client again — remember to replace ``websockets-echo``
+with your app:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.herokuapp.com/
+ Connected to wss://websockets-echo.herokuapp.com/.
+ >
+
+In another shell, restart the app — again, replace ``websockets-echo`` with your
+app:
+
+.. code-block:: console
+
+ $ heroku dyno:restart -a websockets-echo
+ Restarting dynos on ⬢ websockets-echo... done
+
+Go back to the first shell. The connection is closed with code 1001 (going
+away).
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.herokuapp.com/
+ Connected to wss://websockets-echo.herokuapp.com/.
+ Connection closed: 1001 (going away).
+
+If graceful shutdown wasn't working, the server wouldn't perform a closing
+handshake and the connection would be closed with code 1006 (abnormal closure).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst
new file mode 100644
index 0000000000..ddbe67d3ae
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/index.rst
@@ -0,0 +1,56 @@
+How-to guides
+=============
+
+In a hurry? Check out these examples.
+
+.. toctree::
+ :titlesonly:
+
+ quickstart
+
+If you're stuck, perhaps you'll find the answer here.
+
+.. toctree::
+ :titlesonly:
+
+ cheatsheet
+ patterns
+ autoreload
+
+This guide will help you integrate websockets into a broader system.
+
+.. toctree::
+ :titlesonly:
+
+ django
+
+The WebSocket protocol makes provisions for extending or specializing its
+features, which websockets supports fully.
+
+.. toctree::
+ :titlesonly:
+
+ extensions
+
+.. _deployment-howto:
+
+Once your application is ready, learn how to deploy it on various platforms.
+
+.. toctree::
+ :titlesonly:
+
+ render
+ fly
+ heroku
+ kubernetes
+ supervisor
+ nginx
+ haproxy
+
+If you're integrating the Sans-I/O layer of websockets into a library, rather
+than building an application with websockets, follow this guide.
+
+.. toctree::
+ :maxdepth: 2
+
+ sansio
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst
new file mode 100644
index 0000000000..064a6ac4d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/kubernetes.rst
@@ -0,0 +1,215 @@
+Deploy to Kubernetes
+====================
+
+This guide describes how to deploy a websockets server to Kubernetes_. It
+assumes familiarity with Docker and Kubernetes.
+
+We're going to deploy a simple app to a local Kubernetes cluster and to ensure
+that it scales as expected.
+
+In a more realistic context, you would follow your organization's practices
+for deploying to Kubernetes, but you would apply the same principles as far as
+websockets is concerned.
+
+.. _Kubernetes: https://kubernetes.io/
+
+.. _containerize-application:
+
+Containerize application
+------------------------
+
+Here's the app we're going to deploy. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/kubernetes/app.py
+
+This is an echo server with one twist: every message blocks the server for
+100ms, which creates artificial starvation of CPU time. This makes it easier
+to saturate the server for load testing.
+
+The app exposes a health check on ``/healthz``. It also provides two other
+endpoints for testing purposes: ``/inemuri`` will make the app unresponsive
+for 10 seconds and ``/seppuku`` will terminate it.
+
+The quest for the perfect Python container image is out of scope of this
+guide, so we'll go for the simplest possible configuration instead:
+
+.. literalinclude:: ../../example/deployment/kubernetes/Dockerfile
+
+After saving this ``Dockerfile``, build the image:
+
+.. code-block:: console
+
+ $ docker build -t websockets-test:1.0 .
+
+Test your image by running:
+
+.. code-block:: console
+
+ $ docker run --name run-websockets-test --publish 32080:80 --rm \
+ websockets-test:1.0
+
+Then, in another shell, in a virtualenv where websockets is installed, connect
+to the app and check that it echoes anything you send:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:32080/
+ Connected to ws://localhost:32080/.
+ > Hey there!
+ < Hey there!
+ >
+
+Now, in yet another shell, stop the app with:
+
+.. code-block:: console
+
+ $ docker kill -s TERM run-websockets-test
+
+Going to the shell where you connected to the app, you can confirm that it
+shut down gracefully:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:32080/
+ Connected to ws://localhost:32080/.
+ > Hey there!
+ < Hey there!
+ Connection closed: 1001 (going away).
+
+If it didn't, you'd get code 1006 (abnormal closure).
+
+Deploy application
+------------------
+
+Configuring Kubernetes is even further beyond the scope of this guide, so
+we'll use a basic configuration for testing, with just one Service_ and one
+Deployment_:
+
+.. literalinclude:: ../../example/deployment/kubernetes/deployment.yaml
+
+For local testing, a service of type NodePort_ is good enough. For deploying
+to production, you would configure an Ingress_.
+
+.. _Service: https://kubernetes.io/docs/concepts/services-networking/service/
+.. _Deployment: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
+.. _NodePort: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport
+.. _Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/
+
+After saving this to a file called ``deployment.yaml``, you can deploy:
+
+.. code-block:: console
+
+ $ kubectl apply -f deployment.yaml
+ service/websockets-test created
+ deployment.apps/websockets-test created
+
+Now you have a deployment with one pod running:
+
+.. code-block:: console
+
+ $ kubectl get deployment websockets-test
+ NAME READY UP-TO-DATE AVAILABLE AGE
+ websockets-test 1/1 1 1 10s
+ $ kubectl get pods -l app=websockets-test
+ NAME READY STATUS RESTARTS AGE
+ websockets-test-86b48f4bb7-nltfh 1/1 Running 0 10s
+
+You can connect to the service — press Ctrl-D to exit:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:32080/
+ Connected to ws://localhost:32080/.
+ Connection closed: 1000 (OK).
+
+Validate deployment
+-------------------
+
+First, let's ensure the liveness probe works by making the app unresponsive:
+
+.. code-block:: console
+
+ $ curl http://localhost:32080/inemuri
+ Sleeping for 10s
+
+Since we have only one pod, we know that this pod will go to sleep.
+
+The liveness probe is configured to run every second. By default, liveness
+probes time out after one second and have a threshold of three failures.
+Therefore Kubernetes should restart the pod after at most 5 seconds.
+
+Indeed, after a few seconds, the pod reports a restart:
+
+.. code-block:: console
+
+ $ kubectl get pods -l app=websockets-test
+ NAME READY STATUS RESTARTS AGE
+ websockets-test-86b48f4bb7-nltfh 1/1 Running 1 42s
+
+Next, let's take it one step further and crash the app:
+
+.. code-block:: console
+
+ $ curl http://localhost:32080/seppuku
+ Terminating
+
+The pod reports a second restart:
+
+.. code-block:: console
+
+ $ kubectl get pods -l app=websockets-test
+ NAME READY STATUS RESTARTS AGE
+ websockets-test-86b48f4bb7-nltfh 1/1 Running 2 72s
+
+All good — Kubernetes delivers on its promise to keep our app alive!
+
+Scale deployment
+----------------
+
+Of course, Kubernetes is for scaling. Let's scale — modestly — to 10 pods:
+
+.. code-block:: console
+
+ $ kubectl scale deployment.apps/websockets-test --replicas=10
+ deployment.apps/websockets-test scaled
+
+After a few seconds, we have 10 pods running:
+
+.. code-block:: console
+
+ $ kubectl get deployment websockets-test
+ NAME READY UP-TO-DATE AVAILABLE AGE
+ websockets-test 10/10 10 10 10m
+
+Now let's generate load. We'll use this script:
+
+.. literalinclude:: ../../example/deployment/kubernetes/benchmark.py
+
+We'll connect 500 clients in parallel, meaning 50 clients per pod, and have
+each client send 6 messages. Since the app blocks for 100ms before responding,
+if connections are perfectly distributed, we expect a total run time slightly
+over 50 * 6 * 0.1 = 30 seconds.
+
+Let's try it:
+
+.. code-block:: console
+
+ $ ulimit -n 512
+ $ time python benchmark.py 500 6
+ python benchmark.py 500 6 2.40s user 0.51s system 7% cpu 36.471 total
+
+A total runtime of 36 seconds is in the right ballpark. Repeating this
+experiment with other parameters shows roughly consistent results, with the
+high variability you'd expect from a quick benchmark without any effort to
+stabilize the test setup.
+
+Finally, we can scale back to one pod.
+
+.. code-block:: console
+
+ $ kubectl scale deployment.apps/websockets-test --replicas=1
+ deployment.apps/websockets-test scaled
+ $ kubectl get deployment websockets-test
+ NAME READY UP-TO-DATE AVAILABLE AGE
+ websockets-test 1/1 1 1 15m
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst
new file mode 100644
index 0000000000..30545fbc7d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/nginx.rst
@@ -0,0 +1,84 @@
+Deploy behind nginx
+===================
+
+This guide demonstrates a way to load balance connections across multiple
+websockets server processes running on the same machine with nginx_.
+
+We'll run server processes with Supervisor as described in :doc:`this guide
+<supervisor>`.
+
+.. _nginx: https://nginx.org/
+
+Run server processes
+--------------------
+
+Save this app to ``app.py``:
+
+.. literalinclude:: ../../example/deployment/nginx/app.py
+ :emphasize-lines: 21,23
+
+We'd like to nginx to connect to websockets servers via Unix sockets in order
+to avoid the overhead of TCP for communicating between processes running in
+the same OS.
+
+We start the app with :func:`~websockets.server.unix_serve`. Each server
+process listens on a different socket thanks to an environment variable set
+by Supervisor to a different value.
+
+Save this configuration to ``supervisord.conf``:
+
+.. literalinclude:: ../../example/deployment/nginx/supervisord.conf
+
+This configuration runs four instances of the app.
+
+Install Supervisor and run it:
+
+.. code-block:: console
+
+ $ supervisord -c supervisord.conf -n
+
+Configure and run nginx
+-----------------------
+
+Here's a simple nginx configuration to load balance connections across four
+processes:
+
+.. literalinclude:: ../../example/deployment/nginx/nginx.conf
+
+We set ``daemon off`` so we can run nginx in the foreground for testing.
+
+Then we combine the `WebSocket proxying`_ and `load balancing`_ guides:
+
+* The WebSocket protocol requires HTTP/1.1. We must set the HTTP protocol
+ version to 1.1, else nginx defaults to HTTP/1.0 for proxying.
+
+* The WebSocket handshake involves the ``Connection`` and ``Upgrade`` HTTP
+ headers. We must pass them to the upstream explicitly, else nginx drops
+ them because they're hop-by-hop headers.
+
+ We deviate from the `WebSocket proxying`_ guide because its example adds a
+ ``Connection: Upgrade`` header to every upstream request, even if the
+ original request didn't contain that header.
+
+* In the upstream configuration, we set the load balancing method to
+ ``least_conn`` in order to balance the number of active connections across
+ servers. This is best for long running connections.
+
+.. _WebSocket proxying: http://nginx.org/en/docs/http/websocket.html
+.. _load balancing: http://nginx.org/en/docs/http/load_balancing.html
+
+Save the configuration to ``nginx.conf``, install nginx, and run it:
+
+.. code-block:: console
+
+ $ nginx -c nginx.conf -p .
+
+You can confirm that nginx proxies connections properly:
+
+.. code-block:: console
+
+ $ PYTHONPATH=src python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst
new file mode 100644
index 0000000000..c6f325d213
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/patterns.rst
@@ -0,0 +1,110 @@
+Patterns
+========
+
+.. currentmodule:: websockets
+
+Here are typical patterns for processing messages in a WebSocket server or
+client. You will certainly implement some of them in your application.
+
+This page gives examples of connection handlers for a server. However, they're
+also applicable to a client, simply by assuming that ``websocket`` is a
+connection created with :func:`~client.connect`.
+
+WebSocket connections are long-lived. You will usually write a loop to process
+several messages during the lifetime of a connection.
+
+Consumer
+--------
+
+To receive messages from the WebSocket connection::
+
+ async def consumer_handler(websocket):
+ async for message in websocket:
+ await consumer(message)
+
+In this example, ``consumer()`` is a coroutine implementing your business
+logic for processing a message received on the WebSocket connection. Each
+message may be :class:`str` or :class:`bytes`.
+
+Iteration terminates when the client disconnects.
+
+Producer
+--------
+
+To send messages to the WebSocket connection::
+
+ async def producer_handler(websocket):
+ while True:
+ message = await producer()
+ await websocket.send(message)
+
+In this example, ``producer()`` is a coroutine implementing your business
+logic for generating the next message to send on the WebSocket connection.
+Each message must be :class:`str` or :class:`bytes`.
+
+Iteration terminates when the client disconnects
+because :meth:`~server.WebSocketServerProtocol.send` raises a
+:exc:`~exceptions.ConnectionClosed` exception,
+which breaks out of the ``while True`` loop.
+
+Consumer and producer
+---------------------
+
+You can receive and send messages on the same WebSocket connection by
+combining the consumer and producer patterns. This requires running two tasks
+in parallel::
+
+ async def handler(websocket):
+ await asyncio.gather(
+ consumer_handler(websocket),
+ producer_handler(websocket),
+ )
+
+If a task terminates, :func:`~asyncio.gather` doesn't cancel the other task.
+This can result in a situation where the producer keeps running after the
+consumer finished, which may leak resources.
+
+Here's a way to exit and close the WebSocket connection as soon as a task
+terminates, after canceling the other task::
+
+ async def handler(websocket):
+ consumer_task = asyncio.create_task(consumer_handler(websocket))
+ producer_task = asyncio.create_task(producer_handler(websocket))
+ done, pending = await asyncio.wait(
+ [consumer_task, producer_task],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+ for task in pending:
+ task.cancel()
+
+Registration
+------------
+
+To keep track of currently connected clients, you can register them when they
+connect and unregister them when they disconnect::
+
+ connected = set()
+
+ async def handler(websocket):
+ # Register.
+ connected.add(websocket)
+ try:
+ # Broadcast a message to all connected clients.
+ websockets.broadcast(connected, "Hello!")
+ await asyncio.sleep(10)
+ finally:
+ # Unregister.
+ connected.remove(websocket)
+
+This example maintains the set of connected clients in memory. This works as
+long as you run a single process. It doesn't scale to multiple processes.
+
+Publish–subscribe
+-----------------
+
+If you plan to run multiple processes and you want to communicate updates
+between processes, then you must deploy a messaging system. You may find
+publish-subscribe functionality useful.
+
+A complete implementation of this idea with Redis is described in
+the :doc:`Django integration guide <../howto/django>`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst
new file mode 100644
index 0000000000..ab870952c1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/quickstart.rst
@@ -0,0 +1,170 @@
+Quick start
+===========
+
+.. currentmodule:: websockets
+
+Here are a few examples to get you started quickly with websockets.
+
+Say "Hello world!"
+------------------
+
+Here's a WebSocket server.
+
+It receives a name from the client, sends a greeting, and closes the connection.
+
+.. literalinclude:: ../../example/quickstart/server.py
+ :caption: server.py
+ :language: python
+ :linenos:
+
+:func:`~server.serve` executes the connection handler coroutine ``hello()``
+once for each WebSocket connection. It closes the WebSocket connection when
+the handler returns.
+
+Here's a corresponding WebSocket client.
+
+It sends a name to the server, receives a greeting, and closes the connection.
+
+.. literalinclude:: ../../example/quickstart/client.py
+ :caption: client.py
+ :language: python
+ :linenos:
+
+Using :func:`~client.connect` as an asynchronous context manager ensures the
+WebSocket connection is closed.
+
+.. _secure-server-example:
+
+Encrypt connections
+-------------------
+
+Secure WebSocket connections improve confidentiality and also reliability
+because they reduce the risk of interference by bad proxies.
+
+The ``wss`` protocol is to ``ws`` what ``https`` is to ``http``. The
+connection is encrypted with TLS_ (Transport Layer Security). ``wss``
+requires certificates like ``https``.
+
+.. _TLS: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security
+
+.. admonition:: TLS vs. SSL
+ :class: tip
+
+ TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an
+ earlier encryption protocol; the name stuck.
+
+Here's how to adapt the server to encrypt connections. You must download
+:download:`localhost.pem <../../example/quickstart/localhost.pem>` and save it
+in the same directory as ``server_secure.py``.
+
+.. literalinclude:: ../../example/quickstart/server_secure.py
+ :caption: server_secure.py
+ :language: python
+ :linenos:
+
+Here's how to adapt the client similarly.
+
+.. literalinclude:: ../../example/quickstart/client_secure.py
+ :caption: client_secure.py
+ :language: python
+ :linenos:
+
+In this example, the client needs a TLS context because the server uses a
+self-signed certificate.
+
+When connecting to a secure WebSocket server with a valid certificate — any
+certificate signed by a CA that your Python installation trusts — you can
+simply pass ``ssl=True`` to :func:`~client.connect`.
+
+.. admonition:: Configure the TLS context securely
+ :class: attention
+
+ This example demonstrates the ``ssl`` argument with a TLS certificate shared
+ between the client and the server. This is a simplistic setup.
+
+ Please review the advice and security considerations in the documentation of
+ the :mod:`ssl` module to configure the TLS context securely.
+
+Connect from a browser
+----------------------
+
+The WebSocket protocol was invented for the web — as the name says!
+
+Here's how to connect to a WebSocket server from a browser.
+
+Run this script in a console:
+
+.. literalinclude:: ../../example/quickstart/show_time.py
+ :caption: show_time.py
+ :language: python
+ :linenos:
+
+Save this file as ``show_time.html``:
+
+.. literalinclude:: ../../example/quickstart/show_time.html
+ :caption: show_time.html
+ :language: html
+ :linenos:
+
+Save this file as ``show_time.js``:
+
+.. literalinclude:: ../../example/quickstart/show_time.js
+ :caption: show_time.js
+ :language: js
+ :linenos:
+
+Then, open ``show_time.html`` in several browsers. Clocks tick irregularly.
+
+Broadcast messages
+------------------
+
+Let's change the previous example to send the same timestamps to all browsers,
+instead of generating independent sequences for each client.
+
+Stop the previous script if it's still running and run this script in a console:
+
+.. literalinclude:: ../../example/quickstart/show_time_2.py
+ :caption: show_time_2.py
+ :language: python
+ :linenos:
+
+Refresh ``show_time.html`` in all browsers. Clocks tick in sync.
+
+Manage application state
+------------------------
+
+A WebSocket server can receive events from clients, process them to update the
+application state, and broadcast the updated state to all connected clients.
+
+Here's an example where any client can increment or decrement a counter. The
+concurrency model of :mod:`asyncio` guarantees that updates are serialized.
+
+Run this script in a console:
+
+.. literalinclude:: ../../example/quickstart/counter.py
+ :caption: counter.py
+ :language: python
+ :linenos:
+
+Save this file as ``counter.html``:
+
+.. literalinclude:: ../../example/quickstart/counter.html
+ :caption: counter.html
+ :language: html
+ :linenos:
+
+Save this file as ``counter.css``:
+
+.. literalinclude:: ../../example/quickstart/counter.css
+ :caption: counter.css
+ :language: css
+ :linenos:
+
+Save this file as ``counter.js``:
+
+.. literalinclude:: ../../example/quickstart/counter.js
+ :caption: counter.js
+ :language: js
+ :linenos:
+
+Then open ``counter.html`` file in several browsers and play with [+] and [-].
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst
new file mode 100644
index 0000000000..70bf8c376c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/render.rst
@@ -0,0 +1,172 @@
+Deploy to Render
+================
+
+This guide describes how to deploy a websockets server to Render_.
+
+.. _Render: https://render.com/
+
+.. admonition:: The free plan of Render is sufficient for trying this guide.
+ :class: tip
+
+ However, on a `free plan`__, connections are dropped after five minutes,
+ which is quite short for WebSocket application.
+
+ __ https://render.com/docs/free
+
+We're going to deploy a very simple app. The process would be identical for a
+more realistic app.
+
+Create repository
+-----------------
+
+Deploying to Render requires a git repository. Let's initialize one:
+
+.. code-block:: console
+
+ $ mkdir websockets-echo
+ $ cd websockets-echo
+ $ git init -b main
+ Initialized empty Git repository in websockets-echo/.git/
+ $ git commit --allow-empty -m "Initial commit."
+ [main (root-commit) 816c3b1] Initial commit.
+
+Render requires the git repository to be hosted at GitHub or GitLab.
+
+Sign up or log in to GitHub. Create a new repository named ``websockets-echo``.
+Don't enable any of the initialization options offered by GitHub. Then, follow
+instructions for pushing an existing repository from the command line.
+
+After pushing, refresh your repository's homepage on GitHub. You should see an
+empty repository with an empty initial commit.
+
+Create application
+------------------
+
+Here's the implementation of the app, an echo server. Save it in a file called
+``app.py``:
+
+.. literalinclude:: ../../example/deployment/render/app.py
+ :language: python
+
+This app implements requirements for `zero downtime deploys`_:
+
+* it provides a health check at ``/healthz``;
+* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal.
+
+.. _zero downtime deploys: https://render.com/docs/deploys#zero-downtime-deploys
+
+Create a ``requirements.txt`` file containing this line to declare a dependency
+on websockets:
+
+.. literalinclude:: ../../example/deployment/render/requirements.txt
+ :language: text
+
+Confirm that you created the correct files and commit them to git:
+
+.. code-block:: console
+
+ $ ls
+ app.py requirements.txt
+ $ git add .
+ $ git commit -m "Initial implementation."
+ [main f26bf7f] Initial implementation.
+ 2 files changed, 37 insertions(+)
+ create mode 100644 app.py
+ create mode 100644 requirements.txt
+
+Push the changes to GitHub:
+
+.. code-block:: console
+
+ $ git push
+ ...
+ To github.com:<username>/websockets-echo.git
+ 816c3b1..f26bf7f main -> main
+
+The app is ready. Let's deploy it!
+
+Deploy application
+------------------
+
+Sign up or log in to Render.
+
+Create a new web service. Connect the git repository that you just created.
+
+Then, finalize the configuration of your app as follows:
+
+* **Name**: websockets-echo
+* **Start Command**: ``python app.py``
+
+If you're just experimenting, select the free plan. Create the web service.
+
+To configure the health check, go to Settings, scroll down to Health & Alerts,
+and set:
+
+* **Health Check Path**: /healthz
+
+This triggers a new deployment.
+
+Validate deployment
+-------------------
+
+Let's confirm that your application is running as expected.
+
+Since it's a WebSocket server, you need a WebSocket client, such as the
+interactive client that comes with websockets.
+
+If you're currently building a websockets server, perhaps you're already in a
+virtualenv where websockets is installed. If not, you can install it in a new
+virtualenv as follows:
+
+.. code-block:: console
+
+ $ python -m venv websockets-client
+ $ . websockets-client/bin/activate
+ $ pip install websockets
+
+Connect the interactive client — you must replace ``websockets-echo`` with the
+name of your Render app in this command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.onrender.com/
+ Connected to wss://websockets-echo.onrender.com/.
+ >
+
+Great! Your app is running!
+
+Once you're connected, you can send any message and the server will echo it,
+or press Ctrl-D to terminate the connection:
+
+.. code-block:: console
+
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+You can also confirm that your application shuts down gracefully when you deploy
+a new version. Due to limitations of Render's free plan, you must upgrade to a
+paid plan before you perform this test.
+
+Connect an interactive client again — remember to replace ``websockets-echo``
+with your app:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.onrender.com/
+ Connected to wss://websockets-echo.onrender.com/.
+ >
+
+Trigger a new deployment with Manual Deploy > Deploy latest commit. When the
+deployment completes, the connection is closed with code 1001 (going away).
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-echo.onrender.com/
+ Connected to wss://websockets-echo.onrender.com/.
+ Connection closed: 1001 (going away).
+
+If graceful shutdown wasn't working, the server wouldn't perform a closing
+handshake and the connection would be closed with code 1006 (abnormal closure).
+
+Remember to downgrade to a free plan if you upgraded just for testing this feature.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst
new file mode 100644
index 0000000000..d41519ff09
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/sansio.rst
@@ -0,0 +1,322 @@
+Integrate the Sans-I/O layer
+============================
+
+.. currentmodule:: websockets
+
+This guide explains how to integrate the `Sans-I/O`_ layer of websockets to
+add support for WebSocket in another library.
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+As a prerequisite, you should decide how you will handle network I/O and
+asynchronous control flow.
+
+Your integration layer will provide an API for the application on one side,
+will talk to the network on the other side, and will rely on websockets to
+implement the protocol in the middle.
+
+.. image:: ../topics/data-flow.svg
+ :align: center
+
+Opening a connection
+--------------------
+
+Client-side
+...........
+
+If you're building a client, parse the URI you'd like to connect to::
+
+ from websockets.uri import parse_uri
+
+ wsuri = parse_uri("ws://example.com/")
+
+Open a TCP connection to ``(wsuri.host, wsuri.port)`` and perform a TLS
+handshake if ``wsuri.secure`` is :obj:`True`.
+
+Initialize a :class:`~client.ClientProtocol`::
+
+ from websockets.client import ClientProtocol
+
+ protocol = ClientProtocol(wsuri)
+
+Create a WebSocket handshake request
+with :meth:`~client.ClientProtocol.connect` and send it
+with :meth:`~client.ClientProtocol.send_request`::
+
+ request = protocol.connect()
+ protocol.send_request(request)
+
+Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to
+the network, as described in `Send data`_ below.
+
+Once you receive enough data, as explained in `Receive data`_ below, the first
+event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket
+handshake response.
+
+When the handshake fails, the reason is available in
+:attr:`~client.ClientProtocol.handshake_exc`::
+
+ if protocol.handshake_exc is not None:
+ raise protocol.handshake_exc
+
+Else, the WebSocket connection is open.
+
+A WebSocket client API usually performs the handshake then returns a wrapper
+around the network socket and the :class:`~client.ClientProtocol`.
+
+Server-side
+...........
+
+If you're building a server, accept network connections from clients and
+perform a TLS handshake if desired.
+
+For each connection, initialize a :class:`~server.ServerProtocol`::
+
+ from websockets.server import ServerProtocol
+
+ protocol = ServerProtocol()
+
+Once you receive enough data, as explained in `Receive data`_ below, the first
+event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket
+handshake request.
+
+Create a WebSocket handshake response
+with :meth:`~server.ServerProtocol.accept` and send it
+with :meth:`~server.ServerProtocol.send_response`::
+
+ response = protocol.accept(request)
+ protocol.send_response(response)
+
+Alternatively, you may reject the WebSocket handshake and return an HTTP
+response with :meth:`~server.ServerProtocol.reject`::
+
+ response = protocol.reject(status, explanation)
+ protocol.send_response(response)
+
+Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to
+the network, as described in `Send data`_ below.
+
+Even when you call :meth:`~server.ServerProtocol.accept`, the WebSocket
+handshake may fail if the request is incorrect or unsupported.
+
+When the handshake fails, the reason is available in
+:attr:`~server.ServerProtocol.handshake_exc`::
+
+ if protocol.handshake_exc is not None:
+ raise protocol.handshake_exc
+
+Else, the WebSocket connection is open.
+
+A WebSocket server API usually builds a wrapper around the network socket and
+the :class:`~server.ServerProtocol`. Then it invokes a connection handler that
+accepts the wrapper in argument.
+
+It may also provide a way to close all connections and to shut down the server
+gracefully.
+
+Going forwards, this guide focuses on handling an individual connection.
+
+From the network to the application
+-----------------------------------
+
+Go through the five steps below until you reach the end of the data stream.
+
+Receive data
+............
+
+When receiving data from the network, feed it to the protocol's
+:meth:`~protocol.Protocol.receive_data` method.
+
+When reaching the end of the data stream, call the protocol's
+:meth:`~protocol.Protocol.receive_eof` method.
+
+For example, if ``sock`` is a :obj:`~socket.socket`::
+
+ try:
+ data = sock.recv(65536)
+ except OSError: # socket closed
+ data = b""
+ if data:
+ protocol.receive_data(data)
+ else:
+ protocol.receive_eof()
+
+These methods aren't expected to raise exceptions — unless you call them again
+after calling :meth:`~protocol.Protocol.receive_eof`, which is an error.
+(If you get an exception, please file a bug!)
+
+Send data
+.........
+
+Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to
+the network::
+
+ for data in protocol.data_to_send():
+ if data:
+ sock.sendall(data)
+ else:
+ sock.shutdown(socket.SHUT_WR)
+
+The empty bytestring signals the end of the data stream. When you see it, you
+must half-close the TCP connection.
+
+Sending data right after receiving data is necessary because websockets
+responds to ping frames, close frames, and incorrect inputs automatically.
+
+Expect TCP connection to close
+..............................
+
+Closing a WebSocket connection normally involves a two-way WebSocket closing
+handshake. Then, regardless of whether the closure is normal or abnormal, the
+server starts the four-way TCP closing handshake. If the network fails at the
+wrong point, you can end up waiting until the TCP timeout, which is very long.
+
+To prevent dangling TCP connections when you expect the end of the data stream
+but you never reach it, call :meth:`~protocol.Protocol.close_expected`
+and, if it returns :obj:`True`, schedule closing the TCP connection after a
+short timeout::
+
+ # start a new execution thread to run this code
+ sleep(10)
+ sock.close() # does nothing if the socket is already closed
+
+If the connection is still open when the timeout elapses, closing the socket
+makes the execution thread that reads from the socket reach the end of the
+data stream, possibly with an exception.
+
+Close TCP connection
+....................
+
+If you called :meth:`~protocol.Protocol.receive_eof`, close the TCP
+connection now. This is a clean closure because the receive buffer is empty.
+
+After :meth:`~protocol.Protocol.receive_eof` signals the end of the read
+stream, :meth:`~protocol.Protocol.data_to_send` always signals the end of
+the write stream, unless it already ended. So, at this point, the TCP
+connection is already half-closed. The only reason for closing it now is to
+release resources related to the socket.
+
+Now you can exit the loop relaying data from the network to the application.
+
+Receive events
+..............
+
+Finally, call :meth:`~protocol.Protocol.events_received` to obtain events
+parsed from the data provided to :meth:`~protocol.Protocol.receive_data`::
+
+ events = connection.events_received()
+
+The first event will be the WebSocket opening handshake request or response.
+See `Opening a connection`_ above for details.
+
+All later events are WebSocket frames. There are two types of frames:
+
+* Data frames contain messages transferred over the WebSocket connections. You
+ should provide them to the application. See `Fragmentation`_ below for
+ how to reassemble messages from frames.
+* Control frames provide information about the connection's state. The main
+ use case is to expose an abstraction over ping and pong to the application.
+ Keep in mind that websockets responds to ping frames and close frames
+ automatically. Don't duplicate this functionality!
+
+From the application to the network
+-----------------------------------
+
+The connection object provides one method for each type of WebSocket frame.
+
+For sending a data frame:
+
+* :meth:`~protocol.Protocol.send_continuation`
+* :meth:`~protocol.Protocol.send_text`
+* :meth:`~protocol.Protocol.send_binary`
+
+These methods raise :exc:`~exceptions.ProtocolError` if you don't set
+the :attr:`FIN <websockets.frames.Frame.fin>` bit correctly in fragmented
+messages.
+
+For sending a control frame:
+
+* :meth:`~protocol.Protocol.send_close`
+* :meth:`~protocol.Protocol.send_ping`
+* :meth:`~protocol.Protocol.send_pong`
+
+:meth:`~protocol.Protocol.send_close` initiates the closing handshake.
+See `Closing a connection`_ below for details.
+
+If you encounter an unrecoverable error and you must fail the WebSocket
+connection, call :meth:`~protocol.Protocol.fail`.
+
+After any of the above, call :meth:`~protocol.Protocol.data_to_send` and
+send its output to the network, as shown in `Send data`_ above.
+
+If you called :meth:`~protocol.Protocol.send_close`
+or :meth:`~protocol.Protocol.fail`, you expect the end of the data
+stream. You should follow the process described in `Close TCP connection`_
+above in order to prevent dangling TCP connections.
+
+Closing a connection
+--------------------
+
+Under normal circumstances, when a server wants to close the TCP connection:
+
+* it closes the write side;
+* it reads until the end of the stream, because it expects the client to close
+ the read side;
+* it closes the socket.
+
+When a client wants to close the TCP connection:
+
+* it reads until the end of the stream, because it expects the server to close
+ the read side;
+* it closes the write side;
+* it closes the socket.
+
+Applying the rules described earlier in this document gives the intended
+result. As a reminder, the rules are:
+
+* When :meth:`~protocol.Protocol.data_to_send` returns the empty
+ bytestring, close the write side of the TCP connection.
+* When you reach the end of the read stream, close the TCP connection.
+* When :meth:`~protocol.Protocol.close_expected` returns :obj:`True`, if
+ you don't reach the end of the read stream quickly, close the TCP connection.
+
+Fragmentation
+-------------
+
+WebSocket messages may be fragmented. Since this is a protocol-level concern,
+you may choose to reassemble fragmented messages before handing them over to
+the application.
+
+To reassemble a message, read data frames until you get a frame where
+the :attr:`FIN <websockets.frames.Frame.fin>` bit is set, then concatenate
+the payloads of all frames.
+
+You will never receive an inconsistent sequence of frames because websockets
+raises a :exc:`~exceptions.ProtocolError` and fails the connection when this
+happens. However, you may receive an incomplete sequence if the connection
+drops in the middle of a fragmented message.
+
+Tips
+----
+
+Serialize operations
+....................
+
+The Sans-I/O layer expects to run sequentially. If your interact with it from
+multiple threads or coroutines, you must ensure correct serialization. This
+should happen automatically in a cooperative multitasking environment.
+
+However, you still have to make sure you don't break this property by
+accident. For example, serialize writes to the network
+when :meth:`~protocol.Protocol.data_to_send` returns multiple values to
+prevent concurrent writes from interleaving incorrectly.
+
+Avoid buffers
+.............
+
+The Sans-I/O layer doesn't do any buffering. It makes events available in
+:meth:`~protocol.Protocol.events_received` as soon as they're received.
+
+You should make incoming messages available to the application immediately and
+stop further processing until the application fetches them. This will usually
+result in the best performance.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst
new file mode 100644
index 0000000000..5eefc7711b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/howto/supervisor.rst
@@ -0,0 +1,131 @@
+Deploy with Supervisor
+======================
+
+This guide proposes a simple way to deploy a websockets server directly on a
+Linux or BSD operating system.
+
+We'll configure Supervisor_ to run several server processes and to restart
+them if needed.
+
+.. _Supervisor: http://supervisord.org/
+
+We'll bind all servers to the same port. The OS will take care of balancing
+connections.
+
+Create and activate a virtualenv:
+
+.. code-block:: console
+
+ $ python -m venv supervisor-websockets
+ $ . supervisor-websockets/bin/activate
+
+Install websockets and Supervisor:
+
+.. code-block:: console
+
+ $ pip install websockets
+ $ pip install supervisor
+
+Save this app to a file called ``app.py``:
+
+.. literalinclude:: ../../example/deployment/supervisor/app.py
+
+This is an echo server with two features added for the purpose of this guide:
+
+* It shuts down gracefully when receiving a ``SIGTERM`` signal;
+* It enables the ``reuse_port`` option of :meth:`~asyncio.loop.create_server`,
+ which in turns sets ``SO_REUSEPORT`` on the accept socket.
+
+Save this Supervisor configuration to ``supervisord.conf``:
+
+.. literalinclude:: ../../example/deployment/supervisor/supervisord.conf
+
+This is the minimal configuration required to keep four instances of the app
+running, restarting them if they exit.
+
+Now start Supervisor in the foreground:
+
+.. code-block:: console
+
+ $ supervisord -c supervisord.conf -n
+ INFO Increased RLIMIT_NOFILE limit to 1024
+ INFO supervisord started with pid 43596
+ INFO spawned: 'websockets-test_00' with pid 43597
+ INFO spawned: 'websockets-test_01' with pid 43598
+ INFO spawned: 'websockets-test_02' with pid 43599
+ INFO spawned: 'websockets-test_03' with pid 43600
+ INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+ INFO success: websockets-test_01 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+ INFO success: websockets-test_02 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+ INFO success: websockets-test_03 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+
+In another shell, after activating the virtualenv, we can connect to the app —
+press Ctrl-D to exit:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ > Hello!
+ < Hello!
+ Connection closed: 1000 (OK).
+
+Look at the pid of an instance of the app in the logs and terminate it:
+
+.. code-block:: console
+
+ $ kill -TERM 43597
+
+The logs show that Supervisor restarted this instance:
+
+.. code-block:: console
+
+ INFO exited: websockets-test_00 (exit status 0; expected)
+ INFO spawned: 'websockets-test_00' with pid 43629
+ INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
+
+Now let's check what happens when we shut down Supervisor, but first let's
+establish a connection and leave it open:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ >
+
+Look at the pid of supervisord itself in the logs and terminate it:
+
+.. code-block:: console
+
+ $ kill -TERM 43596
+
+The logs show that Supervisor terminated all instances of the app before
+exiting:
+
+.. code-block:: console
+
+ WARN received SIGTERM indicating exit request
+ INFO waiting for websockets-test_00, websockets-test_01, websockets-test_02, websockets-test_03 to die
+ INFO stopped: websockets-test_02 (exit status 0)
+ INFO stopped: websockets-test_03 (exit status 0)
+ INFO stopped: websockets-test_01 (exit status 0)
+ INFO stopped: websockets-test_00 (exit status 0)
+
+And you can see that the connection to the app was closed gracefully:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8080/
+ Connected to ws://localhost:8080/.
+ Connection closed: 1001 (going away).
+
+In this example, we've been sharing the same virtualenv for supervisor and
+websockets.
+
+In a real deployment, you would likely:
+
+* Install Supervisor with the package manager of the OS.
+* Create a virtualenv dedicated to your application.
+* Add ``environment=PATH="path/to/your/virtualenv/bin"`` in the Supervisor
+ configuration. Then ``python app.py`` runs in that virtualenv.
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst
new file mode 100644
index 0000000000..d9737db12a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/index.rst
@@ -0,0 +1,75 @@
+websockets
+==========
+
+|licence| |version| |pyversions| |tests| |docs| |openssf|
+
+.. |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
+
+.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests
+ :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml
+
+.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg
+ :target: https://websockets.readthedocs.io/
+
+.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge
+ :target: https://bestpractices.coreinfrastructure.org/projects/6475
+
+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
+
+It supports several network I/O and control flow paradigms:
+
+1. The default implementation builds upon :mod:`asyncio`, Python's standard
+ asynchronous I/O framework. It provides an elegant coroutine-based API. It's
+ ideal for servers that handle many clients concurrently.
+2. The :mod:`threading` implementation is a good alternative for clients,
+ especially if you aren't familiar with :mod:`asyncio`. It may also be used
+ for servers that don't need to serve many clients.
+3. The `Sans-I/O`_ implementation is designed for integrating in third-party
+ libraries, typically application servers, in addition being used internally
+ by websockets.
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+Here's an echo server with the :mod:`asyncio` API:
+
+.. literalinclude:: ../example/echo.py
+
+Here's how a client sends and receives messages with the :mod:`threading` API:
+
+.. literalinclude:: ../example/hello.py
+
+Don't worry about the opening and closing handshakes, pings and pongs, or any
+other behavior described in the WebSocket specification. websockets takes care
+of this under the hood so you can focus on your application!
+
+Also, websockets provides an interactive client:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8765/
+ Connected to ws://localhost:8765/.
+ > Hello world!
+ < Hello world!
+ Connection closed: 1000 (OK).
+
+Do you like it? :doc:`Let's dive in! <intro/index>`
+
+.. toctree::
+ :hidden:
+
+ intro/index
+ howto/index
+ faq/index
+ reference/index
+ topics/index
+ project/index
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst
new file mode 100644
index 0000000000..095262a207
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/index.rst
@@ -0,0 +1,46 @@
+Getting started
+===============
+
+.. currentmodule:: websockets
+
+Requirements
+------------
+
+websockets requires Python ≥ 3.8.
+
+.. admonition:: Use the most recent Python release
+ :class: tip
+
+ For each minor version (3.x), only the latest bugfix or security release
+ (3.x.y) is officially supported.
+
+It doesn't have any dependencies.
+
+.. _install:
+
+Installation
+------------
+
+Install websockets with:
+
+.. code-block:: console
+
+ $ pip install websockets
+
+Wheels are available for all platforms.
+
+Tutorial
+--------
+
+Learn how to build an real-time web application with websockets.
+
+.. toctree::
+
+ tutorial1
+ tutorial2
+ tutorial3
+
+In a hurry?
+-----------
+
+Look at the :doc:`quick start guide <../howto/quickstart>`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst
new file mode 100644
index 0000000000..ff85003b58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial1.rst
@@ -0,0 +1,591 @@
+Part 1 - Send & receive
+=======================
+
+.. currentmodule:: websockets
+
+In this tutorial, you're going to build a web-based `Connect Four`_ game.
+
+.. _Connect Four: https://en.wikipedia.org/wiki/Connect_Four
+
+The web removes the constraint of being in the same room for playing a game.
+Two players can connect over of the Internet, regardless of where they are,
+and play in their browsers.
+
+When a player makes a move, it should be reflected immediately on both sides.
+This is difficult to implement over HTTP due to the request-response style of
+the protocol.
+
+Indeed, there is no good way to be notified when the other player makes a
+move. Workarounds such as polling or long-polling introduce significant
+overhead.
+
+Enter `WebSocket <websocket>`_.
+
+The WebSocket protocol provides two-way communication between a browser and a
+server over a persistent connection. That's exactly what you need to exchange
+moves between players, via a server.
+
+.. admonition:: This is the first part of the tutorial.
+
+ * In this :doc:`first part <tutorial1>`, you will create a server and
+ connect one browser; you can play if you share the same browser.
+ * In the :doc:`second part <tutorial2>`, you will connect a second
+ browser; you can play from different browsers on a local network.
+ * In the :doc:`third part <tutorial3>`, you will deploy the game to the
+ web; you can play from any browser connected to the Internet.
+
+Prerequisites
+-------------
+
+This tutorial assumes basic knowledge of Python and JavaScript.
+
+If you're comfortable with :doc:`virtual environments <python:tutorial/venv>`,
+you can use one for this tutorial. Else, don't worry: websockets doesn't have
+any dependencies; it shouldn't create trouble in the default environment.
+
+If you haven't installed websockets yet, do it now:
+
+.. code-block:: console
+
+ $ pip install websockets
+
+Confirm that websockets is installed:
+
+.. code-block:: console
+
+ $ python -m websockets --version
+
+.. admonition:: This tutorial is written for websockets |release|.
+ :class: tip
+
+ If you installed another version, you should switch to the corresponding
+ version of the documentation.
+
+Download the starter kit
+------------------------
+
+Create a directory and download these three files:
+:download:`connect4.js <../../example/tutorial/start/connect4.js>`,
+:download:`connect4.css <../../example/tutorial/start/connect4.css>`,
+and :download:`connect4.py <../../example/tutorial/start/connect4.py>`.
+
+The JavaScript module, along with the CSS file, provides a web-based user
+interface. Here's its API.
+
+.. js:module:: connect4
+
+.. js:data:: PLAYER1
+
+ Color of the first player.
+
+.. js:data:: PLAYER2
+
+ Color of the second player.
+
+.. js:function:: createBoard(board)
+
+ Draw a board.
+
+ :param board: DOM element containing the board; must be initially empty.
+
+.. js:function:: playMove(board, player, column, row)
+
+ Play a move.
+
+ :param board: DOM element containing the board.
+ :param player: :js:data:`PLAYER1` or :js:data:`PLAYER2`.
+ :param column: between ``0`` and ``6``.
+ :param row: between ``0`` and ``5``.
+
+The Python module provides a class to record moves and tell when a player
+wins. Here's its API.
+
+.. module:: connect4
+
+.. data:: PLAYER1
+ :value: "red"
+
+ Color of the first player.
+
+.. data:: PLAYER2
+ :value: "yellow"
+
+ Color of the second player.
+
+.. class:: Connect4
+
+ A Connect Four game.
+
+ .. method:: play(player, column)
+
+ Play a move.
+
+ :param player: :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2`.
+ :param column: between ``0`` and ``6``.
+ :returns: Row where the checker lands, between ``0`` and ``5``.
+ :raises RuntimeError: if the move is illegal.
+
+ .. attribute:: moves
+
+ List of moves played during this game, as ``(player, column, row)``
+ tuples.
+
+ .. attribute:: winner
+
+ :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2` if they
+ won; :obj:`None` if the game is still ongoing.
+
+.. currentmodule:: websockets
+
+Bootstrap the web UI
+--------------------
+
+Create an ``index.html`` file next to ``connect4.js`` and ``connect4.css``
+with this content:
+
+.. literalinclude:: ../../example/tutorial/step1/index.html
+ :language: html
+
+This HTML page contains an empty ``<div>`` element where you will draw the
+Connect Four board. It loads a ``main.js`` script where you will write all
+your JavaScript code.
+
+Create a ``main.js`` file next to ``index.html``. In this script, when the
+page loads, draw the board:
+
+.. code-block:: javascript
+
+ import { createBoard, playMove } from "./connect4.js";
+
+ window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ });
+
+Open a shell, navigate to the directory containing these files, and start an
+HTTP server:
+
+.. code-block:: console
+
+ $ python -m http.server
+
+Open http://localhost:8000/ in a web browser. The page displays an empty board
+with seven columns and six rows. You will play moves in this board later.
+
+Bootstrap the server
+--------------------
+
+Create an ``app.py`` file next to ``connect4.py`` with this content:
+
+.. code-block:: python
+
+ #!/usr/bin/env python
+
+ import asyncio
+
+ import websockets
+
+
+ async def handler(websocket):
+ while True:
+ message = await websocket.recv()
+ print(message)
+
+
+ async def main():
+ async with websockets.serve(handler, "", 8001):
+ await asyncio.Future() # run forever
+
+
+ if __name__ == "__main__":
+ asyncio.run(main())
+
+The entry point of this program is ``asyncio.run(main())``. It creates an
+asyncio event loop, runs the ``main()`` coroutine, and shuts down the loop.
+
+The ``main()`` coroutine calls :func:`~server.serve` to start a websockets
+server. :func:`~server.serve` takes three positional arguments:
+
+* ``handler`` is a coroutine that manages a connection. When a client
+ connects, websockets calls ``handler`` with the connection in argument.
+ When ``handler`` terminates, websockets closes the connection.
+* The second argument defines the network interfaces where the server can be
+ reached. Here, the server listens on all interfaces, so that other devices
+ on the same local network can connect.
+* The third argument is the port on which the server listens.
+
+Invoking :func:`~server.serve` as an asynchronous context manager, in an
+``async with`` block, ensures that the server shuts down properly when
+terminating the program.
+
+For each connection, the ``handler()`` coroutine runs an infinite loop that
+receives messages from the browser and prints them.
+
+Open a shell, navigate to the directory containing ``app.py``, and start the
+server:
+
+.. code-block:: console
+
+ $ python app.py
+
+This doesn't display anything. Hopefully the WebSocket server is running.
+Let's make sure that it works. You cannot test the WebSocket server with a
+web browser like you tested the HTTP server. However, you can test it with
+websockets' interactive client.
+
+Open another shell and run this command:
+
+.. code-block:: console
+
+ $ python -m websockets ws://localhost:8001/
+
+You get a prompt. Type a message and press "Enter". Switch to the shell where
+the server is running and check that the server received the message. Good!
+
+Exit the interactive client with Ctrl-C or Ctrl-D.
+
+Now, if you look at the console where you started the server, you can see the
+stack trace of an exception:
+
+.. code-block:: pytb
+
+ connection handler failed
+ Traceback (most recent call last):
+ ...
+ File "app.py", line 22, in handler
+ message = await websocket.recv()
+ ...
+ websockets.exceptions.ConnectionClosedOK: received 1000 (OK); then sent 1000 (OK)
+
+Indeed, the server was waiting for the next message
+with :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` when the client
+disconnected. When this happens, websockets raises
+a :exc:`~exceptions.ConnectionClosedOK` exception to let you know that you
+won't receive another message on this connection.
+
+This exception creates noise in the server logs, making it more difficult to
+spot real errors when you add functionality to the server. Catch it in the
+``handler()`` coroutine:
+
+.. code-block:: python
+
+ async def handler(websocket):
+ while True:
+ try:
+ message = await websocket.recv()
+ except websockets.ConnectionClosedOK:
+ break
+ print(message)
+
+Stop the server with Ctrl-C and start it again:
+
+.. code-block:: console
+
+ $ python app.py
+
+.. admonition:: You must restart the WebSocket server when you make changes.
+ :class: tip
+
+ The WebSocket server loads the Python code in ``app.py`` then serves every
+ WebSocket request with this version of the code. As a consequence,
+ changes to ``app.py`` aren't visible until you restart the server.
+
+ This is unlike the HTTP server that you started earlier with ``python -m
+ http.server``. For every request, this HTTP server reads the target file
+ and sends it. That's why changes are immediately visible.
+
+ It is possible to :doc:`restart the WebSocket server automatically
+ <../howto/autoreload>` but this isn't necessary for this tutorial.
+
+Try connecting and disconnecting the interactive client again.
+The :exc:`~exceptions.ConnectionClosedOK` exception doesn't appear anymore.
+
+This pattern is so common that websockets provides a shortcut for iterating
+over messages received on the connection until the client disconnects:
+
+.. code-block:: python
+
+ async def handler(websocket):
+ async for message in websocket:
+ print(message)
+
+Restart the server and check with the interactive client that its behavior
+didn't change.
+
+At this point, you bootstrapped a web application and a WebSocket server.
+Let's connect them.
+
+Transmit from browser to server
+-------------------------------
+
+In JavaScript, you open a WebSocket connection as follows:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket("ws://localhost:8001/");
+
+Before you exchange messages with the server, you need to decide their format.
+There is no universal convention for this.
+
+Let's use JSON objects with a ``type`` key identifying the type of the event
+and the rest of the object containing properties of the event.
+
+Here's an event describing a move in the middle slot of the board:
+
+.. code-block:: javascript
+
+ const event = {type: "play", column: 3};
+
+Here's how to serialize this event to JSON and send it to the server:
+
+.. code-block:: javascript
+
+ websocket.send(JSON.stringify(event));
+
+Now you have all the building blocks to send moves to the server.
+
+Add this function to ``main.js``:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :language: js
+ :start-at: function sendMoves
+ :end-before: window.addEventListener
+
+``sendMoves()`` registers a listener for ``click`` events on the board. The
+listener figures out which column was clicked, builds a event of type
+``"play"``, serializes it, and sends it to the server.
+
+Modify the initialization to open the WebSocket connection and call the
+``sendMoves()`` function:
+
+.. code-block:: javascript
+
+ window.addEventListener("DOMContentLoaded", () => {
+ // Initialize the UI.
+ const board = document.querySelector(".board");
+ createBoard(board);
+ // Open the WebSocket connection and register event handlers.
+ const websocket = new WebSocket("ws://localhost:8001/");
+ sendMoves(board, websocket);
+ });
+
+Check that the HTTP server and the WebSocket server are still running. If you
+stopped them, here are the commands to start them again:
+
+.. code-block:: console
+
+ $ python -m http.server
+
+.. code-block:: console
+
+ $ python app.py
+
+Refresh http://localhost:8000/ in your web browser. Click various columns in
+the board. The server receives messages with the expected column number.
+
+There isn't any feedback in the board because you haven't implemented that
+yet. Let's do it.
+
+Transmit from server to browser
+-------------------------------
+
+In JavaScript, you receive WebSocket messages by listening to ``message``
+events. Here's how to receive a message from the server and deserialize it
+from JSON:
+
+.. code-block:: javascript
+
+ websocket.addEventListener("message", ({ data }) => {
+ const event = JSON.parse(data);
+ // do something with event
+ });
+
+You're going to need three types of messages from the server to the browser:
+
+.. code-block:: javascript
+
+ {type: "play", player: "red", column: 3, row: 0}
+ {type: "win", player: "red"}
+ {type: "error", message: "This slot is full."}
+
+The JavaScript code receiving these messages will dispatch events depending on
+their type and take appropriate action. For example, it will react to an
+event of type ``"play"`` by displaying the move on the board with
+the :js:func:`~connect4.playMove` function.
+
+Add this function to ``main.js``:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :language: js
+ :start-at: function showMessage
+ :end-before: function sendMoves
+
+.. admonition:: Why does ``showMessage`` use ``window.setTimeout``?
+ :class: hint
+
+ When :js:func:`playMove` modifies the state of the board, the browser
+ renders changes asynchronously. Conversely, ``window.alert()`` runs
+ synchronously and blocks rendering while the alert is visible.
+
+ If you called ``window.alert()`` immediately after :js:func:`playMove`,
+ the browser could display the alert before rendering the move. You could
+ get a "Player red wins!" alert without seeing red's last move.
+
+ We're using ``window.alert()`` for simplicity in this tutorial. A real
+ application would display these messages in the user interface instead.
+ It wouldn't be vulnerable to this problem.
+
+Modify the initialization to call the ``receiveMoves()`` function:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :language: js
+ :start-at: window.addEventListener
+
+At this point, the user interface should receive events properly. Let's test
+it by modifying the server to send some events.
+
+Sending an event from Python is quite similar to JavaScript:
+
+.. code-block:: python
+
+ event = {"type": "play", "player": "red", "column": 3, "row": 0}
+ await websocket.send(json.dumps(event))
+
+.. admonition:: Don't forget to serialize the event with :func:`json.dumps`.
+ :class: tip
+
+ Else, websockets raises ``TypeError: data is a dict-like object``.
+
+Modify the ``handler()`` coroutine in ``app.py`` as follows:
+
+.. code-block:: python
+
+ import json
+
+ from connect4 import PLAYER1, PLAYER2
+
+ async def handler(websocket):
+ for player, column, row in [
+ (PLAYER1, 3, 0),
+ (PLAYER2, 3, 1),
+ (PLAYER1, 4, 0),
+ (PLAYER2, 4, 1),
+ (PLAYER1, 2, 0),
+ (PLAYER2, 1, 0),
+ (PLAYER1, 5, 0),
+ ]:
+ event = {
+ "type": "play",
+ "player": player,
+ "column": column,
+ "row": row,
+ }
+ await websocket.send(json.dumps(event))
+ await asyncio.sleep(0.5)
+ event = {
+ "type": "win",
+ "player": PLAYER1,
+ }
+ await websocket.send(json.dumps(event))
+
+Restart the WebSocket server and refresh http://localhost:8000/ in your web
+browser. Seven moves appear at 0.5 second intervals. Then an alert announces
+the winner.
+
+Good! Now you know how to communicate both ways.
+
+Once you plug the game engine to process moves, you will have a fully
+functional game.
+
+Add the game logic
+------------------
+
+In the ``handler()`` coroutine, you're going to initialize a game:
+
+.. code-block:: python
+
+ from connect4 import Connect4
+
+ async def handler(websocket):
+ # Initialize a Connect Four game.
+ game = Connect4()
+
+ ...
+
+Then, you're going to iterate over incoming messages and take these steps:
+
+* parse an event of type ``"play"``, the only type of event that the user
+ interface sends;
+* play the move in the board with the :meth:`~connect4.Connect4.play` method,
+ alternating between the two players;
+* if :meth:`~connect4.Connect4.play` raises :exc:`RuntimeError` because the
+ move is illegal, send an event of type ``"error"``;
+* else, send an event of type ``"play"`` to tell the user interface where the
+ checker lands;
+* if the move won the game, send an event of type ``"win"``.
+
+Try to implement this by yourself!
+
+Keep in mind that you must restart the WebSocket server and reload the page in
+the browser when you make changes.
+
+When it works, you can play the game from a single browser, with players
+taking alternate turns.
+
+.. admonition:: Enable debug logs to see all messages sent and received.
+ :class: tip
+
+ Here's how to enable debug logs:
+
+ .. code-block:: python
+
+ import logging
+
+ logging.basicConfig(format="%(message)s", level=logging.DEBUG)
+
+If you're stuck, a solution is available at the bottom of this document.
+
+Summary
+-------
+
+In this first part of the tutorial, you learned how to:
+
+* build and run a WebSocket server in Python with :func:`~server.serve`;
+* receive a message in a connection handler
+ with :meth:`~server.WebSocketServerProtocol.recv`;
+* send a message in a connection handler
+ with :meth:`~server.WebSocketServerProtocol.send`;
+* iterate over incoming messages with ``async for
+ message in websocket: ...``;
+* open a WebSocket connection in JavaScript with the ``WebSocket`` API;
+* send messages in a browser with ``WebSocket.send()``;
+* receive messages in a browser by listening to ``message`` events;
+* design a set of events to be exchanged between the browser and the server.
+
+You can now play a Connect Four game in a browser, communicating over a
+WebSocket connection with a server where the game logic resides!
+
+However, the two players share a browser, so the constraint of being in the
+same room still applies.
+
+Move on to the :doc:`second part <tutorial2>` of the tutorial to break this
+constraint and play from separate browsers.
+
+Solution
+--------
+
+.. literalinclude:: ../../example/tutorial/step1/app.py
+ :caption: app.py
+ :language: python
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step1/index.html
+ :caption: index.html
+ :language: html
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step1/main.js
+ :caption: main.js
+ :language: js
+ :linenos:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst
new file mode 100644
index 0000000000..5ac4ae9dd5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial2.rst
@@ -0,0 +1,565 @@
+Part 2 - Route & broadcast
+==========================
+
+.. currentmodule:: websockets
+
+.. admonition:: This is the second part of the tutorial.
+
+ * In the :doc:`first part <tutorial1>`, you created a server and
+ connected one browser; you could play if you shared the same browser.
+ * In this :doc:`second part <tutorial2>`, you will connect a second
+ browser; you can play from different browsers on a local network.
+ * In the :doc:`third part <tutorial3>`, you will deploy the game to the
+ web; you can play from any browser connected to the Internet.
+
+In the first part of the tutorial, you opened a WebSocket connection from a
+browser to a server and exchanged events to play moves. The state of the game
+was stored in an instance of the :class:`~connect4.Connect4` class,
+referenced as a local variable in the connection handler coroutine.
+
+Now you want to open two WebSocket connections from two separate browsers, one
+for each player, to the same server in order to play the same game. This
+requires moving the state of the game to a place where both connections can
+access it.
+
+Share game state
+----------------
+
+As long as you're running a single server process, you can share state by
+storing it in a global variable.
+
+.. admonition:: What if you need to scale to multiple server processes?
+ :class: hint
+
+ In that case, you must design a way for the process that handles a given
+ connection to be aware of relevant events for that client. This is often
+ achieved with a publish / subscribe mechanism.
+
+How can you make two connection handlers agree on which game they're playing?
+When the first player starts a game, you give it an identifier. Then, you
+communicate the identifier to the second player. When the second player joins
+the game, you look it up with the identifier.
+
+In addition to the game itself, you need to keep track of the WebSocket
+connections of the two players. Since both players receive the same events,
+you don't need to treat the two connections differently; you can store both
+in the same set.
+
+Let's sketch this in code.
+
+A module-level :class:`dict` enables lookups by identifier:
+
+.. code-block:: python
+
+ JOIN = {}
+
+When the first player starts the game, initialize and store it:
+
+.. code-block:: python
+
+ import secrets
+
+ async def handler(websocket):
+ ...
+
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access token.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ try:
+
+ ...
+
+ finally:
+ del JOIN[join_key]
+
+When the second player joins the game, look it up:
+
+.. code-block:: python
+
+ async def handler(websocket):
+ ...
+
+ join_key = ... # TODO
+
+ # Find the Connect Four game.
+ game, connected = JOIN[join_key]
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+
+ ...
+
+ finally:
+ connected.remove(websocket)
+
+Notice how we're carefully cleaning up global state with ``try: ...
+finally: ...`` blocks. Else, we could leave references to games or
+connections in global state, which would cause a memory leak.
+
+In both connection handlers, you have a ``game`` pointing to the same
+:class:`~connect4.Connect4` instance, so you can interact with the game,
+and a ``connected`` set of connections, so you can send game events to
+both players as follows:
+
+.. code-block:: python
+
+ async def handler(websocket):
+
+ ...
+
+ for connection in connected:
+ await connection.send(json.dumps(event))
+
+ ...
+
+Perhaps you spotted a major piece missing from the puzzle. How does the second
+player obtain ``join_key``? Let's design new events to carry this information.
+
+To start a game, the first player sends an ``"init"`` event:
+
+.. code-block:: javascript
+
+ {type: "init"}
+
+The connection handler for the first player creates a game as shown above and
+responds with:
+
+.. code-block:: javascript
+
+ {type: "init", join: "<join_key>"}
+
+With this information, the user interface of the first player can create a
+link to ``http://localhost:8000/?join=<join_key>``. For the sake of simplicity,
+we will assume that the first player shares this link with the second player
+outside of the application, for example via an instant messaging service.
+
+To join the game, the second player sends a different ``"init"`` event:
+
+.. code-block:: javascript
+
+ {type: "init", join: "<join_key>"}
+
+The connection handler for the second player can look up the game with the
+join key as shown above. There is no need to respond.
+
+Let's dive into the details of implementing this design.
+
+Start a game
+------------
+
+We'll start with the initialization sequence for the first player.
+
+In ``main.js``, define a function to send an initialization event when the
+WebSocket connection is established, which triggers an ``open`` event:
+
+.. code-block:: javascript
+
+ function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event for the first player.
+ const event = { type: "init" };
+ websocket.send(JSON.stringify(event));
+ });
+ }
+
+Update the initialization sequence to call ``initGame()``:
+
+.. literalinclude:: ../../example/tutorial/step2/main.js
+ :language: js
+ :start-at: window.addEventListener
+
+In ``app.py``, define a new ``handler`` coroutine — keep a copy of the
+previous one to reuse it later:
+
+.. code-block:: python
+
+ import secrets
+
+
+ JOIN = {}
+
+
+ async def start(websocket):
+ # Initialize a Connect Four game, the set of WebSocket connections
+ # receiving moves from this game, and secret access token.
+ game = Connect4()
+ connected = {websocket}
+
+ join_key = secrets.token_urlsafe(12)
+ JOIN[join_key] = game, connected
+
+ try:
+ # Send the secret access token to the browser of the first player,
+ # where it'll be used for building a "join" link.
+ event = {
+ "type": "init",
+ "join": join_key,
+ }
+ await websocket.send(json.dumps(event))
+
+ # Temporary - for testing.
+ print("first player started game", id(game))
+ async for message in websocket:
+ print("first player sent", message)
+
+ finally:
+ del JOIN[join_key]
+
+
+ async def handler(websocket):
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ # First player starts a new game.
+ await start(websocket)
+
+In ``index.html``, add an ``<a>`` element to display the link to share with
+the other player.
+
+.. code-block:: html
+
+ <body>
+ <div class="actions">
+ <a class="action join" href="">Join</a>
+ </div>
+ <!-- ... -->
+ </body>
+
+In ``main.js``, modify ``receiveMoves()`` to handle the ``"init"`` message and
+set the target of that link:
+
+.. code-block:: javascript
+
+ switch (event.type) {
+ case "init":
+ // Create link for inviting the second player.
+ document.querySelector(".join").href = "?join=" + event.join;
+ break;
+ // ...
+ }
+
+Restart the WebSocket server and reload http://localhost:8000/ in the browser.
+There's a link labeled JOIN below the board with a target that looks like
+http://localhost:8000/?join=95ftAaU5DJVP1zvb.
+
+The server logs say ``first player started game ...``. If you click the board,
+you see ``"play"`` events. There is no feedback in the UI, though, because
+you haven't restored the game logic yet.
+
+Before we get there, let's handle links with a ``join`` query parameter.
+
+Join a game
+-----------
+
+We'll now update the initialization sequence to account for the second
+player.
+
+In ``main.js``, update ``initGame()`` to send the join key in the ``"init"``
+message when it's in the URL:
+
+.. code-block:: javascript
+
+ function initGame(websocket) {
+ websocket.addEventListener("open", () => {
+ // Send an "init" event according to who is connecting.
+ const params = new URLSearchParams(window.location.search);
+ let event = { type: "init" };
+ if (params.has("join")) {
+ // Second player joins an existing game.
+ event.join = params.get("join");
+ } else {
+ // First player starts a new game.
+ }
+ websocket.send(JSON.stringify(event));
+ });
+ }
+
+In ``app.py``, update the ``handler`` coroutine to look for the join key in
+the ``"init"`` message, then load that game:
+
+.. code-block:: python
+
+ async def error(websocket, message):
+ event = {
+ "type": "error",
+ "message": message,
+ }
+ await websocket.send(json.dumps(event))
+
+
+ async def join(websocket, join_key):
+ # Find the Connect Four game.
+ try:
+ game, connected = JOIN[join_key]
+ except KeyError:
+ await error(websocket, "Game not found.")
+ return
+
+ # Register to receive moves from this game.
+ connected.add(websocket)
+ try:
+
+ # Temporary - for testing.
+ print("second player joined game", id(game))
+ async for message in websocket:
+ print("second player sent", message)
+
+ finally:
+ connected.remove(websocket)
+
+
+ async def handler(websocket):
+ # Receive and parse the "init" event from the UI.
+ message = await websocket.recv()
+ event = json.loads(message)
+ assert event["type"] == "init"
+
+ if "join" in event:
+ # Second player joins an existing game.
+ await join(websocket, event["join"])
+ else:
+ # First player starts a new game.
+ await start(websocket)
+
+Restart the WebSocket server and reload http://localhost:8000/ in the browser.
+
+Copy the link labeled JOIN and open it in another browser. You may also open
+it in another tab or another window of the same browser; however, that makes
+it a bit tricky to remember which one is the first or second player.
+
+.. admonition:: You must start a new game when you restart the server.
+ :class: tip
+
+ Since games are stored in the memory of the Python process, they're lost
+ when you stop the server.
+
+ Whenever you make changes to ``app.py``, you must restart the server,
+ create a new game in a browser, and join it in another browser.
+
+The server logs say ``first player started game ...`` and ``second player
+joined game ...``. The numbers match, proving that the ``game`` local
+variable in both connection handlers points to same object in the memory of
+the Python process.
+
+Click the board in either browser. The server receives ``"play"`` events from
+the corresponding player.
+
+In the initialization sequence, you're routing connections to ``start()`` or
+``join()`` depending on the first message received by the server. This is a
+common pattern in servers that handle different clients.
+
+.. admonition:: Why not use different URIs for ``start()`` and ``join()``?
+ :class: hint
+
+ Instead of sending an initialization event, you could encode the join key
+ in the WebSocket URI e.g. ``ws://localhost:8001/join/<join_key>``. The
+ WebSocket server would parse ``websocket.path`` and route the connection,
+ similar to how HTTP servers route requests.
+
+ When you need to send sensitive data like authentication credentials to
+ the server, sending it an event is considered more secure than encoding
+ it in the URI because URIs end up in logs.
+
+ For the purposes of this tutorial, both approaches are equivalent because
+ the join key comes from an HTTP URL. There isn't much at risk anyway!
+
+Now you can restore the logic for playing moves and you'll have a fully
+functional two-player game.
+
+Add the game logic
+------------------
+
+Once the initialization is done, the game is symmetrical, so you can write a
+single coroutine to process the moves of both players:
+
+.. code-block:: python
+
+ async def play(websocket, game, player, connected):
+ ...
+
+With such a coroutine, you can replace the temporary code for testing in
+``start()`` by:
+
+.. code-block:: python
+
+ await play(websocket, game, PLAYER1, connected)
+
+and in ``join()`` by:
+
+.. code-block:: python
+
+ await play(websocket, game, PLAYER2, connected)
+
+The ``play()`` coroutine will reuse much of the code you wrote in the first
+part of the tutorial.
+
+Try to implement this by yourself!
+
+Keep in mind that you must restart the WebSocket server, reload the page to
+start a new game with the first player, copy the JOIN link, and join the game
+with the second player when you make changes.
+
+When ``play()`` works, you can play the game from two separate browsers,
+possibly running on separate computers on the same local network.
+
+A complete solution is available at the bottom of this document.
+
+Watch a game
+------------
+
+Let's add one more feature: allow spectators to watch the game.
+
+The process for inviting a spectator can be the same as for inviting the
+second player. You will have to duplicate all the initialization logic:
+
+- declare a ``WATCH`` global variable similar to ``JOIN``;
+- generate a watch key when creating a game; it must be different from the
+ join key, or else a spectator could hijack a game by tweaking the URL;
+- include the watch key in the ``"init"`` event sent to the first player;
+- generate a WATCH link in the UI with a ``watch`` query parameter;
+- update the ``initGame()`` function to handle such links;
+- update the ``handler()`` coroutine to invoke a ``watch()`` coroutine for
+ spectators;
+- prevent ``sendMoves()`` from sending ``"play"`` events for spectators.
+
+Once the initialization sequence is done, watching a game is as simple as
+registering the WebSocket connection in the ``connected`` set in order to
+receive game events and doing nothing until the spectator disconnects. You
+can wait for a connection to terminate with
+:meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`:
+
+.. code-block:: python
+
+ async def watch(websocket, watch_key):
+
+ ...
+
+ connected.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ connected.remove(websocket)
+
+The connection can terminate because the ``receiveMoves()`` function closed it
+explicitly after receiving a ``"win"`` event, because the spectator closed
+their browser, or because the network failed.
+
+Again, try to implement this by yourself.
+
+When ``watch()`` works, you can invite spectators to watch the game from other
+browsers, as long as they're on the same local network.
+
+As a further improvement, you may support adding spectators while a game is
+already in progress. This requires replaying moves that were played before
+the spectator was added to the ``connected`` set. Past moves are available in
+the :attr:`~connect4.Connect4.moves` attribute of the game.
+
+This feature is included in the solution proposed below.
+
+Broadcast
+---------
+
+When you need to send a message to the two players and to all spectators,
+you're using this pattern:
+
+.. code-block:: python
+
+ async def handler(websocket):
+
+ ...
+
+ for connection in connected:
+ await connection.send(json.dumps(event))
+
+ ...
+
+Since this is a very common pattern in WebSocket servers, websockets provides
+the :func:`broadcast` helper for this purpose:
+
+.. code-block:: python
+
+ async def handler(websocket):
+
+ ...
+
+ websockets.broadcast(connected, json.dumps(event))
+
+ ...
+
+Calling :func:`broadcast` once is more efficient than
+calling :meth:`~legacy.protocol.WebSocketCommonProtocol.send` in a loop.
+
+However, there's a subtle difference in behavior. Did you notice that there's
+no ``await`` in the second version? Indeed, :func:`broadcast` is a function,
+not a coroutine like :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+or :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`.
+
+It's quite obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`
+is a coroutine. When you want to receive the next message, you have to wait
+until the client sends it and the network transmits it.
+
+It's less obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.send` is
+a coroutine. If you send many messages or large messages, you could write
+data faster than the network can transmit it or the client can read it. Then,
+outgoing data will pile up in buffers, which will consume memory and may
+crash your application.
+
+To avoid this problem, :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+waits until the write buffer drains. By slowing down the application as
+necessary, this ensures that the server doesn't send data too quickly. This
+is called backpressure and it's useful for building robust systems.
+
+That said, when you're sending the same messages to many clients in a loop,
+applying backpressure in this way can become counterproductive. When you're
+broadcasting, you don't want to slow down everyone to the pace of the slowest
+clients; you want to drop clients that cannot keep up with the data stream.
+That's why :func:`broadcast` doesn't wait until write buffers drain.
+
+For our Connect Four game, there's no difference in practice: the total amount
+of data sent on a connection for a game of Connect Four is less than 64 KB,
+so the write buffer never fills up and backpressure never kicks in anyway.
+
+Summary
+-------
+
+In this second part of the tutorial, you learned how to:
+
+* configure a connection by exchanging initialization messages;
+* keep track of connections within a single server process;
+* wait until a client disconnects in a connection handler;
+* broadcast a message to many connections efficiently.
+
+You can now play a Connect Four game from separate browser, communicating over
+WebSocket connections with a server that synchronizes the game logic!
+
+However, the two players have to be on the same local network as the server,
+so the constraint of being in the same place still mostly applies.
+
+Head over to the :doc:`third part <tutorial3>` of the tutorial to deploy the
+game to the web and remove this constraint.
+
+Solution
+--------
+
+.. literalinclude:: ../../example/tutorial/step2/app.py
+ :caption: app.py
+ :language: python
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step2/index.html
+ :caption: index.html
+ :language: html
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step2/main.js
+ :caption: main.js
+ :language: js
+ :linenos:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst
new file mode 100644
index 0000000000..6fdec113b2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/intro/tutorial3.rst
@@ -0,0 +1,290 @@
+Part 3 - Deploy to the web
+==========================
+
+.. currentmodule:: websockets
+
+.. admonition:: This is the third part of the tutorial.
+
+ * In the :doc:`first part <tutorial1>`, you created a server and
+ connected one browser; you could play if you shared the same browser.
+ * In this :doc:`second part <tutorial2>`, you connected a second browser;
+ you could play from different browsers on a local network.
+ * In this :doc:`third part <tutorial3>`, you will deploy the game to the
+ web; you can play from any browser connected to the Internet.
+
+In the first and second parts of the tutorial, for local development, you ran
+an HTTP server on ``http://localhost:8000/`` with:
+
+.. code-block:: console
+
+ $ python -m http.server
+
+and a WebSocket server on ``ws://localhost:8001/`` with:
+
+.. code-block:: console
+
+ $ python app.py
+
+Now you want to deploy these servers on the Internet. There's a vast range of
+hosting providers to choose from. For the sake of simplicity, we'll rely on:
+
+* GitHub Pages for the HTTP server;
+* Heroku for the WebSocket server.
+
+Commit project to git
+---------------------
+
+Perhaps you committed your work to git while you were progressing through the
+tutorial. If you didn't, now is a good time, because GitHub and Heroku offer
+git-based deployment workflows.
+
+Initialize a git repository:
+
+.. code-block:: console
+
+ $ git init -b main
+ Initialized empty Git repository in websockets-tutorial/.git/
+ $ git commit --allow-empty -m "Initial commit."
+ [main (root-commit) ...] Initial commit.
+
+Add all files and commit:
+
+.. code-block:: console
+
+ $ git add .
+ $ git commit -m "Initial implementation of Connect Four game."
+ [main ...] Initial implementation of Connect Four game.
+ 6 files changed, 500 insertions(+)
+ create mode 100644 app.py
+ create mode 100644 connect4.css
+ create mode 100644 connect4.js
+ create mode 100644 connect4.py
+ create mode 100644 index.html
+ create mode 100644 main.js
+
+Prepare the WebSocket server
+----------------------------
+
+Before you deploy the server, you must adapt it to meet requirements of
+Heroku's runtime. This involves two small changes:
+
+1. Heroku expects the server to `listen on a specific port`_, provided in the
+ ``$PORT`` environment variable.
+
+2. Heroku sends a ``SIGTERM`` signal when `shutting down a dyno`_, which
+ should trigger a clean exit.
+
+.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port
+
+.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown
+
+Adapt the ``main()`` coroutine accordingly:
+
+.. code-block:: python
+
+ import os
+ import signal
+
+.. literalinclude:: ../../example/tutorial/step3/app.py
+ :pyobject: main
+
+To catch the ``SIGTERM`` signal, ``main()`` creates a :class:`~asyncio.Future`
+called ``stop`` and registers a signal handler that sets the result of this
+future. The value of the future doesn't matter; it's only for waiting for
+``SIGTERM``.
+
+Then, by using :func:`~server.serve` as a context manager and exiting the
+context when ``stop`` has a result, ``main()`` ensures that the server closes
+connections cleanly and exits on ``SIGTERM``.
+
+The app is now fully compatible with Heroku.
+
+Deploy the WebSocket server
+---------------------------
+
+Create a ``requirements.txt`` file with this content to install ``websockets``
+when building the image:
+
+.. literalinclude:: ../../example/tutorial/step3/requirements.txt
+ :language: text
+
+.. admonition:: Heroku treats ``requirements.txt`` as a signal to `detect a Python app`_.
+ :class: tip
+
+ That's why you don't need to declare that you need a Python runtime.
+
+.. _detect a Python app: https://devcenter.heroku.com/articles/python-support#recognizing-a-python-app
+
+Create a ``Procfile`` file with this content to configure the command for
+running the server:
+
+.. literalinclude:: ../../example/tutorial/step3/Procfile
+ :language: text
+
+Commit your changes:
+
+.. code-block:: console
+
+ $ git add .
+ $ git commit -m "Deploy to Heroku."
+ [main ...] Deploy to Heroku.
+ 3 files changed, 12 insertions(+), 2 deletions(-)
+ create mode 100644 Procfile
+ create mode 100644 requirements.txt
+
+Follow the `set-up instructions`_ to install the Heroku CLI and to log in, if
+you haven't done that yet.
+
+.. _set-up instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up
+
+Create a Heroku app. You must choose a unique name and replace
+``websockets-tutorial`` by this name in the following command:
+
+.. code-block:: console
+
+ $ heroku create websockets-tutorial
+ Creating ⬢ websockets-tutorial... done
+ https://websockets-tutorial.herokuapp.com/ | https://git.heroku.com/websockets-tutorial.git
+
+If you reuse a name that someone else already uses, you will receive this
+error; if this happens, try another name:
+
+.. code-block:: console
+
+ $ heroku create websockets-tutorial
+ Creating ⬢ websockets-tutorial... !
+ ▸ Name websockets-tutorial is already taken
+
+Deploy by pushing the code to Heroku:
+
+.. code-block:: console
+
+ $ git push heroku
+
+ ... lots of output...
+
+ remote: Released v1
+ remote: https://websockets-tutorial.herokuapp.com/ deployed to Heroku
+ remote:
+ remote: Verifying deploy... done.
+ To https://git.heroku.com/websockets-tutorial.git
+ * [new branch] main -> main
+
+You can test the WebSocket server with the interactive client exactly like you
+did in the first part of the tutorial. Replace ``websockets-tutorial`` by the
+name of your app in the following command:
+
+.. code-block:: console
+
+ $ python -m websockets wss://websockets-tutorial.herokuapp.com/
+ Connected to wss://websockets-tutorial.herokuapp.com/.
+ > {"type": "init"}
+ < {"type": "init", "join": "54ICxFae_Ip7TJE2", "watch": "634w44TblL5Dbd9a"}
+ Connection closed: 1000 (OK).
+
+It works!
+
+Prepare the web application
+---------------------------
+
+Before you deploy the web application, perhaps you're wondering how it will
+locate the WebSocket server? Indeed, at this point, its address is hard-coded
+in ``main.js``:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket("ws://localhost:8001/");
+
+You can take this strategy one step further by checking the address of the
+HTTP server and determining the address of the WebSocket server accordingly.
+
+Add this function to ``main.js``; replace ``python-websockets`` by your GitHub
+username and ``websockets-tutorial`` by the name of your app on Heroku:
+
+.. literalinclude:: ../../example/tutorial/step3/main.js
+ :language: js
+ :start-at: function getWebSocketServer
+ :end-before: function initGame
+
+Then, update the initialization to connect to this address instead:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket(getWebSocketServer());
+
+Commit your changes:
+
+.. code-block:: console
+
+ $ git add .
+ $ git commit -m "Configure WebSocket server address."
+ [main ...] Configure WebSocket server address.
+ 1 file changed, 11 insertions(+), 1 deletion(-)
+
+Deploy the web application
+--------------------------
+
+Go to GitHub and create a new repository called ``websockets-tutorial``.
+
+Push your code to this repository. You must replace ``python-websockets`` by
+your GitHub username in the following command:
+
+.. code-block:: console
+
+ $ git remote add origin git@github.com:python-websockets/websockets-tutorial.git
+ $ git push -u origin main
+ Enumerating objects: 11, done.
+ Counting objects: 100% (11/11), done.
+ Delta compression using up to 8 threads
+ Compressing objects: 100% (10/10), done.
+ Writing objects: 100% (11/11), 5.90 KiB | 2.95 MiB/s, done.
+ Total 11 (delta 0), reused 0 (delta 0), pack-reused 0
+ To github.com:<username>/websockets-tutorial.git
+ * [new branch] main -> main
+ Branch 'main' set up to track remote branch 'main' from 'origin'.
+
+Go back to GitHub, open the Settings tab of the repository and select Pages in
+the menu. Select the main branch as source and click Save. GitHub tells you
+that your site is published.
+
+Follow the link and start a game!
+
+Summary
+-------
+
+In this third part of the tutorial, you learned how to deploy a WebSocket
+application with Heroku.
+
+You can start a Connect Four game, send the JOIN link to a friend, and play
+over the Internet!
+
+Congratulations for completing the tutorial. Enjoy building real-time web
+applications with websockets!
+
+Solution
+--------
+
+.. literalinclude:: ../../example/tutorial/step3/app.py
+ :caption: app.py
+ :language: python
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/index.html
+ :caption: index.html
+ :language: html
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/main.js
+ :caption: main.js
+ :language: js
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/Procfile
+ :caption: Procfile
+ :language: text
+ :linenos:
+
+.. literalinclude:: ../../example/tutorial/step3/requirements.txt
+ :caption: requirements.txt
+ :language: text
+ :linenos:
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat
new file mode 100644
index 0000000000..922152e96a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst
new file mode 100644
index 0000000000..264e6e42d1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/changelog.rst
@@ -0,0 +1,1230 @@
+Changelog
+=========
+
+.. currentmodule:: websockets
+
+.. _backwards-compatibility policy:
+
+Backwards-compatibility policy
+------------------------------
+
+websockets is intended for production use. Therefore, stability is a goal.
+
+websockets also aims at providing the best API for WebSocket in Python.
+
+While we value stability, we value progress more. When an improvement requires
+changing a public API, we make the change and document it in this changelog.
+
+When possible with reasonable effort, we preserve backwards-compatibility for
+five years after the release that introduced the change.
+
+When a release contains backwards-incompatible API changes, the major version
+is increased, else the minor version is increased. Patch versions are only for
+fixing regressions shortly after a release.
+
+Only documented APIs are public. Undocumented, private APIs may change without
+notice.
+
+12.0
+----
+
+*October 21, 2023*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 12.0 requires Python ≥ 3.8.
+ :class: tip
+
+ websockets 11.0 is the last version supporting Python 3.7.
+
+Improvements
+............
+
+* Made convenience imports from ``websockets`` compatible with static code
+ analysis tools such as auto-completion in an IDE or type checking with mypy_.
+
+ .. _mypy: https://github.com/python/mypy
+
+* Accepted a plain :class:`int` where an :class:`~http.HTTPStatus` is expected.
+
+* Added :class:`~frames.CloseCode`.
+
+11.0.3
+------
+
+*May 7, 2023*
+
+Bug fixes
+.........
+
+* Fixed the :mod:`threading` implementation of servers on Windows.
+
+11.0.2
+------
+
+*April 18, 2023*
+
+Bug fixes
+.........
+
+* Fixed a deadlock in the :mod:`threading` implementation when closing a
+ connection without reading all messages.
+
+11.0.1
+------
+
+*April 6, 2023*
+
+Bug fixes
+.........
+
+* Restored the C extension in the source distribution.
+
+11.0
+----
+
+*April 2, 2023*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: The Sans-I/O implementation was moved.
+ :class: caution
+
+ Aliases provide compatibility for all previously public APIs according to
+ the `backwards-compatibility policy`_.
+
+ * The ``connection`` module was renamed to ``protocol``.
+
+ * The ``connection.Connection``, ``server.ServerConnection``, and
+ ``client.ClientConnection`` classes were renamed to ``protocol.Protocol``,
+ ``server.ServerProtocol``, and ``client.ClientProtocol``.
+
+.. admonition:: Sans-I/O protocol constructors now use keyword-only arguments.
+ :class: caution
+
+ If you instantiate :class:`~server.ServerProtocol` or
+ :class:`~client.ClientProtocol` directly, make sure you are using keyword
+ arguments.
+
+.. admonition:: Closing a connection without an empty close frame is OK.
+ :class: note
+
+ Receiving an empty close frame now results in
+ :exc:`~exceptions.ConnectionClosedOK` instead of
+ :exc:`~exceptions.ConnectionClosedError`.
+
+ As a consequence, calling ``WebSocket.close()`` without arguments in a
+ browser isn't reported as an error anymore.
+
+.. admonition:: :func:`~server.serve` times out on the opening handshake after 10 seconds by default.
+ :class: note
+
+ You can adjust the timeout with the ``open_timeout`` parameter. Set it to
+ :obj:`None` to disable the timeout entirely.
+
+New features
+............
+
+.. admonition:: websockets 11.0 introduces a implementation on top of :mod:`threading`.
+ :class: important
+
+ It may be more convenient if you don't need to manage many connections and
+ you're more comfortable with :mod:`threading` than :mod:`asyncio`.
+
+ It is particularly suited to client applications that establish only one
+ connection. It may be used for servers handling few connections.
+
+ See :func:`~sync.client.connect` and :func:`~sync.server.serve` for details.
+
+* Added ``open_timeout`` to :func:`~server.serve`.
+
+* Made it possible to close a server without closing existing connections.
+
+* Added :attr:`~server.ServerProtocol.select_subprotocol` to customize
+ negotiation of subprotocols in the Sans-I/O layer.
+
+Improvements
+............
+
+* Added platform-independent wheels.
+
+* Improved error handling in :func:`~websockets.broadcast`.
+
+* Set ``server_hostname`` automatically on TLS connections when providing a
+ ``sock`` argument to :func:`~sync.client.connect`.
+
+10.4
+----
+
+*October 25, 2022*
+
+New features
+............
+
+* Validated compatibility with Python 3.11.
+
+* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` property to
+ protocols.
+
+* Changed :attr:`~legacy.protocol.WebSocketCommonProtocol.ping` to return the
+ latency of the connection.
+
+* Supported overriding or removing the ``User-Agent`` header in clients and the
+ ``Server`` header in servers.
+
+* Added deployment guides for more Platform as a Service providers.
+
+Improvements
+............
+
+* Improved FAQ.
+
+10.3
+----
+
+*April 17, 2022*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: The ``exception`` attribute of :class:`~http11.Request` and :class:`~http11.Response` is deprecated.
+ :class: note
+
+ Use the ``handshake_exc`` attribute of :class:`~server.ServerProtocol` and
+ :class:`~client.ClientProtocol` instead.
+
+ See :doc:`../howto/sansio` for details.
+
+Improvements
+............
+
+* Reduced noise in logs when :mod:`ssl` or :mod:`zlib` raise exceptions.
+
+10.2
+----
+
+*February 21, 2022*
+
+Improvements
+............
+
+* Made compression negotiation more lax for compatibility with Firefox.
+
+* Improved FAQ and quick start guide.
+
+Bug fixes
+.........
+
+* Fixed backwards-incompatibility in 10.1 for connection handlers created with
+ :func:`functools.partial`.
+
+* Avoided leaking open sockets when :func:`~client.connect` is canceled.
+
+10.1
+----
+
+*November 14, 2021*
+
+New features
+............
+
+* Added a tutorial.
+
+* Made the second parameter of connection handlers optional. It will be
+ deprecated in the next major release. The request path is available in
+ the :attr:`~legacy.protocol.WebSocketCommonProtocol.path` attribute of
+ the first argument.
+
+ If you implemented the connection handler of a server as::
+
+ async def handler(request, path):
+ ...
+
+ You should replace it by::
+
+ async def handler(request):
+ path = request.path # if handler() uses the path argument
+ ...
+
+* Added ``python -m websockets --version``.
+
+Improvements
+............
+
+* Added wheels for Python 3.10, PyPy 3.7, and for more platforms.
+
+* Reverted optimization of default compression settings for clients, mainly to
+ avoid triggering bugs in poorly implemented servers like `AWS API Gateway`_.
+
+ .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065
+
+* Mirrored the entire :class:`~asyncio.Server` API
+ in :class:`~server.WebSocketServer`.
+
+* Improved performance for large messages on ARM processors.
+
+* Documented how to auto-reload on code changes in development.
+
+Bug fixes
+.........
+
+* Avoided half-closing TCP connections that are already closed.
+
+10.0
+----
+
+*September 9, 2021*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 10.0 requires Python ≥ 3.7.
+ :class: tip
+
+ websockets 9.1 is the last version supporting Python 3.6.
+
+.. admonition:: The ``loop`` parameter is deprecated from all APIs.
+ :class: caution
+
+ This reflects a decision made in Python 3.8. See the release notes of
+ Python 3.10 for details.
+
+ The ``loop`` parameter is also removed
+ from :class:`~server.WebSocketServer`. This should be transparent.
+
+.. admonition:: :func:`~client.connect` times out after 10 seconds by default.
+ :class: note
+
+ You can adjust the timeout with the ``open_timeout`` parameter. Set it to
+ :obj:`None` to disable the timeout entirely.
+
+.. admonition:: The ``legacy_recv`` option is deprecated.
+ :class: note
+
+ See the release notes of websockets 3.0 for details.
+
+.. admonition:: The signature of :exc:`~exceptions.ConnectionClosed` changed.
+ :class: note
+
+ If you raise :exc:`~exceptions.ConnectionClosed` or a subclass, rather
+ than catch them when websockets raises them, you must change your code.
+
+.. admonition:: A ``msg`` parameter was added to :exc:`~exceptions.InvalidURI`.
+ :class: note
+
+ If you raise :exc:`~exceptions.InvalidURI`, rather than catch it when
+ websockets raises it, you must change your code.
+
+New features
+............
+
+.. admonition:: websockets 10.0 introduces a `Sans-I/O API
+ <https://sans-io.readthedocs.io/>`_ for easier integration
+ in third-party libraries.
+ :class: important
+
+ If you're integrating websockets in a library, rather than just using it,
+ look at the :doc:`Sans-I/O integration guide <../howto/sansio>`.
+
+* Added compatibility with Python 3.10.
+
+* Added :func:`~websockets.broadcast` to send a message to many clients.
+
+* Added support for reconnecting automatically by using
+ :func:`~client.connect` as an asynchronous iterator.
+
+* Added ``open_timeout`` to :func:`~client.connect`.
+
+* Documented how to integrate with `Django <https://www.djangoproject.com/>`_.
+
+* Documented how to deploy websockets in production, with several options.
+
+* Documented how to authenticate connections.
+
+* Documented how to broadcast messages to many connections.
+
+Improvements
+............
+
+* Improved logging. See the :doc:`logging guide <../topics/logging>`.
+
+* Optimized default compression settings to reduce memory usage.
+
+* Optimized processing of client-to-server messages when the C extension isn't
+ available.
+
+* Supported relative redirects in :func:`~client.connect`.
+
+* Handled TCP connection drops during the opening handshake.
+
+* Made it easier to customize authentication with
+ :meth:`~auth.BasicAuthWebSocketServerProtocol.check_credentials`.
+
+* Provided additional information in :exc:`~exceptions.ConnectionClosed`
+ exceptions.
+
+* Clarified several exceptions or log messages.
+
+* Restructured documentation.
+
+* Improved API documentation.
+
+* Extended FAQ.
+
+Bug fixes
+.........
+
+* Avoided a crash when receiving a ping while the connection is closing.
+
+9.1
+---
+
+*May 27, 2021*
+
+Security fix
+............
+
+.. admonition:: websockets 9.1 fixes a security issue introduced in 8.0.
+ :class: important
+
+ Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords
+ (`CVE-2021-33880`_).
+
+ .. _CVE-2021-33880: https://nvd.nist.gov/vuln/detail/CVE-2021-33880
+
+9.0.2
+-----
+
+*May 15, 2021*
+
+Bug fixes
+.........
+
+* Restored compatibility of ``python -m websockets`` with Python < 3.9.
+
+* Restored compatibility with mypy.
+
+9.0.1
+-----
+
+*May 2, 2021*
+
+Bug fixes
+.........
+
+* Fixed issues with the packaging of the 9.0 release.
+
+9.0
+---
+
+*May 1, 2021*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: Several modules are moved or deprecated.
+ :class: caution
+
+ Aliases provide compatibility for all previously public APIs according to
+ the `backwards-compatibility policy`_
+
+ * :class:`~datastructures.Headers` and
+ :exc:`~datastructures.MultipleValuesError` are moved from
+ ``websockets.http`` to :mod:`websockets.datastructures`. If you're using
+ them, you should adjust the import path.
+
+ * The ``client``, ``server``, ``protocol``, and ``auth`` modules were
+ moved from the ``websockets`` package to a ``websockets.legacy``
+ sub-package. Despite the name, they're still fully supported.
+
+ * The ``framing``, ``handshake``, ``headers``, ``http``, and ``uri``
+ modules in the ``websockets`` package are deprecated. These modules
+ provided low-level APIs for reuse by other projects, but they didn't
+ reach that goal. Keeping these APIs public makes it more difficult to
+ improve websockets.
+
+ These changes pave the path for a refactoring that should be a transparent
+ upgrade for most uses and facilitate integration by other projects.
+
+.. admonition:: Convenience imports from ``websockets`` are performed lazily.
+ :class: note
+
+ While Python supports this, tools relying on static code analysis don't.
+ This breaks auto-completion in an IDE or type checking with mypy_.
+
+ .. _mypy: https://github.com/python/mypy
+
+ If you depend on such tools, use the real import paths, which can be found
+ in the API documentation, for example::
+
+ from websockets.client import connect
+ from websockets.server import serve
+
+New features
+............
+
+* Added compatibility with Python 3.9.
+
+Improvements
+............
+
+* Added support for IRIs in addition to URIs.
+
+* Added close codes 1012, 1013, and 1014.
+
+* Raised an error when passing a :class:`dict` to
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send`.
+
+* Improved error reporting.
+
+Bug fixes
+.........
+
+* Fixed sending fragmented, compressed messages.
+
+* Fixed ``Host`` header sent when connecting to an IPv6 address.
+
+* Fixed creating a client or a server with an existing Unix socket.
+
+* Aligned maximum cookie size with popular web browsers.
+
+* Ensured cancellation always propagates, even on Python versions where
+ :exc:`~asyncio.CancelledError` inherits :exc:`Exception`.
+
+8.1
+---
+
+*November 1, 2019*
+
+New features
+............
+
+* Added compatibility with Python 3.8.
+
+8.0.2
+-----
+
+*July 31, 2019*
+
+Bug fixes
+.........
+
+* Restored the ability to pass a socket with the ``sock`` parameter of
+ :func:`~server.serve`.
+
+* Removed an incorrect assertion when a connection drops.
+
+8.0.1
+-----
+
+*July 21, 2019*
+
+Bug fixes
+.........
+
+* Restored the ability to import ``WebSocketProtocolError`` from
+ ``websockets``.
+
+8.0
+---
+
+*July 7, 2019*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 8.0 requires Python ≥ 3.6.
+ :class: tip
+
+ websockets 7.0 is the last version supporting Python 3.4 and 3.5.
+
+.. admonition:: ``process_request`` is now expected to be a coroutine.
+ :class: note
+
+ If you're passing a ``process_request`` argument to
+ :func:`~server.serve` or :class:`~server.WebSocketServerProtocol`, or if
+ you're overriding
+ :meth:`~server.WebSocketServerProtocol.process_request` in a subclass,
+ define it with ``async def`` instead of ``def``. Previously, both were supported.
+
+ For backwards compatibility, functions are still accepted, but mixing
+ functions and coroutines won't work in some inheritance scenarios.
+
+.. admonition:: ``max_queue`` must be :obj:`None` to disable the limit.
+ :class: note
+
+ If you were setting ``max_queue=0`` to make the queue of incoming messages
+ unbounded, change it to ``max_queue=None``.
+
+.. admonition:: The ``host``, ``port``, and ``secure`` attributes
+ of :class:`~legacy.protocol.WebSocketCommonProtocol` are deprecated.
+ :class: note
+
+ Use :attr:`~legacy.protocol.WebSocketCommonProtocol.local_address` in
+ servers and
+ :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` in clients
+ instead of ``host`` and ``port``.
+
+.. admonition:: ``WebSocketProtocolError`` is renamed
+ to :exc:`~exceptions.ProtocolError`.
+ :class: note
+
+ An alias provides backwards compatibility.
+
+.. admonition:: ``read_response()`` now returns the reason phrase.
+ :class: note
+
+ If you're using this low-level API, you must change your code.
+
+New features
+............
+
+* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP
+ Basic Auth on the server side.
+
+* :func:`~client.connect` handles redirects from the server during the
+ handshake.
+
+* :func:`~client.connect` supports overriding ``host`` and ``port``.
+
+* Added :func:`~client.unix_connect` for connecting to Unix sockets.
+
+* Added support for asynchronous generators
+ in :meth:`~legacy.protocol.WebSocketCommonProtocol.send`
+ to generate fragmented messages incrementally.
+
+* Enabled readline in the interactive client.
+
+* Added type hints (:pep:`484`).
+
+* Added a FAQ to the documentation.
+
+* Added documentation for extensions.
+
+* Documented how to optimize memory usage.
+
+Improvements
+............
+
+* :meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support bytes-like
+ types :class:`bytearray` and :class:`memoryview` in addition to
+ :class:`bytes`.
+
+* Added :exc:`~exceptions.ConnectionClosedOK` and
+ :exc:`~exceptions.ConnectionClosedError` subclasses of
+ :exc:`~exceptions.ConnectionClosed` to tell apart normal connection
+ termination from errors.
+
+* Changed :meth:`WebSocketServer.close()
+ <server.WebSocketServer.close>` to perform a proper closing handshake
+ instead of failing the connection.
+
+* Improved error messages when HTTP parsing fails.
+
+* Improved API documentation.
+
+Bug fixes
+.........
+
+* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed`
+ exceptions in keepalive ping task. If you were using ``ping_timeout=None``
+ as a workaround, you can remove it.
+
+* Avoided a crash when a ``extra_headers`` callable returns :obj:`None`.
+
+7.0
+---
+
+*November 1, 2018*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: Keepalive is enabled by default.
+ :class: important
+
+ websockets now sends Ping frames at regular intervals and closes the
+ connection if it doesn't receive a matching Pong frame.
+ See :class:`~legacy.protocol.WebSocketCommonProtocol` for details.
+
+.. admonition:: Termination of connections by :meth:`WebSocketServer.close()
+ <server.WebSocketServer.close>` changes.
+ :class: caution
+
+ Previously, connections handlers were canceled. Now, connections are
+ closed with close code 1001 (going away).
+
+ From the perspective of the connection handler, this is the same as if the
+ remote endpoint was disconnecting. This removes the need to prepare for
+ :exc:`~asyncio.CancelledError` in connection handlers.
+
+ You can restore the previous behavior by adding the following line at the
+ beginning of connection handlers::
+
+ def handler(websocket, path):
+ closed = asyncio.ensure_future(websocket.wait_closed())
+ closed.add_done_callback(lambda task: task.cancel())
+
+.. admonition:: Calling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`
+ concurrently raises a :exc:`RuntimeError`.
+ :class: note
+
+ Concurrent calls lead to non-deterministic behavior because there are no
+ guarantees about which coroutine will receive which message.
+
+.. admonition:: The ``timeout`` argument of :func:`~server.serve`
+ and :func:`~client.connect` is renamed to ``close_timeout`` .
+ :class: note
+
+ This prevents confusion with ``ping_timeout``.
+
+ For backwards compatibility, ``timeout`` is still supported.
+
+.. admonition:: The ``origins`` argument of :func:`~server.serve` changes.
+ :class: note
+
+ Include :obj:`None` in the list rather than ``''`` to allow requests that
+ don't contain an Origin header.
+
+.. admonition:: Pending pings aren't canceled when the connection is closed.
+ :class: note
+
+ A ping — as in ``ping = await websocket.ping()`` — for which no pong was
+ received yet used to be canceled when the connection is closed, so that
+ ``await ping`` raised :exc:`~asyncio.CancelledError`.
+
+ Now ``await ping`` raises :exc:`~exceptions.ConnectionClosed` like other
+ public APIs.
+
+New features
+............
+
+* Added ``process_request`` and ``select_subprotocol`` arguments to
+ :func:`~server.serve` and
+ :class:`~server.WebSocketServerProtocol` to facilitate customization of
+ :meth:`~server.WebSocketServerProtocol.process_request` and
+ :meth:`~server.WebSocketServerProtocol.select_subprotocol`.
+
+* Added support for sending fragmented messages.
+
+* Added the :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`
+ method to protocols.
+
+* Added an interactive client: ``python -m websockets <uri>``.
+
+Improvements
+............
+
+* Improved handling of multiple HTTP headers with the same name.
+
+* Improved error messages when a required HTTP header is missing.
+
+Bug fixes
+.........
+
+* Fixed a data loss bug in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`:
+ canceling it at the wrong time could result in messages being dropped.
+
+6.0
+---
+
+*July 16, 2018*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: The :class:`~datastructures.Headers` class is introduced and
+ several APIs are updated to use it.
+ :class: caution
+
+ * The ``request_headers`` argument
+ of :meth:`~server.WebSocketServerProtocol.process_request` is now
+ a :class:`~datastructures.Headers` instead of
+ an ``http.client.HTTPMessage``.
+
+ * The ``request_headers`` and ``response_headers`` attributes of
+ :class:`~legacy.protocol.WebSocketCommonProtocol` are now
+ :class:`~datastructures.Headers` instead of ``http.client.HTTPMessage``.
+
+ * The ``raw_request_headers`` and ``raw_response_headers`` attributes of
+ :class:`~legacy.protocol.WebSocketCommonProtocol` are removed. Use
+ :meth:`~datastructures.Headers.raw_items` instead.
+
+ * Functions defined in the ``handshake`` module now receive
+ :class:`~datastructures.Headers` in argument instead of ``get_header``
+ or ``set_header`` functions. This affects libraries that rely on
+ low-level APIs.
+
+ * Functions defined in the ``http`` module now return HTTP headers as
+ :class:`~datastructures.Headers` instead of lists of ``(name, value)``
+ pairs.
+
+ Since :class:`~datastructures.Headers` and ``http.client.HTTPMessage``
+ provide similar APIs, much of the code dealing with HTTP headers won't
+ require changes.
+
+New features
+............
+
+* Added compatibility with Python 3.7.
+
+5.0.1
+-----
+
+*May 24, 2018*
+
+Bug fixes
+.........
+
+* Fixed a regression in 5.0 that broke some invocations of
+ :func:`~server.serve` and :func:`~client.connect`.
+
+5.0
+---
+
+*May 22, 2018*
+
+Security fix
+............
+
+.. admonition:: websockets 5.0 fixes a security issue introduced in 4.0.
+ :class: important
+
+ Version 4.0 was vulnerable to denial of service by memory exhaustion
+ because it didn't enforce ``max_size`` when decompressing compressed
+ messages (`CVE-2018-1000518`_).
+
+ .. _CVE-2018-1000518: https://nvd.nist.gov/vuln/detail/CVE-2018-1000518
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: A ``user_info`` field is added to the return value of
+ ``parse_uri`` and ``WebSocketURI``.
+ :class: note
+
+ If you're unpacking ``WebSocketURI`` into four variables, adjust your code
+ to account for that fifth field.
+
+New features
+............
+
+* :func:`~client.connect` performs HTTP Basic Auth when the URI contains
+ credentials.
+
+* :func:`~server.unix_serve` can be used as an asynchronous context
+ manager on Python ≥ 3.5.1.
+
+* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.closed` property
+ to protocols.
+
+* Added new examples in the documentation.
+
+Improvements
+............
+
+* Iterating on incoming messages no longer raises an exception when the
+ connection terminates with close code 1001 (going away).
+
+* A plain HTTP request now receives a 426 Upgrade Required response and
+ doesn't log a stack trace.
+
+* If a :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` doesn't receive a
+ pong, it's canceled when the connection is closed.
+
+* Reported the cause of :exc:`~exceptions.ConnectionClosed` exceptions.
+
+* Stopped logging stack traces when the TCP connection dies prematurely.
+
+* Prevented writing to a closing TCP connection during unclean shutdowns.
+
+* Made connection termination more robust to network congestion.
+
+* Prevented processing of incoming frames after failing the connection.
+
+* Updated documentation with new features from Python 3.6.
+
+* Improved several sections of the documentation.
+
+Bug fixes
+.........
+
+* Prevented :exc:`TypeError` due to missing close code on connection close.
+
+* Fixed a race condition in the closing handshake that raised
+ :exc:`~exceptions.InvalidState`.
+
+4.0.1
+-----
+
+*November 2, 2017*
+
+Bug fixes
+.........
+
+* Fixed issues with the packaging of the 4.0 release.
+
+4.0
+---
+
+*November 2, 2017*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: websockets 4.0 requires Python ≥ 3.4.
+ :class: tip
+
+ websockets 3.4 is the last version supporting Python 3.3.
+
+.. admonition:: Compression is enabled by default.
+ :class: important
+
+ In August 2017, Firefox and Chrome support the permessage-deflate
+ extension, but not Safari and IE.
+
+ Compression should improve performance but it increases RAM and CPU use.
+
+ If you want to disable compression, add ``compression=None`` when calling
+ :func:`~server.serve` or :func:`~client.connect`.
+
+.. admonition:: The ``state_name`` attribute of protocols is deprecated.
+ :class: note
+
+ Use ``protocol.state.name`` instead of ``protocol.state_name``.
+
+New features
+............
+
+* :class:`~legacy.protocol.WebSocketCommonProtocol` instances can be used as
+ asynchronous iterators on Python ≥ 3.6. They yield incoming messages.
+
+* Added :func:`~server.unix_serve` for listening on Unix sockets.
+
+* Added the :attr:`~server.WebSocketServer.sockets` attribute to the
+ return value of :func:`~server.serve`.
+
+* Allowed ``extra_headers`` to override ``Server`` and ``User-Agent`` headers.
+
+Improvements
+............
+
+* Reorganized and extended documentation.
+
+* Rewrote connection termination to increase robustness in edge cases.
+
+* Reduced verbosity of "Failing the WebSocket connection" logs.
+
+Bug fixes
+.........
+
+* Aborted connections if they don't close within the configured ``timeout``.
+
+* Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on
+ a connection while it's being closed.
+
+3.4
+---
+
+*August 20, 2017*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: ``InvalidStatus`` is replaced
+ by :class:`~exceptions.InvalidStatusCode`.
+ :class: note
+
+ This exception is raised when :func:`~client.connect` receives an invalid
+ response status code from the server.
+
+New features
+............
+
+* :func:`~server.serve` can be used as an asynchronous context manager
+ on Python ≥ 3.5.1.
+
+* Added support for customizing handling of incoming connections with
+ :meth:`~server.WebSocketServerProtocol.process_request`.
+
+* Made read and write buffer sizes configurable.
+
+Improvements
+............
+
+* Renamed :func:`~server.serve` and :func:`~client.connect`'s
+ ``klass`` argument to ``create_protocol`` to reflect that it can also be a
+ callable. For backwards compatibility, ``klass`` is still supported.
+
+* Rewrote HTTP handling for simplicity and performance.
+
+* Added an optional C extension to speed up low-level operations.
+
+Bug fixes
+.........
+
+* Providing a ``sock`` argument to :func:`~client.connect` no longer
+ crashes.
+
+3.3
+---
+
+*March 29, 2017*
+
+New features
+............
+
+* Ensured compatibility with Python 3.6.
+
+Improvements
+............
+
+* Reduced noise in logs caused by connection resets.
+
+Bug fixes
+.........
+
+* Avoided crashing on concurrent writes on slow connections.
+
+3.2
+---
+
+*August 17, 2016*
+
+New features
+............
+
+* Added ``timeout``, ``max_size``, and ``max_queue`` arguments to
+ :func:`~client.connect` and :func:`~server.serve`.
+
+Improvements
+............
+
+* Made server shutdown more robust.
+
+3.1
+---
+
+*April 21, 2016*
+
+New features
+............
+
+* Added flow control for incoming data.
+
+Bug fixes
+.........
+
+* Avoided a warning when closing a connection before the opening handshake.
+
+3.0
+---
+
+*December 25, 2015*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` now
+ raises an exception when the connection is closed.
+ :class: caution
+
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` used to return
+ :obj:`None` when the connection was closed. This required checking the
+ return value of every call::
+
+ message = await websocket.recv()
+ if message is None:
+ return
+
+ Now it raises a :exc:`~exceptions.ConnectionClosed` exception instead.
+ This is more Pythonic. The previous code can be simplified to::
+
+ message = await websocket.recv()
+
+ When implementing a server, there's no strong reason to handle such
+ exceptions. Let them bubble up, terminate the handler coroutine, and the
+ server will simply ignore them.
+
+ In order to avoid stranding projects built upon an earlier version, the
+ previous behavior can be restored by passing ``legacy_recv=True`` to
+ :func:`~server.serve`, :func:`~client.connect`,
+ :class:`~server.WebSocketServerProtocol`, or
+ :class:`~client.WebSocketClientProtocol`.
+
+New features
+............
+
+* :func:`~client.connect` can be used as an asynchronous context
+ manager on Python ≥ 3.5.1.
+
+* :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support data passed as
+ :class:`str` in addition to :class:`bytes`.
+
+* Made ``state_name`` attribute on protocols a public API.
+
+Improvements
+............
+
+* Updated documentation with ``await`` and ``async`` syntax from Python 3.5.
+
+* Worked around an :mod:`asyncio` bug affecting connection termination under
+ load.
+
+* Improved documentation.
+
+2.7
+---
+
+*November 18, 2015*
+
+New features
+............
+
+* Added compatibility with Python 3.5.
+
+Improvements
+............
+
+* Refreshed documentation.
+
+2.6
+---
+
+*August 18, 2015*
+
+New features
+............
+
+* Added ``local_address`` and ``remote_address`` attributes on protocols.
+
+* Closed open connections with code 1001 when a server shuts down.
+
+Bug fixes
+.........
+
+* Avoided TCP fragmentation of small frames.
+
+2.5
+---
+
+*July 28, 2015*
+
+New features
+............
+
+* Provided access to handshake request and response HTTP headers.
+
+* Allowed customizing handshake request and response HTTP headers.
+
+* Added support for running on a non-default event loop.
+
+Improvements
+............
+
+* Improved documentation.
+
+* Sent a 403 status code instead of 400 when request Origin isn't allowed.
+
+* Clarified that the closing handshake can be initiated by the client.
+
+* Set the close code and reason more consistently.
+
+* Strengthened connection termination.
+
+Bug fixes
+.........
+
+* Canceling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` no longer
+ drops the next message.
+
+2.4
+---
+
+*January 31, 2015*
+
+New features
+............
+
+* Added support for subprotocols.
+
+* Added ``loop`` argument to :func:`~client.connect` and
+ :func:`~server.serve`.
+
+2.3
+---
+
+*November 3, 2014*
+
+Improvements
+............
+
+* Improved compliance of close codes.
+
+2.2
+---
+
+*July 28, 2014*
+
+New features
+............
+
+* Added support for limiting message size.
+
+2.1
+---
+
+*April 26, 2014*
+
+New features
+............
+
+* Added ``host``, ``port`` and ``secure`` attributes on protocols.
+
+* Added support for providing and checking Origin_.
+
+.. _Origin: https://www.rfc-editor.org/rfc/rfc6455.html#section-10.2
+
+2.0
+---
+
+*February 16, 2014*
+
+Backwards-incompatible changes
+..............................
+
+.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` are now coroutines.
+ :class: caution
+
+ They used to be functions.
+
+ Instead of::
+
+ websocket.send(message)
+
+ you must write::
+
+ await websocket.send(message)
+
+New features
+............
+
+* Added flow control for outgoing data.
+
+1.0
+---
+
+*November 14, 2013*
+
+New features
+............
+
+* Initial public release.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst
new file mode 100644
index 0000000000..020ed7ad85
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/contributing.rst
@@ -0,0 +1,66 @@
+Contributing
+============
+
+Thanks for taking the time to contribute to websockets!
+
+Code of Conduct
+---------------
+
+This project and everyone participating in it is governed by the `Code of
+Conduct`_. By participating, you are expected to uphold this code. Please
+report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com.
+
+.. _Code of Conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md
+
+*(If I'm the person with the inappropriate behavior, please accept my
+apologies. I know I can mess up. I can't expect you to tell me, but if you
+choose to do so, I'll do my best to handle criticism constructively.
+-- Aymeric)*
+
+Contributions
+-------------
+
+Bug reports, patches and suggestions are welcome!
+
+Please open an issue_ or send a `pull request`_.
+
+Feedback about the documentation is especially valuable, as the primary author
+feels more confident about writing code than writing docs :-)
+
+If you're wondering why things are done in a certain way, the :doc:`design
+document <../topics/design>` provides lots of details about the internals of
+websockets.
+
+.. _issue: https://github.com/python-websockets/websockets/issues/new
+.. _pull request: https://github.com/python-websockets/websockets/compare/
+
+Questions
+---------
+
+GitHub issues aren't a good medium for handling questions. There are better
+places to ask questions, for example Stack Overflow.
+
+If you want to ask a question anyway, please make sure that:
+
+- it's a question about websockets and not about :mod:`asyncio`;
+- it isn't answered in the documentation;
+- it wasn't asked already.
+
+A good question can be written as a suggestion to improve the documentation.
+
+Cryptocurrency users
+--------------------
+
+websockets appears to be quite popular for interfacing with Bitcoin or other
+cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint.
+
+I'm aware of efforts to build proof-of-stake models. I'll care once the total
+energy consumption of all cryptocurrencies drops to a non-bullshit level.
+
+You already negated all of humanity's efforts to develop renewable energy.
+Please stop heating the planet where my children will have to live.
+
+Since websockets is released under an open-source license, you can use it for
+any purpose you like. However, I won't spend any of my time to help you.
+
+I will summarily close issues related to Bitcoin or cryptocurrency in any way.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst
new file mode 100644
index 0000000000..459146345b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/index.rst
@@ -0,0 +1,12 @@
+About websockets
+================
+
+This is about websockets-the-project rather than websockets-the-software.
+
+.. toctree::
+ :titlesonly:
+
+ changelog
+ contributing
+ license
+ For enterprise <tidelift>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst
new file mode 100644
index 0000000000..0a3b8703d5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/license.rst
@@ -0,0 +1,4 @@
+License
+=======
+
+.. include:: ../../LICENSE
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst
new file mode 100644
index 0000000000..42100fade9
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/project/tidelift.rst
@@ -0,0 +1,112 @@
+websockets for enterprise
+=========================
+
+Available as part of the Tidelift Subscription
+----------------------------------------------
+
+.. image:: ../_static/tidelift.png
+ :height: 150px
+ :width: 150px
+ :align: left
+
+Tidelift is working with the maintainers of websockets and thousands of other
+open source projects to deliver commercial support and maintenance for the
+open source dependencies you use to build your applications. Save time, reduce
+risk, and improve code health, while paying the maintainers of the exact
+dependencies you use.
+
+.. raw:: html
+
+ <style type="text/css">
+ .tidelift-links {
+ display: flex;
+ justify-content: center;
+ }
+ @media only screen and (max-width: 600px) {
+ .tidelift-links {
+ flex-direction: column;
+ }
+ }
+ .tidelift-links a {
+ border: thin solid #f6914d;
+ border-radius: 0.25em;
+ font-family: Verdana, sans-serif;
+ font-size: 15px;
+ margin: 0.5em 2em;
+ padding: 0.5em 2em;
+ text-align: center;
+ text-decoration: none;
+ text-transform: uppercase;
+ }
+ .tidelift-links a.tidelift-links__learn-more {
+ background-color: white;
+ color: #f6914d;
+ }
+ .tidelift-links a.tidelift-links__request-a-demo {
+ background-color: #f6914d;
+ color: white;
+ }
+ </style>
+
+ <div class="tidelift-links">
+ <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a>
+ <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a>
+ </div>
+
+Enterprise-ready open source software—managed for you
+-----------------------------------------------------
+
+The Tidelift Subscription is a managed open source subscription for
+application dependencies covering millions of open source projects across
+JavaScript, Python, Java, PHP, Ruby, .NET, and more.
+
+Your subscription includes:
+
+* **Security updates**
+
+ * Tidelift’s security response team coordinates patches for new breaking
+ security vulnerabilities and alerts immediately through a private channel,
+ so your software supply chain is always secure.
+
+* **Licensing verification and indemnification**
+
+ * Tidelift verifies license information to enable easy policy enforcement
+ and adds intellectual property indemnification to cover creators and users
+ in case something goes wrong. You always have a 100% up-to-date bill of
+ materials for your dependencies to share with your legal team, customers,
+ or partners.
+
+* **Maintenance and code improvement**
+
+ * Tidelift ensures the software you rely on keeps working as long as you
+ need it to work. Your managed dependencies are actively maintained and we
+ recruit additional maintainers where required.
+
+* **Package selection and version guidance**
+
+ * We help you choose the best open source packages from the start—and then
+ guide you through updates to stay on the best releases as new issues
+ arise.
+
+* **Roadmap input**
+
+ * Take a seat at the table with the creators behind the software you use.
+ Tidelift’s participating maintainers earn more income as their software is
+ used by more subscribers, so they’re interested in knowing what you need.
+
+* **Tooling and cloud integration**
+
+ * Tidelift works with GitHub, GitLab, BitBucket, and more. We support every
+ cloud platform (and other deployment targets, too).
+
+The end result? All of the capabilities you expect from commercial-grade
+software, for the full breadth of open source you use. That means less time
+grappling with esoteric open source trivia, and more time building your own
+applications—and your business.
+
+.. raw:: html
+
+ <div class="tidelift-links">
+ <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a>
+ <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a>
+ </div>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst
new file mode 100644
index 0000000000..5086015b7b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst
@@ -0,0 +1,64 @@
+Client (:mod:`asyncio`)
+=======================
+
+.. automodule:: websockets.client
+
+Opening a connection
+--------------------
+
+.. autofunction:: connect(uri, *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+.. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+Using a connection
+------------------
+
+.. autoclass:: WebSocketClientProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16)
+
+ .. automethod:: recv
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ .. autoproperty:: open
+
+ .. autoproperty:: closed
+
+ .. autoattribute:: latency
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: path
+
+ .. autoattribute:: request_headers
+
+ .. autoattribute:: response_headers
+
+ .. autoattribute:: subprotocol
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst
new file mode 100644
index 0000000000..dc7a54ee1a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst
@@ -0,0 +1,54 @@
+:orphan:
+
+Both sides (:mod:`asyncio`)
+===========================
+
+.. automodule:: websockets.legacy.protocol
+
+.. autoclass:: WebSocketCommonProtocol(*, logger=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16)
+
+ .. automethod:: recv
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ .. autoproperty:: open
+
+ .. autoproperty:: closed
+
+ .. autoattribute:: latency
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: path
+
+ .. autoattribute:: request_headers
+
+ .. autoattribute:: response_headers
+
+ .. autoattribute:: subprotocol
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst
new file mode 100644
index 0000000000..1063179162
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst
@@ -0,0 +1,113 @@
+Server (:mod:`asyncio`)
+=======================
+
+.. automodule:: websockets.server
+
+Starting a server
+-----------------
+
+.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds)
+ :async:
+
+Stopping a server
+-----------------
+
+.. autoclass:: WebSocketServer
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: get_loop
+
+ .. automethod:: is_serving
+
+ .. automethod:: start_serving
+
+ .. automethod:: serve_forever
+
+ .. autoattribute:: sockets
+
+Using a connection
+------------------
+
+.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16)
+
+ .. automethod:: recv
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: wait_closed
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ You can customize the opening handshake in a subclass by overriding these methods:
+
+ .. automethod:: process_request
+
+ .. automethod:: select_subprotocol
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ .. autoproperty:: open
+
+ .. autoproperty:: closed
+
+ .. autoattribute:: latency
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: path
+
+ .. autoattribute:: request_headers
+
+ .. autoattribute:: response_headers
+
+ .. autoattribute:: subprotocol
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+
+Basic authentication
+--------------------
+
+.. automodule:: websockets.auth
+
+websockets supports HTTP Basic Authentication according to
+:rfc:`7235` and :rfc:`7617`.
+
+.. autofunction:: basic_auth_protocol_factory
+
+.. autoclass:: BasicAuthWebSocketServerProtocol
+
+ .. autoattribute:: realm
+
+ .. autoattribute:: username
+
+ .. automethod:: check_credentials
+
+Broadcast
+---------
+
+.. autofunction:: websockets.broadcast
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst
new file mode 100644
index 0000000000..ec02d42101
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/datastructures.rst
@@ -0,0 +1,66 @@
+Data structures
+===============
+
+WebSocket events
+----------------
+
+.. automodule:: websockets.frames
+
+ .. autoclass:: Frame
+
+ .. autoclass:: Opcode
+
+ .. autoattribute:: CONT
+ .. autoattribute:: TEXT
+ .. autoattribute:: BINARY
+ .. autoattribute:: CLOSE
+ .. autoattribute:: PING
+ .. autoattribute:: PONG
+
+ .. autoclass:: Close
+
+ .. autoclass:: CloseCode
+
+ .. autoattribute:: NORMAL_CLOSURE
+ .. autoattribute:: GOING_AWAY
+ .. autoattribute:: PROTOCOL_ERROR
+ .. autoattribute:: UNSUPPORTED_DATA
+ .. autoattribute:: NO_STATUS_RCVD
+ .. autoattribute:: ABNORMAL_CLOSURE
+ .. autoattribute:: INVALID_DATA
+ .. autoattribute:: POLICY_VIOLATION
+ .. autoattribute:: MESSAGE_TOO_BIG
+ .. autoattribute:: MANDATORY_EXTENSION
+ .. autoattribute:: INTERNAL_ERROR
+ .. autoattribute:: SERVICE_RESTART
+ .. autoattribute:: TRY_AGAIN_LATER
+ .. autoattribute:: BAD_GATEWAY
+ .. autoattribute:: TLS_HANDSHAKE
+
+HTTP events
+-----------
+
+.. automodule:: websockets.http11
+
+ .. autoclass:: Request
+
+ .. autoclass:: Response
+
+.. automodule:: websockets.datastructures
+
+ .. autoclass:: Headers
+
+ .. automethod:: get_all
+
+ .. automethod:: raw_items
+
+ .. autoexception:: MultipleValuesError
+
+URIs
+----
+
+.. automodule:: websockets.uri
+
+ .. autofunction:: parse_uri
+
+ .. autoclass:: WebSocketURI
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst
new file mode 100644
index 0000000000..907a650d20
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/exceptions.rst
@@ -0,0 +1,6 @@
+Exceptions
+==========
+
+.. automodule:: websockets.exceptions
+ :members:
+
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst
new file mode 100644
index 0000000000..a70f1b1e58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/extensions.rst
@@ -0,0 +1,60 @@
+Extensions
+==========
+
+.. currentmodule:: websockets.extensions
+
+The WebSocket protocol supports extensions_.
+
+At the time of writing, there's only one `registered extension`_ with a public
+specification, WebSocket Per-Message Deflate.
+
+.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9
+.. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name
+
+Per-Message Deflate
+-------------------
+
+.. automodule:: websockets.extensions.permessage_deflate
+
+ :mod:`websockets.extensions.permessage_deflate` implements WebSocket
+ Per-Message Deflate.
+
+ This extension is specified in :rfc:`7692`.
+
+ Refer to the :doc:`topic guide on compression <../topics/compression>` to
+ learn more about tuning compression settings.
+
+ .. autoclass:: ClientPerMessageDeflateFactory
+
+ .. autoclass:: ServerPerMessageDeflateFactory
+
+Base classes
+------------
+
+.. automodule:: websockets.extensions
+
+ :mod:`websockets.extensions` defines base classes for implementing
+ extensions.
+
+ Refer to the :doc:`how-to guide on extensions <../howto/extensions>` to
+ learn more about writing an extension.
+
+ .. autoclass:: Extension
+
+ .. autoattribute:: name
+
+ .. automethod:: decode
+
+ .. automethod:: encode
+
+ .. autoclass:: ClientExtensionFactory
+
+ .. autoattribute:: name
+
+ .. automethod:: get_request_params
+
+ .. automethod:: process_response_params
+
+ .. autoclass:: ServerExtensionFactory
+
+ .. automethod:: process_request_params
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst
new file mode 100644
index 0000000000..98b3c0ddaf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/features.rst
@@ -0,0 +1,187 @@
+Features
+========
+
+.. currentmodule:: websockets
+
+Feature support matrices summarize which implementations support which features.
+
+.. raw:: html
+
+ <style>
+ .support-matrix-table { width: 100%; }
+ .support-matrix-table th:first-child { text-align: left; }
+ .support-matrix-table th:not(:first-child) { text-align: center; width: 15%; }
+ .support-matrix-table td:not(:first-child) { text-align: center; }
+ </style>
+
+.. |aio| replace:: :mod:`asyncio`
+.. |sync| replace:: :mod:`threading`
+.. |sans| replace:: `Sans-I/O`_
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+Both sides
+----------
+
+.. table::
+ :class: support-matrix-table
+
+ +------------------------------------+--------+--------+--------+
+ | | |aio| | |sync| | |sans| |
+ +====================================+========+========+========+
+ | Perform the opening handshake | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Send a message | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Receive a message | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Iterate over received messages | ✅ | ✅ | ❌ |
+ +------------------------------------+--------+--------+--------+
+ | Send a fragmented message | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Receive a fragmented message after | ✅ | ✅ | ❌ |
+ | reassembly | | | |
+ +------------------------------------+--------+--------+--------+
+ | Receive a fragmented message frame | ❌ | ✅ | ✅ |
+ | by frame (`#479`_) | | | |
+ +------------------------------------+--------+--------+--------+
+ | Send a ping | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Respond to pings automatically | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Send a pong | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform the closing handshake | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Report close codes and reasons | ❌ | ✅ | ✅ |
+ | from both sides | | | |
+ +------------------------------------+--------+--------+--------+
+ | Compress messages (:rfc:`7692`) | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Tune memory usage for compression | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Negotiate extensions | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Implement custom extensions | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Negotiate a subprotocol | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Enforce security limits | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Log events | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Enforce opening timeout | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Enforce closing timeout | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Keepalive | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Heartbeat | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+
+.. _#479: https://github.com/python-websockets/websockets/issues/479
+
+Server
+------
+
+.. table::
+ :class: support-matrix-table
+
+ +------------------------------------+--------+--------+--------+
+ | | |aio| | |sync| | |sans| |
+ +====================================+========+========+========+
+ | Listen on a TCP socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Listen on a Unix socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Listen using a preexisting socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Encrypt connection with TLS | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Close server on context exit | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Close connection on handler exit | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Shut down server gracefully | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Check ``Origin`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Customize subprotocol selection | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Configure ``Server`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Alter opening handshake request | ❌ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Alter opening handshake response | ❌ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Basic Authentication | ✅ | ❌ | ❌ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ |
+ +------------------------------------+--------+--------+--------+
+ | Force HTTP response | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+
+Client
+------
+
+.. table::
+ :class: support-matrix-table
+
+ +------------------------------------+--------+--------+--------+
+ | | |aio| | |sync| | |sans| |
+ +====================================+========+========+========+
+ | Connect to a TCP socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect to a Unix socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect using a preexisting socket | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Encrypt connection with TLS | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Close connection on context exit | ✅ | ✅ | — |
+ +------------------------------------+--------+--------+--------+
+ | Reconnect automatically | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Configure ``Origin`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Configure ``User-Agent`` header | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Alter opening handshake request | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Connect to non-ASCII IRIs | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Basic Authentication | ✅ | ✅ | ✅ |
+ +------------------------------------+--------+--------+--------+
+ | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ |
+ | (`#784`_) | | | |
+ +------------------------------------+--------+--------+--------+
+ | Follow HTTP redirects | ✅ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect via a HTTP proxy (`#364`_) | ❌ | ❌ | — |
+ +------------------------------------+--------+--------+--------+
+ | Connect via a SOCKS5 proxy | ❌ | ❌ | — |
+ | (`#475`_) | | | |
+ +------------------------------------+--------+--------+--------+
+
+.. _#364: https://github.com/python-websockets/websockets/issues/364
+.. _#475: https://github.com/python-websockets/websockets/issues/475
+.. _#784: https://github.com/python-websockets/websockets/issues/784
+
+Known limitations
+-----------------
+
+There is no way to control compression of outgoing frames on a per-frame basis
+(`#538`_). If compression is enabled, all frames are compressed.
+
+.. _#538: https://github.com/python-websockets/websockets/issues/538
+
+The server doesn't check the Host header and respond with a HTTP 400 Bad Request
+if it is missing or invalid (`#1246`).
+
+.. _#1246: https://github.com/python-websockets/websockets/issues/1246
+
+The client API doesn't attempt to guarantee that there is no more than one
+connection to a given IP address in a CONNECTING state. This behavior is
+`mandated by RFC 6455`_. However, :func:`~client.connect()` isn't the right
+layer for enforcing this constraint. It's the caller's responsibility.
+
+.. _mandated by RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4.1
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst
new file mode 100644
index 0000000000..0b80f087a1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/index.rst
@@ -0,0 +1,90 @@
+API reference
+=============
+
+.. currentmodule:: websockets
+
+Features
+--------
+
+Check which implementations support which features and known limitations.
+
+.. toctree::
+ :titlesonly:
+
+ features
+
+
+:mod:`asyncio`
+--------------
+
+This is the default implementation. It's ideal for servers that handle many
+clients concurrently.
+
+.. toctree::
+ :titlesonly:
+
+ asyncio/server
+ asyncio/client
+
+:mod:`threading`
+----------------
+
+This alternative implementation can be a good choice for clients.
+
+.. toctree::
+ :titlesonly:
+
+ sync/server
+ sync/client
+
+`Sans-I/O`_
+-----------
+
+This layer is designed for integrating in third-party libraries, typically
+application servers.
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. toctree::
+ :titlesonly:
+
+ sansio/server
+ sansio/client
+
+Extensions
+----------
+
+The Per-Message Deflate extension is built in. You may also define custom
+extensions.
+
+.. toctree::
+ :titlesonly:
+
+ extensions
+
+Shared
+------
+
+These low-level APIs are shared by all implementations.
+
+.. toctree::
+ :titlesonly:
+
+ datastructures
+ exceptions
+ types
+
+API stability
+-------------
+
+Public APIs documented in this API reference are subject to the
+:ref:`backwards-compatibility policy <backwards-compatibility policy>`.
+
+Anything that isn't listed in the API reference is a private API. There's no
+guarantees of behavior or backwards-compatibility for private APIs.
+
+Convenience imports
+-------------------
+
+For convenience, many public APIs can be imported directly from the
+``websockets`` package.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst
new file mode 100644
index 0000000000..09bafc7455
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/client.rst
@@ -0,0 +1,58 @@
+Client (`Sans-I/O`_)
+====================
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. currentmodule:: websockets.client
+
+.. autoclass:: ClientProtocol(wsuri, origin=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None)
+
+ .. automethod:: receive_data
+
+ .. automethod:: receive_eof
+
+ .. automethod:: connect
+
+ .. automethod:: send_request
+
+ .. automethod:: send_continuation
+
+ .. automethod:: send_text
+
+ .. automethod:: send_binary
+
+ .. automethod:: send_close
+
+ .. automethod:: send_ping
+
+ .. automethod:: send_pong
+
+ .. automethod:: fail
+
+ .. automethod:: events_received
+
+ .. automethod:: data_to_send
+
+ .. automethod:: close_expected
+
+ WebSocket protocol objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: state
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: handshake_exc
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+ .. autoproperty:: close_exc
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst
new file mode 100644
index 0000000000..cd1ef3c63a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/common.rst
@@ -0,0 +1,64 @@
+:orphan:
+
+Both sides (`Sans-I/O`_)
+=========================
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. automodule:: websockets.protocol
+
+.. autoclass:: Protocol(side, state=State.OPEN, max_size=2 ** 20, logger=None)
+
+ .. automethod:: receive_data
+
+ .. automethod:: receive_eof
+
+ .. automethod:: send_continuation
+
+ .. automethod:: send_text
+
+ .. automethod:: send_binary
+
+ .. automethod:: send_close
+
+ .. automethod:: send_ping
+
+ .. automethod:: send_pong
+
+ .. automethod:: fail
+
+ .. automethod:: events_received
+
+ .. automethod:: data_to_send
+
+ .. automethod:: close_expected
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: state
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+ .. autoproperty:: close_exc
+
+.. autoclass:: Side
+
+ .. autoattribute:: SERVER
+
+ .. autoattribute:: CLIENT
+
+.. autoclass:: State
+
+ .. autoattribute:: CONNECTING
+
+ .. autoattribute:: OPEN
+
+ .. autoattribute:: CLOSING
+
+ .. autoattribute:: CLOSED
+
+.. autodata:: SEND_EOF
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst
new file mode 100644
index 0000000000..d70df6277a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sansio/server.rst
@@ -0,0 +1,62 @@
+Server (`Sans-I/O`_)
+====================
+
+.. _Sans-I/O: https://sans-io.readthedocs.io/
+
+.. currentmodule:: websockets.server
+
+.. autoclass:: ServerProtocol(origins=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None)
+
+ .. automethod:: receive_data
+
+ .. automethod:: receive_eof
+
+ .. automethod:: accept
+
+ .. automethod:: select_subprotocol
+
+ .. automethod:: reject
+
+ .. automethod:: send_response
+
+ .. automethod:: send_continuation
+
+ .. automethod:: send_text
+
+ .. automethod:: send_binary
+
+ .. automethod:: send_close
+
+ .. automethod:: send_ping
+
+ .. automethod:: send_pong
+
+ .. automethod:: fail
+
+ .. automethod:: events_received
+
+ .. automethod:: data_to_send
+
+ .. automethod:: close_expected
+
+ WebSocket protocol objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: state
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: handshake_exc
+
+ The following attributes are available after the closing handshake,
+ once the WebSocket connection is closed:
+
+ .. autoproperty:: close_code
+
+ .. autoproperty:: close_reason
+
+ .. autoproperty:: close_exc
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst
new file mode 100644
index 0000000000..6cccd6ec48
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/client.rst
@@ -0,0 +1,49 @@
+Client (:mod:`threading`)
+=========================
+
+.. automodule:: websockets.sync.client
+
+Opening a connection
+--------------------
+
+.. autofunction:: connect(uri, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+.. autofunction:: unix_connect(path, uri=None, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+Using a connection
+------------------
+
+.. autoclass:: ClientConnection
+
+ .. automethod:: __iter__
+
+ .. automethod:: recv
+
+ .. automethod:: recv_streaming
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: request
+
+ .. autoattribute:: response
+
+ .. autoproperty:: subprotocol
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst
new file mode 100644
index 0000000000..3dc6d4a509
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/common.rst
@@ -0,0 +1,41 @@
+:orphan:
+
+Both sides (:mod:`threading`)
+=============================
+
+.. automodule:: websockets.sync.connection
+
+.. autoclass:: Connection
+
+ .. automethod:: __iter__
+
+ .. automethod:: recv
+
+ .. automethod:: recv_streaming
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: request
+
+ .. autoattribute:: response
+
+ .. autoproperty:: subprotocol
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst
new file mode 100644
index 0000000000..35c112046a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/sync/server.rst
@@ -0,0 +1,60 @@
+Server (:mod:`threading`)
+=========================
+
+.. automodule:: websockets.sync.server
+
+Creating a server
+-----------------
+
+.. autofunction:: serve(handler, host=None, port=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+.. autofunction:: unix_serve(handler, path=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None)
+
+Running a server
+----------------
+
+.. autoclass:: WebSocketServer
+
+ .. automethod:: serve_forever
+
+ .. automethod:: shutdown
+
+ .. automethod:: fileno
+
+Using a connection
+------------------
+
+.. autoclass:: ServerConnection
+
+ .. automethod:: __iter__
+
+ .. automethod:: recv
+
+ .. automethod:: recv_streaming
+
+ .. automethod:: send
+
+ .. automethod:: close
+
+ .. automethod:: ping
+
+ .. automethod:: pong
+
+ WebSocket connection objects also provide these attributes:
+
+ .. autoattribute:: id
+
+ .. autoattribute:: logger
+
+ .. autoproperty:: local_address
+
+ .. autoproperty:: remote_address
+
+ The following attributes are available after the opening handshake,
+ once the WebSocket connection is open:
+
+ .. autoattribute:: request
+
+ .. autoattribute:: response
+
+ .. autoproperty:: subprotocol
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst
new file mode 100644
index 0000000000..9d3aa8310b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/reference/types.rst
@@ -0,0 +1,24 @@
+Types
+=====
+
+.. automodule:: websockets.typing
+
+ .. autodata:: Data
+
+ .. autodata:: LoggerLike
+
+ .. autodata:: StatusLike
+
+ .. autodata:: Origin
+
+ .. autodata:: Subprotocol
+
+ .. autodata:: ExtensionName
+
+ .. autodata:: ExtensionParameter
+
+.. autodata:: websockets.protocol.Event
+
+.. autodata:: websockets.datastructures.HeadersLike
+
+.. autodata:: websockets.datastructures.SupportsKeysAndGetItem
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt
new file mode 100644
index 0000000000..bcd1d71143
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/requirements.txt
@@ -0,0 +1,8 @@
+furo
+sphinx
+sphinx-autobuild
+sphinx-copybutton
+sphinx-inline-tabs
+sphinxcontrib-spelling
+sphinxcontrib-trio
+sphinxext-opengraph
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt
new file mode 100644
index 0000000000..dfa7065e79
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/spelling_wordlist.txt
@@ -0,0 +1,85 @@
+augustin
+auth
+autoscaler
+aymeric
+backend
+backoff
+backpressure
+balancer
+balancers
+bottlenecked
+bufferbloat
+bugfix
+buildpack
+bytestring
+bytestrings
+changelog
+coroutine
+coroutines
+cryptocurrencies
+cryptocurrency
+css
+ctrl
+deserialize
+django
+dev
+Dockerfile
+dyno
+formatter
+fractalideas
+gunicorn
+healthz
+html
+hypercorn
+iframe
+IPv
+istio
+iterable
+js
+keepalive
+KiB
+kubernetes
+lifecycle
+linkerd
+liveness
+lookups
+MiB
+mutex
+mypy
+nginx
+Paketo
+permessage
+pid
+procfile
+proxying
+py
+pythonic
+reconnection
+redis
+redistributions
+retransmit
+runtime
+scalable
+stateful
+subclasses
+subclassing
+submodule
+subpackages
+subprotocol
+subprotocols
+supervisord
+tidelift
+tls
+tox
+txt
+unregister
+uple
+uvicorn
+uvloop
+virtualenv
+WebSocket
+websocket
+websockets
+ws
+wsgi
+www
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst
new file mode 100644
index 0000000000..31bc8e6da8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.rst
@@ -0,0 +1,348 @@
+Authentication
+==============
+
+The WebSocket protocol was designed for creating web applications that need
+bidirectional communication between clients running in browsers and servers.
+
+In most practical use cases, WebSocket servers need to authenticate clients in
+order to route communications appropriately and securely.
+
+:rfc:`6455` stays elusive when it comes to authentication:
+
+ This protocol doesn't prescribe any particular way that servers can
+ authenticate clients during the WebSocket handshake. The WebSocket
+ server can use any client authentication mechanism available to a
+ generic HTTP server, such as cookies, HTTP authentication, or TLS
+ authentication.
+
+None of these three mechanisms works well in practice. Using cookies is
+cumbersome, HTTP authentication isn't supported by all mainstream browsers,
+and TLS authentication in a browser is an esoteric user experience.
+
+Fortunately, there are better alternatives! Let's discuss them.
+
+System design
+-------------
+
+Consider a setup where the WebSocket server is separate from the HTTP server.
+
+Most servers built with websockets to complement a web application adopt this
+design because websockets doesn't aim at supporting HTTP.
+
+The following diagram illustrates the authentication flow.
+
+.. image:: authentication.svg
+
+Assuming the current user is authenticated with the HTTP server (1), the
+application needs to obtain credentials from the HTTP server (2) in order to
+send them to the WebSocket server (3), who can check them against the database
+of user accounts (4).
+
+Usernames and passwords aren't a good choice of credentials here, if only
+because passwords aren't available in clear text in the database.
+
+Tokens linked to user accounts are a better choice. These tokens must be
+impossible to forge by an attacker. For additional security, they can be
+short-lived or even single-use.
+
+Sending credentials
+-------------------
+
+Assume the web application obtained authentication credentials, likely a
+token, from the HTTP server. There's four options for passing them to the
+WebSocket server.
+
+1. **Sending credentials as the first message in the WebSocket connection.**
+
+ This is fully reliable and the most secure mechanism in this discussion. It
+ has two minor downsides:
+
+ * Authentication is performed at the application layer. Ideally, it would
+ be managed at the protocol layer.
+
+ * Authentication is performed after the WebSocket handshake, making it
+ impossible to monitor authentication failures with HTTP response codes.
+
+2. **Adding credentials to the WebSocket URI in a query parameter.**
+
+ This is also fully reliable but less secure. Indeed, it has a major
+ downside:
+
+ * URIs end up in logs, which leaks credentials. Even if that risk could be
+ lowered with single-use tokens, it is usually considered unacceptable.
+
+ Authentication is still performed at the application layer but it can
+ happen before the WebSocket handshake, which improves separation of
+ concerns and enables responding to authentication failures with HTTP 401.
+
+3. **Setting a cookie on the domain of the WebSocket URI.**
+
+ Cookies are undoubtedly the most common and hardened mechanism for sending
+ credentials from a web application to a server. In an HTTP application,
+ credentials would be a session identifier or a serialized, signed session.
+
+ Unfortunately, when the WebSocket server runs on a different domain from
+ the web application, this idea bumps into the `Same-Origin Policy`_. For
+ security reasons, setting a cookie on a different origin is impossible.
+
+ The proper workaround consists in:
+
+ * creating a hidden iframe_ served from the domain of the WebSocket server
+ * sending the token to the iframe with postMessage_
+ * setting the cookie in the iframe
+
+ before opening the WebSocket connection.
+
+ Sharing a parent domain (e.g. example.com) between the HTTP server (e.g.
+ www.example.com) and the WebSocket server (e.g. ws.example.com) and setting
+ the cookie on that parent domain would work too.
+
+ However, the cookie would be shared with all subdomains of the parent
+ domain. For a cookie containing credentials, this is unacceptable.
+
+.. _Same-Origin Policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
+.. _iframe: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
+.. _postMessage: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage
+
+4. **Adding credentials to the WebSocket URI in user information.**
+
+ Letting the browser perform HTTP Basic Auth is a nice idea in theory.
+
+ In practice it doesn't work due to poor support in browsers.
+
+ As of May 2021:
+
+ * Chrome 90 behaves as expected.
+
+ * Firefox 88 caches credentials too aggressively.
+
+ When connecting again to the same server with new credentials, it reuses
+ the old credentials, which may be expired, resulting in an HTTP 401. Then
+ the next connection succeeds. Perhaps errors clear the cache.
+
+ When tokens are short-lived or single-use, this bug produces an
+ interesting effect: every other WebSocket connection fails.
+
+ * Safari 14 ignores credentials entirely.
+
+Two other options are off the table:
+
+1. **Setting a custom HTTP header**
+
+ This would be the most elegant mechanism, solving all issues with the options
+ discussed above.
+
+ Unfortunately, it doesn't work because the `WebSocket API`_ doesn't support
+ `setting custom headers`_.
+
+.. _WebSocket API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
+.. _setting custom headers: https://github.com/whatwg/html/issues/3062
+
+2. **Authenticating with a TLS certificate**
+
+ While this is suggested by the RFC, installing a TLS certificate is too far
+ from the mainstream experience of browser users. This could make sense in
+ high security contexts. I hope developers working on such projects don't
+ take security advice from the documentation of random open source projects.
+
+Let's experiment!
+-----------------
+
+The `experiments/authentication`_ directory demonstrates these techniques.
+
+Run the experiment in an environment where websockets is installed:
+
+.. _experiments/authentication: https://github.com/python-websockets/websockets/tree/main/experiments/authentication
+
+.. code-block:: console
+
+ $ python experiments/authentication/app.py
+ Running on http://localhost:8000/
+
+When you browse to the HTTP server at http://localhost:8000/ and you submit a
+username, the server creates a token and returns a testing web page.
+
+This page opens WebSocket connections to four WebSocket servers running on
+four different origins. It attempts to authenticate with the token in four
+different ways.
+
+First message
+.............
+
+As soon as the connection is open, the client sends a message containing the
+token:
+
+.. code-block:: javascript
+
+ const websocket = new WebSocket("ws://.../");
+ websocket.onopen = () => websocket.send(token);
+
+ // ...
+
+At the beginning of the connection handler, the server receives this message
+and authenticates the user. If authentication fails, the server closes the
+connection:
+
+.. code-block:: python
+
+ async def first_message_handler(websocket):
+ token = await websocket.recv()
+ user = get_user(token)
+ if user is None:
+ await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed")
+ return
+
+ ...
+
+Query parameter
+...............
+
+The client adds the token to the WebSocket URI in a query parameter before
+opening the connection:
+
+.. code-block:: javascript
+
+ const uri = `ws://.../?token=${token}`;
+ const websocket = new WebSocket(uri);
+
+ // ...
+
+The server intercepts the HTTP request, extracts the token and authenticates
+the user. If authentication fails, it returns an HTTP 401:
+
+.. code-block:: python
+
+ class QueryParamProtocol(websockets.WebSocketServerProtocol):
+ async def process_request(self, path, headers):
+ token = get_query_parameter(path, "token")
+ if token is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"
+
+ user = get_user(token)
+ if user is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"
+
+ self.user = user
+
+ async def query_param_handler(websocket):
+ user = websocket.user
+
+ ...
+
+Cookie
+......
+
+The client sets a cookie containing the token before opening the connection.
+
+The cookie must be set by an iframe loaded from the same origin as the
+WebSocket server. This requires passing the token to this iframe.
+
+.. code-block:: javascript
+
+ // in main window
+ iframe.contentWindow.postMessage(token, "http://...");
+
+ // in iframe
+ document.cookie = `token=${data}; SameSite=Strict`;
+
+ // in main window
+ const websocket = new WebSocket("ws://.../");
+
+ // ...
+
+This sequence must be synchronized between the main window and the iframe.
+This involves several events. Look at the full implementation for details.
+
+The server intercepts the HTTP request, extracts the token and authenticates
+the user. If authentication fails, it returns an HTTP 401:
+
+.. code-block:: python
+
+ class CookieProtocol(websockets.WebSocketServerProtocol):
+ async def process_request(self, path, headers):
+ # Serve iframe on non-WebSocket requests
+ ...
+
+ token = get_cookie(headers.get("Cookie", ""), "token")
+ if token is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n"
+
+ user = get_user(token)
+ if user is None:
+ return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n"
+
+ self.user = user
+
+ async def cookie_handler(websocket):
+ user = websocket.user
+
+ ...
+
+User information
+................
+
+The client adds the token to the WebSocket URI in user information before
+opening the connection:
+
+.. code-block:: javascript
+
+ const uri = `ws://token:${token}@.../`;
+ const websocket = new WebSocket(uri);
+
+ // ...
+
+Since HTTP Basic Auth is designed to accept a username and a password rather
+than a token, we send ``token`` as username and the token as password.
+
+The server intercepts the HTTP request, extracts the token and authenticates
+the user. If authentication fails, it returns an HTTP 401:
+
+.. code-block:: python
+
+ class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol):
+ async def check_credentials(self, username, password):
+ if username != "token":
+ return False
+
+ user = get_user(password)
+ if user is None:
+ return False
+
+ self.user = user
+ return True
+
+ async def user_info_handler(websocket):
+ user = websocket.user
+
+ ...
+
+Machine-to-machine authentication
+---------------------------------
+
+When the WebSocket client is a standalone program rather than a script running
+in a browser, there are far fewer constraints. HTTP Authentication is the best
+solution in this scenario.
+
+To authenticate a websockets client with HTTP Basic Authentication
+(:rfc:`7617`), include the credentials in the URI:
+
+.. code-block:: python
+
+ async with websockets.connect(
+ f"wss://{username}:{password}@example.com",
+ ) as websocket:
+ ...
+
+(You must :func:`~urllib.parse.quote` ``username`` and ``password`` if they
+contain unsafe characters.)
+
+To authenticate a websockets client with HTTP Bearer Authentication
+(:rfc:`6750`), add a suitable ``Authorization`` header:
+
+.. code-block:: python
+
+ async with websockets.connect(
+ "wss://example.com",
+ extra_headers={"Authorization": f"Bearer {token}"}
+ ) as websocket:
+ ...
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg
new file mode 100644
index 0000000000..79b5fb8d56
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/authentication.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-514 74 644 380" width="644px" height="380px"><defs><style>@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix");
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix");
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg");
+}
+@font-face{
+ font-weight: 500;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix");
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 500;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix");
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg");
+}
+@font-face{
+ font-weight: 700;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix");
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 700;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix");
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg");
+}
+
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff');
+ font-style: italic;
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff');
+ font-style: italic;
+ font-weight: 700;
+}
+* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-504" y="228" width="192" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="228" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-504" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-492" font-family="DIN Next, sans-serif" height="48"><tspan x="-408" y="258" text-anchor="middle" style="white-space:pre;">HTTP</tspan><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-72" y="228" width="192" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="228" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-72" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-60" font-family="DIN Next, sans-serif" height="48"><tspan x="24" y="258" text-anchor="middle" style="white-space:pre;">WebSocket</tspan><tspan x="24" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-288" y="84" width="192" height="72" rx="3" ry="3" fill="#d9dde0"/><rect y="84" rx="3" stroke="#4B5C6B" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-288" ry="3" height="72"/><text y="96" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="48"><tspan x="-192" y="114" text-anchor="middle" style="white-space:pre;">web app</tspan><tspan x="-192" y="138" text-anchor="middle" style="white-space:pre;">in browser</tspan></text></g></g><g><g><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="#e2cdf2"/><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="none" stroke="#730FC3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="396" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="24"><tspan x="-192" y="414" text-anchor="middle" style="white-space:pre;">user accounts</tspan></text></g></g><g><g><path d="M-400.46606,302.69069l37.26606,13.30931M-284.8,344l33.4991,11.96396" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-252.34924,349.70583 -247.53394,357.30931 -256.07648,360.14213" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-404" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="336" text-anchor="middle" style="white-space:pre;">(1) authenticate user</tspan></text></g></g><g><g><path d="M-247.35316,159.15135l-43.98017,18.84865M-356.66667,206l-40.30359,17.27297" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-391.94549,227.14787 -400.64684,224.84865 -396.31086,216.96199" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="164" xml:space="preserve" x="-406" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="198" text-anchor="middle" style="white-space:pre;">(2) obtain credentials</tspan></text></g></g><g><g><path d="M-136.64684,159.15135l43.98017,18.84865M-27.33333,206l40.30359,17.27297" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="12.31086,216.96199 16.64684,224.84865 7.94549,227.14787" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="153" xml:space="preserve" x="-136.5" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="198" text-anchor="middle" style="white-space:pre;">(3) send credentials</tspan></text></g></g><g><g><path d="M16.46606,302.69069l-37.26606,13.30931M-99.2,344l-33.4991,11.96396" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-127.92352,360.14213 -136.46606,357.30931 -131.65076,349.70583" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-140" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="336" text-anchor="middle" style="white-space:pre;">(4) authenticate user</tspan></text></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst
new file mode 100644
index 0000000000..1acb372d4f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/broadcast.rst
@@ -0,0 +1,348 @@
+Broadcasting messages
+=====================
+
+.. currentmodule:: websockets
+
+
+.. admonition:: If you just want to send a message to all connected clients,
+ use :func:`broadcast`.
+ :class: tip
+
+ If you want to learn about its design in depth, continue reading this
+ document.
+
+WebSocket servers often send the same message to all connected clients or to a
+subset of clients for which the message is relevant.
+
+Let's explore options for broadcasting a message, explain the design
+of :func:`broadcast`, and discuss alternatives.
+
+For each option, we'll provide a connection handler called ``handler()`` and a
+function or coroutine called ``broadcast()`` that sends a message to all
+connected clients.
+
+Integrating them is left as an exercise for the reader. You could start with::
+
+ import asyncio
+ import websockets
+
+ async def handler(websocket):
+ ...
+
+ async def broadcast(message):
+ ...
+
+ async def broadcast_messages():
+ while True:
+ await asyncio.sleep(1)
+ message = ... # your application logic goes here
+ await broadcast(message)
+
+ async def main():
+ async with websockets.serve(handler, "localhost", 8765):
+ await broadcast_messages() # runs forever
+
+ if __name__ == "__main__":
+ asyncio.run(main())
+
+``broadcast_messages()`` must yield control to the event loop between each
+message, or else it will never let the server run. That's why it includes
+``await asyncio.sleep(1)``.
+
+A complete example is available in the `experiments/broadcast`_ directory.
+
+.. _experiments/broadcast: https://github.com/python-websockets/websockets/tree/main/experiments/broadcast
+
+The naive way
+-------------
+
+The most obvious way to send a message to all connected clients consists in
+keeping track of them and sending the message to each of them.
+
+Here's a connection handler that registers clients in a global variable::
+
+ CLIENTS = set()
+
+ async def handler(websocket):
+ CLIENTS.add(websocket)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CLIENTS.remove(websocket)
+
+This implementation assumes that the client will never send any messages. If
+you'd rather not make this assumption, you can change::
+
+ await websocket.wait_closed()
+
+to::
+
+ async for _ in websocket:
+ pass
+
+Here's a coroutine that broadcasts a message to all clients::
+
+ async def broadcast(message):
+ for websocket in CLIENTS.copy():
+ try:
+ await websocket.send(message)
+ except websockets.ConnectionClosed:
+ pass
+
+There are two tricks in this version of ``broadcast()``.
+
+First, it makes a copy of ``CLIENTS`` before iterating it. Else, if a client
+connects or disconnects while ``broadcast()`` is running, the loop would fail
+with::
+
+ RuntimeError: Set changed size during iteration
+
+Second, it ignores :exc:`~exceptions.ConnectionClosed` exceptions because a
+client could disconnect between the moment ``broadcast()`` makes a copy of
+``CLIENTS`` and the moment it sends a message to this client. This is fine: a
+client that disconnected doesn't belongs to "all connected clients" anymore.
+
+The naive way can be very fast. Indeed, if all connections have enough free
+space in their write buffers, ``await websocket.send(message)`` writes the
+message and returns immediately, as it doesn't need to wait for the buffer to
+drain. In this case, ``broadcast()`` doesn't yield control to the event loop,
+which minimizes overhead.
+
+The naive way can also fail badly. If the write buffer of a connection reaches
+``write_limit``, ``broadcast()`` waits for the buffer to drain before sending
+the message to other clients. This can cause a massive drop in performance.
+
+As a consequence, this pattern works only when write buffers never fill up,
+which is usually outside of the control of the server.
+
+If you know for sure that you will never write more than ``write_limit`` bytes
+within ``ping_interval + ping_timeout``, then websockets will terminate slow
+connections before the write buffer has time to fill up.
+
+Don't set extreme ``write_limit``, ``ping_interval``, and ``ping_timeout``
+values to ensure that this condition holds. Set reasonable values and use the
+built-in :func:`broadcast` function instead.
+
+The concurrent way
+------------------
+
+The naive way didn't work well because it serialized writes, while the whole
+point of asynchronous I/O is to perform I/O concurrently.
+
+Let's modify ``broadcast()`` to send messages concurrently::
+
+ async def send(websocket, message):
+ try:
+ await websocket.send(message)
+ except websockets.ConnectionClosed:
+ pass
+
+ def broadcast(message):
+ for websocket in CLIENTS:
+ asyncio.create_task(send(websocket, message))
+
+We move the error handling logic in a new coroutine and we schedule
+a :class:`~asyncio.Task` to run it instead of executing it immediately.
+
+Since ``broadcast()`` no longer awaits coroutines, we can make it a function
+rather than a coroutine and do away with the copy of ``CLIENTS``.
+
+This version of ``broadcast()`` makes clients independent from one another: a
+slow client won't block others. As a side effect, it makes messages
+independent from one another.
+
+If you broadcast several messages, there is no strong guarantee that they will
+be sent in the expected order. Fortunately, the event loop runs tasks in the
+order in which they are created, so the order is correct in practice.
+
+Technically, this is an implementation detail of the event loop. However, it
+seems unlikely for an event loop to run tasks in an order other than FIFO.
+
+If you wanted to enforce the order without relying this implementation detail,
+you could be tempted to wait until all clients have received the message::
+
+ async def broadcast(message):
+ if CLIENTS: # asyncio.wait doesn't accept an empty list
+ await asyncio.wait([
+ asyncio.create_task(send(websocket, message))
+ for websocket in CLIENTS
+ ])
+
+However, this doesn't really work in practice. Quite often, it will block
+until the slowest client times out.
+
+Backpressure meets broadcast
+----------------------------
+
+At this point, it becomes apparent that backpressure, usually a good practice,
+doesn't work well when broadcasting a message to thousands of clients.
+
+When you're sending messages to a single client, you don't want to send them
+faster than the network can transfer them and the client accept them. This is
+why :meth:`~server.WebSocketServerProtocol.send` checks if the write buffer
+is full and, if it is, waits until it drain, giving the network and the
+client time to catch up. This provides backpressure.
+
+Without backpressure, you could pile up data in the write buffer until the
+server process runs out of memory and the operating system kills it.
+
+The :meth:`~server.WebSocketServerProtocol.send` API is designed to enforce
+backpressure by default. This helps users of websockets write robust programs
+even if they never heard about backpressure.
+
+For comparison, :class:`asyncio.StreamWriter` requires users to understand
+backpressure and to await :meth:`~asyncio.StreamWriter.drain` explicitly
+after each :meth:`~asyncio.StreamWriter.write`.
+
+When broadcasting messages, backpressure consists in slowing down all clients
+in an attempt to let the slowest client catch up. With thousands of clients,
+the slowest one is probably timing out and isn't going to receive the message
+anyway. So it doesn't make sense to synchronize with the slowest client.
+
+How do we avoid running out of memory when slow clients can't keep up with the
+broadcast rate, then? The most straightforward option is to disconnect them.
+
+If a client gets too far behind, eventually it reaches the limit defined by
+``ping_timeout`` and websockets terminates the connection. You can read the
+discussion of :doc:`keepalive and timeouts <./timeouts>` for details.
+
+How :func:`broadcast` works
+---------------------------
+
+The built-in :func:`broadcast` function is similar to the naive way. The main
+difference is that it doesn't apply backpressure.
+
+This provides the best performance by avoiding the overhead of scheduling and
+running one task per client.
+
+Also, when sending text messages, encoding to UTF-8 happens only once rather
+than once per client, providing a small performance gain.
+
+Per-client queues
+-----------------
+
+At this point, we deal with slow clients rather brutally: we disconnect then.
+
+Can we do better? For example, we could decide to skip or to batch messages,
+depending on how far behind a client is.
+
+To implement this logic, we can create a queue of messages for each client and
+run a task that gets messages from the queue and sends them to the client::
+
+ import asyncio
+
+ CLIENTS = set()
+
+ async def relay(queue, websocket):
+ while True:
+ # Implement custom logic based on queue.qsize() and
+ # websocket.transport.get_write_buffer_size() here.
+ message = await queue.get()
+ await websocket.send(message)
+
+ async def handler(websocket):
+ queue = asyncio.Queue()
+ relay_task = asyncio.create_task(relay(queue, websocket))
+ CLIENTS.add(queue)
+ try:
+ await websocket.wait_closed()
+ finally:
+ CLIENTS.remove(queue)
+ relay_task.cancel()
+
+Then we can broadcast a message by pushing it to all queues::
+
+ def broadcast(message):
+ for queue in CLIENTS:
+ queue.put_nowait(message)
+
+The queues provide an additional buffer between the ``broadcast()`` function
+and clients. This makes it easier to support slow clients without excessive
+memory usage because queued messages aren't duplicated to write buffers
+until ``relay()`` processes them.
+
+Publish–subscribe
+-----------------
+
+Can we avoid centralizing the list of connected clients in a global variable?
+
+If each client subscribes to a stream a messages, then broadcasting becomes as
+simple as publishing a message to the stream.
+
+Here's a message stream that supports multiple consumers::
+
+ class PubSub:
+ def __init__(self):
+ self.waiter = asyncio.Future()
+
+ def publish(self, value):
+ waiter, self.waiter = self.waiter, asyncio.Future()
+ waiter.set_result((value, self.waiter))
+
+ async def subscribe(self):
+ waiter = self.waiter
+ while True:
+ value, waiter = await waiter
+ yield value
+
+ __aiter__ = subscribe
+
+ PUBSUB = PubSub()
+
+The stream is implemented as a linked list of futures. It isn't necessary to
+synchronize consumers. They can read the stream at their own pace,
+independently from one another. Once all consumers read a message, there are
+no references left, therefore the garbage collector deletes it.
+
+The connection handler subscribes to the stream and sends messages::
+
+ async def handler(websocket):
+ async for message in PUBSUB:
+ await websocket.send(message)
+
+The broadcast function publishes to the stream::
+
+ def broadcast(message):
+ PUBSUB.publish(message)
+
+Like per-client queues, this version supports slow clients with limited memory
+usage. Unlike per-client queues, it makes it difficult to tell how far behind
+a client is. The ``PubSub`` class could be extended or refactored to provide
+this information.
+
+The ``for`` loop is gone from this version of the ``broadcast()`` function.
+However, there's still a ``for`` loop iterating on all clients hidden deep
+inside :mod:`asyncio`. When ``publish()`` sets the result of the ``waiter``
+future, :mod:`asyncio` loops on callbacks registered with this future and
+schedules them. This is how connection handlers receive the next value from
+the asynchronous iterator returned by ``subscribe()``.
+
+Performance considerations
+--------------------------
+
+The built-in :func:`broadcast` function sends all messages without yielding
+control to the event loop. So does the naive way when the network and clients
+are fast and reliable.
+
+For each client, a WebSocket frame is prepared and sent to the network. This
+is the minimum amount of work required to broadcast a message.
+
+It would be tempting to prepare a frame and reuse it for all connections.
+However, this isn't possible in general for two reasons:
+
+* Clients can negotiate different extensions. You would have to enforce the
+ same extensions with the same parameters. For example, you would have to
+ select some compression settings and reject clients that cannot support
+ these settings.
+
+* Extensions can be stateful, producing different encodings of the same
+ message depending on previous messages. For example, you would have to
+ disable context takeover to make compression stateless, resulting in poor
+ compression rates.
+
+All other patterns discussed above yield control to the event loop once per
+client because messages are sent by different tasks. This makes them slower
+than the built-in :func:`broadcast` function.
+
+There is no major difference between the performance of per-client queues and
+publish–subscribe.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst
new file mode 100644
index 0000000000..eaf99070db
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/compression.rst
@@ -0,0 +1,222 @@
+Compression
+===========
+
+.. currentmodule:: websockets.extensions.permessage_deflate
+
+Most WebSocket servers exchange JSON messages because they're convenient to
+parse and serialize in a browser. These messages contain text data and tend to
+be repetitive.
+
+This makes the stream of messages highly compressible. Enabling compression
+can reduce network traffic by more than 80%.
+
+There's a standard for compressing messages. :rfc:`7692` defines WebSocket
+Per-Message Deflate, a compression extension based on the Deflate_ algorithm.
+
+.. _Deflate: https://en.wikipedia.org/wiki/Deflate
+
+Configuring compression
+-----------------------
+
+:func:`~websockets.client.connect` and :func:`~websockets.server.serve` enable
+compression by default because the reduction in network bandwidth is usually
+worth the additional memory and CPU cost.
+
+If you want to disable compression, set ``compression=None``::
+
+ import websockets
+
+ websockets.connect(..., compression=None)
+
+ websockets.serve(..., compression=None)
+
+If you want to customize compression settings, you can enable the Per-Message
+Deflate extension explicitly with :class:`ClientPerMessageDeflateFactory` or
+:class:`ServerPerMessageDeflateFactory`::
+
+ import websockets
+ from websockets.extensions import permessage_deflate
+
+ websockets.connect(
+ ...,
+ extensions=[
+ permessage_deflate.ClientPerMessageDeflateFactory(
+ server_max_window_bits=11,
+ client_max_window_bits=11,
+ compress_settings={"memLevel": 4},
+ ),
+ ],
+ )
+
+ websockets.serve(
+ ...,
+ extensions=[
+ permessage_deflate.ServerPerMessageDeflateFactory(
+ server_max_window_bits=11,
+ client_max_window_bits=11,
+ compress_settings={"memLevel": 4},
+ ),
+ ],
+ )
+
+The Window Bits and Memory Level values in these examples reduce memory usage
+at the expense of compression rate.
+
+Compression settings
+--------------------
+
+When a client and a server enable the Per-Message Deflate extension, they
+negotiate two parameters to guarantee compatibility between compression and
+decompression. These parameters affect the trade-off between compression rate
+and memory usage for both sides.
+
+* **Context Takeover** means that the compression context is retained between
+ messages. In other words, compression is applied to the stream of messages
+ rather than to each message individually.
+
+ Context takeover should remain enabled to get good performance on
+ applications that send a stream of messages with similar structure,
+ that is, most applications.
+
+ This requires retaining the compression context and state between messages,
+ which increases the memory footprint of a connection.
+
+* **Window Bits** controls the size of the compression context. It must be
+ an integer between 9 (lowest memory usage) and 15 (best compression).
+ Setting it to 8 is possible but rejected by some versions of zlib.
+
+ On the server side, websockets defaults to 12. Specifically, the compression
+ window size (server to client) is always 12 while the decompression window
+ (client to server) size may be 12 or 15 depending on whether the client
+ supports configuring it.
+
+ On the client side, websockets lets the server pick a suitable value, which
+ has the same effect as defaulting to 15.
+
+:mod:`zlib` offers additional parameters for tuning compression. They control
+the trade-off between compression rate, memory usage, and CPU usage only for
+compressing. They're transparent for decompressing. Unless mentioned
+otherwise, websockets inherits defaults of :func:`~zlib.compressobj`.
+
+* **Memory Level** controls the size of the compression state. It must be an
+ integer between 1 (lowest memory usage) and 9 (best compression).
+
+ websockets defaults to 5. This is lower than zlib's default of 8. Not only
+ does a lower memory level reduce memory usage, but it can also increase
+ speed thanks to memory locality.
+
+* **Compression Level** controls the effort to optimize compression. It must
+ be an integer between 1 (lowest CPU usage) and 9 (best compression).
+
+* **Strategy** selects the compression strategy. The best choice depends on
+ the type of data being compressed.
+
+
+Tuning compression
+------------------
+
+For servers
+...........
+
+By default, websockets enables compression with conservative settings that
+optimize memory usage at the cost of a slightly worse compression rate:
+Window Bits = 12 and Memory Level = 5. This strikes a good balance for small
+messages that are typical of WebSocket servers.
+
+Here's how various compression settings affect memory usage of a single
+connection on a 64-bit system, as well a benchmark of compressed size and
+compression time for a corpus of small JSON documents.
+
+=========== ============ ============ ================ ================
+Window Bits Memory Level Memory usage Size vs. default Time vs. default
+=========== ============ ============ ================ ================
+15 8 322 KiB -4.0% +15%
+14 7 178 KiB -2.6% +10%
+13 6 106 KiB -1.4% +5%
+**12** **5** **70 KiB** **=** **=**
+11 4 52 KiB +3.7% -5%
+10 3 43 KiB +90% +50%
+9 2 39 KiB +160% +100%
+— — 19 KiB +452% —
+=========== ============ ============ ================ ================
+
+Window Bits and Memory Level don't have to move in lockstep. However, other
+combinations don't yield significantly better results than those shown above.
+
+Compressed size and compression time depend heavily on the kind of messages
+exchanged by the application so this example may not apply to your use case.
+
+You can adapt `compression/benchmark.py`_ by creating a list of typical
+messages and passing it to the ``_run`` function.
+
+Window Bits = 11 and Memory Level = 4 looks like the sweet spot in this table.
+
+websockets defaults to Window Bits = 12 and Memory Level = 5 to stay away from
+Window Bits = 10 or Memory Level = 3 where performance craters, raising doubts
+on what could happen at Window Bits = 11 and Memory Level = 4 on a different
+corpus.
+
+Defaults must be safe for all applications, hence a more conservative choice.
+
+.. _compression/benchmark.py: https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py
+
+The benchmark focuses on compression because it's more expensive than
+decompression. Indeed, leaving aside small allocations, theoretical memory
+usage is:
+
+* ``(1 << (windowBits + 2)) + (1 << (memLevel + 9))`` for compression;
+* ``1 << windowBits`` for decompression.
+
+CPU usage is also higher for compression than decompression.
+
+While it's always possible for a server to use a smaller window size for
+compressing outgoing messages, using a smaller window size for decompressing
+incoming messages requires collaboration from clients.
+
+When a client doesn't support configuring the size of its compression window,
+websockets enables compression with the largest possible decompression window.
+In most use cases, this is more efficient than disabling compression both ways.
+
+If you are very sensitive to memory usage, you can reverse this behavior by
+setting the ``require_client_max_window_bits`` parameter of
+:class:`ServerPerMessageDeflateFactory` to ``True``.
+
+For clients
+...........
+
+By default, websockets enables compression with Memory Level = 5 but leaves
+the Window Bits setting up to the server.
+
+There's two good reasons and one bad reason for not optimizing the client side
+like the server side:
+
+1. If the maintainers of a server configured some optimized settings, we don't
+ want to override them with more restrictive settings.
+
+2. Optimizing memory usage doesn't matter very much for clients because it's
+ uncommon to open thousands of client connections in a program.
+
+3. On a more pragmatic note, some servers misbehave badly when a client
+ configures compression settings. `AWS API Gateway`_ is the worst offender.
+
+ .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065
+
+ Unfortunately, even though websockets is right and AWS is wrong, many users
+ jump to the conclusion that websockets doesn't work.
+
+ Until the ecosystem levels up, interoperability with buggy servers seems
+ more valuable than optimizing memory usage.
+
+
+Further reading
+---------------
+
+This `blog post by Ilya Grigorik`_ provides more details about how compression
+settings affect memory usage and how to optimize them.
+
+.. _blog post by Ilya Grigorik: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/
+
+This `experiment by Peter Thorson`_ recommends Window Bits = 11 and Memory
+Level = 4 for optimizing memory usage.
+
+.. _experiment by Peter Thorson: https://mailarchive.ietf.org/arch/msg/hybi/F9t4uPufVEy8KBLuL36cZjCmM_Y/
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg
new file mode 100644
index 0000000000..22a198ed83
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/data-flow.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-382 -214 500 464" width="500px" height="464px"><defs><style>@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix");
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix");
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg");
+}
+@font-face{
+ font-weight: 500;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix");
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 500;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix");
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg");
+}
+@font-face{
+ font-weight: 700;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix");
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 700;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix");
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg");
+}
+
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff');
+ font-style: italic;
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff');
+ font-style: italic;
+ font-weight: 700;
+}
+* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-372" y="-12" width="480" height="72" rx="3" ry="3" fill="#f6d8dd"/><rect y="-12" rx="3" stroke="#D3455B" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="30" text-anchor="middle" style="white-space:pre;">Integration layer</tspan></text></g></g><g><g><rect x="-372" y="168" width="480" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="168" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="192" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="210" text-anchor="middle" style="white-space:pre;">Sans-I/O layer</tspan></text></g></g><g><g><rect x="-372" y="-192" width="216" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="-192" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="216" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-264" y="-150" text-anchor="middle" style="white-space:pre;">Application</tspan></text></g></g><g><g><path d="M-324,-20v-20M-324,-92v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,-104.9079 -324,-112 -318.45905,-104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-363.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="-72" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M-204,-112l0,20M-204,-40v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,-27.0921 -204,-20 -209.54095,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-243.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="#e3e6e9"/><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-96" font-family="DIN Next, sans-serif" height="24"><tspan x="0" y="-150" text-anchor="middle" style="white-space:pre;">Network</tspan></text></g></g><g><g><path d="M-60,-20v-20M-60,-92v-15.53727" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,-104.44518 -60,-111.53727 -54.45905,-104.44518" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="38" xml:space="preserve" x="-79" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="-48" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,-108.78722v18.78722M60,-38v14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,-27.0921 60,-20 54.45905,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-88" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="-70" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="-46" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,68v20M60,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,152.9079 60,160 54.45905,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="108" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-60,160v-20M-60,88v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,75.0921 -60,68 -54.45905,75.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="42" xml:space="preserve" x="-81" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-212,42" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-212,42 -212,42 -212,42" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><path d="M-204,68v20M-204,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,152.9079 -204,160 -209.54095,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="52" xml:space="preserve" x="-230" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="132" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g><g><g><path d="M-324,160l0,-18M-324,90v-14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,79.0921 -324,72 -318.45905,79.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="92" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="-352" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="110" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="134" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst
new file mode 100644
index 0000000000..2a1fe9a785
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.rst
@@ -0,0 +1,181 @@
+Deployment
+==========
+
+.. currentmodule:: websockets
+
+When you deploy your websockets server to production, at a high level, your
+architecture will almost certainly look like the following diagram:
+
+.. image:: deployment.svg
+
+The basic unit for scaling a websockets server is "one server process". Each
+blue box in the diagram represents one server process.
+
+There's more variation in routing. While the routing layer is shown as one big
+box, it is likely to involve several subsystems.
+
+When you design a deployment, your should consider two questions:
+
+1. How will I run the appropriate number of server processes?
+2. How will I route incoming connections to these processes?
+
+These questions are strongly related. There's a wide range of acceptable
+answers, depending on your goals and your constraints.
+
+You can find a few concrete examples in the :ref:`deployment how-to guides
+<deployment-howto>`.
+
+Running server processes
+------------------------
+
+How many processes do I need?
+.............................
+
+Typically, one server process will manage a few hundreds or thousands
+connections, depending on the frequency of messages and the amount of work
+they require.
+
+CPU and memory usage increase with the number of connections to the server.
+
+Often CPU is the limiting factor. If a server process goes to 100% CPU, then
+you reached the limit. How much headroom you want to keep is up to you.
+
+Once you know how many connections a server process can manage and how many
+connections you need to handle, you can calculate how many processes to run.
+
+You can also automate this calculation by configuring an autoscaler to keep
+CPU usage or connection count within acceptable limits.
+
+Don't scale with threads. Threads doesn't make sense for a server built with
+:mod:`asyncio`.
+
+How do I run processes?
+.......................
+
+Most solutions for running multiple instances of a server process fall into
+one of these three buckets:
+
+1. Running N processes on a platform:
+
+ * a Kubernetes Deployment
+
+ * its equivalent on a Platform as a Service provider
+
+2. Running N servers:
+
+ * an AWS Auto Scaling group, a GCP Managed instance group, etc.
+
+ * a fixed set of long-lived servers
+
+3. Running N processes on a server:
+
+ * preferably via a process manager or supervisor
+
+Option 1 is easiest of you have access to such a platform.
+
+Option 2 almost always combines with option 3.
+
+How do I start a process?
+.........................
+
+Run a Python program that invokes :func:`~server.serve`. That's it.
+
+Don't run an ASGI server such as Uvicorn, Hypercorn, or Daphne. They're
+alternatives to websockets, not complements.
+
+Don't run a WSGI server such as Gunicorn, Waitress, or mod_wsgi. They aren't
+designed to run WebSocket applications.
+
+Applications servers handle network connections and expose a Python API. You
+don't need one because websockets handles network connections directly.
+
+How do I stop a process?
+........................
+
+Process managers send the SIGTERM signal to terminate processes. Catch this
+signal and exit the server to ensure a graceful shutdown.
+
+Here's an example:
+
+.. literalinclude:: ../../example/faq/shutdown_server.py
+ :emphasize-lines: 12-15,18
+
+When exiting the context manager, :func:`~server.serve` closes all connections
+with code 1001 (going away). As a consequence:
+
+* If the connection handler is awaiting
+ :meth:`~server.WebSocketServerProtocol.recv`, it receives a
+ :exc:`~exceptions.ConnectionClosedOK` exception. It can catch the exception
+ and clean up before exiting.
+
+* Otherwise, it should be waiting on
+ :meth:`~server.WebSocketServerProtocol.wait_closed`, so it can receive the
+ :exc:`~exceptions.ConnectionClosedOK` exception and exit.
+
+This example is easily adapted to handle other signals.
+
+If you override the default signal handler for SIGINT, which raises
+:exc:`KeyboardInterrupt`, be aware that you won't be able to interrupt a
+program with Ctrl-C anymore when it's stuck in a loop.
+
+Routing connections
+-------------------
+
+What does routing involve?
+..........................
+
+Since the routing layer is directly exposed to the Internet, it should provide
+appropriate protection against threats ranging from Internet background noise
+to targeted attacks.
+
+You should always secure WebSocket connections with TLS. Since the routing
+layer carries the public domain name, it should terminate TLS connections.
+
+Finally, it must route connections to the server processes, balancing new
+connections across them.
+
+How do I route connections?
+...........................
+
+Here are typical solutions for load balancing, matched to ways of running
+processes:
+
+1. If you're running on a platform, it comes with a routing layer:
+
+ * a Kubernetes Ingress and Service
+
+ * a service mesh: Istio, Consul, Linkerd, etc.
+
+ * the routing mesh of a Platform as a Service
+
+2. If you're running N servers, you may load balance with:
+
+ * a cloud load balancer: AWS Elastic Load Balancing, GCP Cloud Load
+ Balancing, etc.
+
+ * A software load balancer: HAProxy, NGINX, etc.
+
+3. If you're running N processes on a server, you may load balance with:
+
+ * A software load balancer: HAProxy, NGINX, etc.
+
+ * The operating system — all processes listen on the same port
+
+You may trust the load balancer to handle encryption and to provide security.
+You may add another layer in front of the load balancer for these purposes.
+
+There are many possibilities. Don't add layers that you don't need, though.
+
+How do I implement a health check?
+..................................
+
+Load balancers need a way to check whether server processes are up and running
+to avoid routing connections to a non-functional backend.
+
+websockets provide minimal support for responding to HTTP requests with the
+:meth:`~server.WebSocketServerProtocol.process_request` hook.
+
+Here's an example:
+
+.. literalinclude:: ../../example/faq/health_check_server.py
+ :emphasize-lines: 7-9,18
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg
new file mode 100644
index 0000000000..cb948d8d6b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/deployment.svg
@@ -0,0 +1,63 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-490 -34 644 356" width="644px" height="356px"><defs><style>@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix");
+ src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 400;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix");
+ src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg");
+}
+@font-face{
+ font-weight: 500;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix");
+ src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 500;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix");
+ src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg");
+}
+@font-face{
+ font-weight: 700;
+ font-family:"DIN Next";
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix");
+ src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg");
+}
+@font-face{
+ font-family:"DIN Next";
+ font-weight: 700;
+ font-style: italic;
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix");
+ src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg");
+}
+
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff');
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff');
+ font-style: italic;
+ font-weight: 400;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff');
+ font-weight: 700;
+}
+@font-face {
+ font-family: 'PFDINMonoPro';
+ src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff');
+ font-style: italic;
+ font-weight: 700;
+}
+* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="#e3e6e9"/><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="30" text-anchor="middle" style="white-space:pre;">Internet</tspan></text></g></g><g><g><rect x="-480" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-312" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-312" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-300" font-family="DIN Next, sans-serif" height="24"><tspan x="-240" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="0" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="0" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="12" font-family="DIN Next, sans-serif" height="24"><tspan x="72" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-144" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-144" ry="1.5" height="36"/></g></g><g><g><rect x="-48" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-48" ry="1.5" height="36"/></g></g><g><g><rect x="-96" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-96" ry="1.5" height="36"/></g></g><g><g><rect x="-480" y="120" width="624" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="120" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="624" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="144" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="162" text-anchor="middle" style="white-space:pre;">routing</tspan></text></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-240" y2="227.99999999999994" x2="-240"/><polygon points="-234.45905,224.9079 -240,232 -245.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-407.99999999999994" y2="227.99999999999994" x2="-407.99999999999994"/><polygon points="-402.45905,224.9079 -408,232 -413.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="200.00000000000003" stroke-linecap="round" stroke-width="4" x1="72" y2="228" x2="72"/><polygon points="77.54095,224.9079 72,232 66.45905,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="79.92376615249583" stroke-linecap="round" stroke-width="4" x1="-168" y2="107.99999999999994" x2="-168"/><polygon points="-162.45905,104.9079 -168,112 -173.54095,104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst
new file mode 100644
index 0000000000..f164d29905
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/design.rst
@@ -0,0 +1,572 @@
+Design
+======
+
+.. currentmodule:: websockets
+
+This document describes the design of websockets. It assumes familiarity with
+the specification of the WebSocket protocol in :rfc:`6455`.
+
+It's primarily intended at maintainers. It may also be useful for users who
+wish to understand what happens under the hood.
+
+.. warning::
+
+ Internals described in this document may change at any time.
+
+ Backwards compatibility is only guaranteed for :doc:`public APIs
+ <../reference/index>`.
+
+Lifecycle
+---------
+
+State
+.....
+
+WebSocket connections go through a trivial state machine:
+
+- ``CONNECTING``: initial state,
+- ``OPEN``: when the opening handshake is complete,
+- ``CLOSING``: when the closing handshake is started,
+- ``CLOSED``: when the TCP connection is closed.
+
+Transitions happen in the following places:
+
+- ``CONNECTING -> OPEN``: in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` which runs when
+ the :ref:`opening handshake <opening-handshake>` completes and the WebSocket
+ connection is established — not to be confused with
+ :meth:`~asyncio.BaseProtocol.connection_made` which runs when the TCP connection
+ is established;
+- ``OPEN -> CLOSING``: in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.write_frame` immediately before
+ sending a close frame; since receiving a close frame triggers sending a
+ close frame, this does the right thing regardless of which side started the
+ :ref:`closing handshake <closing-handshake>`; also in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` which duplicates
+ a few lines of code from ``write_close_frame()`` and ``write_frame()``;
+- ``* -> CLOSED``: in
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_lost` which is always
+ called exactly once when the TCP connection is closed.
+
+Coroutines
+..........
+
+The following diagram shows which coroutines are running at each stage of the
+connection lifecycle on the client side.
+
+.. image:: lifecycle.svg
+ :target: _images/lifecycle.svg
+
+The lifecycle is identical on the server side, except inversion of control
+makes the equivalent of :meth:`~client.connect` implicit.
+
+Coroutines shown in green are called by the application. Multiple coroutines
+may interact with the WebSocket connection concurrently.
+
+Coroutines shown in gray manage the connection. When the opening handshake
+succeeds, :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` starts
+two tasks:
+
+- :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` runs
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.transfer_data` which handles
+ incoming data and lets :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`
+ consume it. It may be canceled to terminate the connection. It never exits
+ with an exception other than :exc:`~asyncio.CancelledError`. See :ref:`data
+ transfer <data-transfer>` below.
+
+- :attr:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task` runs
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping` which sends Ping
+ frames at regular intervals and ensures that corresponding Pong frames are
+ received. It is canceled when the connection terminates. It never exits
+ with an exception other than :exc:`~asyncio.CancelledError`.
+
+- :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` runs
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.close_connection` which waits for
+ the data transfer to terminate, then takes care of closing the TCP
+ connection. It must not be canceled. It never exits with an exception. See
+ :ref:`connection termination <connection-termination>` below.
+
+Besides, :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` starts
+the same :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` when
+the opening handshake fails, in order to close the TCP connection.
+
+Splitting the responsibilities between two tasks makes it easier to guarantee
+that websockets can terminate connections:
+
+- within a fixed timeout,
+- without leaking pending tasks,
+- without leaking open TCP connections,
+
+regardless of whether the connection terminates normally or abnormally.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` completes when no
+more data will be received on the connection. Under normal circumstances, it
+exits after exchanging close frames.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` completes when
+the TCP connection is closed.
+
+
+.. _opening-handshake:
+
+Opening handshake
+-----------------
+
+websockets performs the opening handshake when establishing a WebSocket
+connection. On the client side, :meth:`~client.connect` executes it
+before returning the protocol to the caller. On the server side, it's executed
+before passing the protocol to the ``ws_handler`` coroutine handling the
+connection.
+
+While the opening handshake is asymmetrical — the client sends an HTTP Upgrade
+request and the server replies with an HTTP Switching Protocols response —
+websockets aims at keeping the implementation of both sides consistent with
+one another.
+
+On the client side, :meth:`~client.WebSocketClientProtocol.handshake`:
+
+- builds an HTTP request based on the ``uri`` and parameters passed to
+ :meth:`~client.connect`;
+- writes the HTTP request to the network;
+- reads an HTTP response from the network;
+- checks the HTTP response, validates ``extensions`` and ``subprotocol``, and
+ configures the protocol accordingly;
+- moves to the ``OPEN`` state.
+
+On the server side, :meth:`~server.WebSocketServerProtocol.handshake`:
+
+- reads an HTTP request from the network;
+- calls :meth:`~server.WebSocketServerProtocol.process_request` which may
+ abort the WebSocket handshake and return an HTTP response instead; this
+ hook only makes sense on the server side;
+- checks the HTTP request, negotiates ``extensions`` and ``subprotocol``, and
+ configures the protocol accordingly;
+- builds an HTTP response based on the above and parameters passed to
+ :meth:`~server.serve`;
+- writes the HTTP response to the network;
+- moves to the ``OPEN`` state;
+- returns the ``path`` part of the ``uri``.
+
+The most significant asymmetry between the two sides of the opening handshake
+lies in the negotiation of extensions and, to a lesser extent, of the
+subprotocol. The server knows everything about both sides and decides what the
+parameters should be for the connection. The client merely applies them.
+
+If anything goes wrong during the opening handshake, websockets :ref:`fails
+the connection <connection-failure>`.
+
+
+.. _data-transfer:
+
+Data transfer
+-------------
+
+Symmetry
+........
+
+Once the opening handshake has completed, the WebSocket protocol enters the
+data transfer phase. This part is almost symmetrical. There are only two
+differences between a server and a client:
+
+- `client-to-server masking`_: the client masks outgoing frames; the server
+ unmasks incoming frames;
+- `closing the TCP connection`_: the server closes the connection immediately;
+ the client waits for the server to do it.
+
+.. _client-to-server masking: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3
+.. _closing the TCP connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.1
+
+These differences are so minor that all the logic for `data framing`_, for
+`sending and receiving data`_ and for `closing the connection`_ is implemented
+in the same class, :class:`~legacy.protocol.WebSocketCommonProtocol`.
+
+.. _data framing: https://www.rfc-editor.org/rfc/rfc6455.html#section-5
+.. _sending and receiving data: https://www.rfc-editor.org/rfc/rfc6455.html#section-6
+.. _closing the connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-7
+
+The :attr:`~legacy.protocol.WebSocketCommonProtocol.is_client` attribute tells which
+side a protocol instance is managing. This attribute is defined on the
+:attr:`~server.WebSocketServerProtocol` and
+:attr:`~client.WebSocketClientProtocol` classes.
+
+Data flow
+.........
+
+The following diagram shows how data flows between an application built on top
+of websockets and a remote endpoint. It applies regardless of which side is
+the server or the client.
+
+.. image:: protocol.svg
+ :target: _images/protocol.svg
+
+Public methods are shown in green, private methods in yellow, and buffers in
+orange. Methods related to connection termination are omitted; connection
+termination is discussed in another section below.
+
+Receiving data
+..............
+
+The left side of the diagram shows how websockets receives data.
+
+Incoming data is written to a :class:`~asyncio.StreamReader` in order to
+implement flow control and provide backpressure on the TCP connection.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, which is started
+when the WebSocket connection is established, processes this data.
+
+When it receives data frames, it reassembles fragments and puts the resulting
+messages in the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue.
+
+When it encounters a control frame:
+
+- if it's a close frame, it starts the closing handshake;
+- if it's a ping frame, it answers with a pong frame;
+- if it's a pong frame, it acknowledges the corresponding ping (unless it's an
+ unsolicited pong).
+
+Running this process in a task guarantees that control frames are processed
+promptly. Without such a task, websockets would depend on the application to
+drive the connection by having exactly one coroutine awaiting
+:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` at any time. While this
+happens naturally in many use cases, it cannot be relied upon.
+
+Then :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` fetches the next message
+from the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue, with some
+complexity added for handling backpressure and termination correctly.
+
+Sending data
+............
+
+The right side of the diagram shows how websockets sends data.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send` writes one or several data
+frames containing the message. While sending a fragmented message, concurrent
+calls to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` are put on hold until
+all fragments are sent. This makes concurrent calls safe.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ping` writes a ping frame and
+yields a :class:`~asyncio.Future` which will be completed when a matching pong
+frame is received.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` writes a pong frame.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` writes a close frame and
+waits for the TCP connection to terminate.
+
+Outgoing data is written to a :class:`~asyncio.StreamWriter` in order to
+implement flow control and provide backpressure from the TCP connection.
+
+.. _closing-handshake:
+
+Closing handshake
+.................
+
+When the other side of the connection initiates the closing handshake,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives a close
+frame while in the ``OPEN`` state. It moves to the ``CLOSING`` state, sends a
+close frame, and returns :obj:`None`, causing
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate.
+
+When this side of the connection initiates the closing handshake with
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close`, it moves to the ``CLOSING``
+state and sends a close frame. When the other side sends a close frame,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives it in the
+``CLOSING`` state and returns :obj:`None`, also causing
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate.
+
+If the other side doesn't send a close frame within the connection's close
+timeout, websockets :ref:`fails the connection <connection-failure>`.
+
+The closing handshake can take up to ``2 * close_timeout``: one
+``close_timeout`` to write a close frame and one ``close_timeout`` to receive
+a close frame.
+
+Then websockets terminates the TCP connection.
+
+
+.. _connection-termination:
+
+Connection termination
+----------------------
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which is
+started when the WebSocket connection is established, is responsible for
+eventually closing the TCP connection.
+
+First :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` waits
+for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate,
+which may happen as a result of:
+
+- a successful closing handshake: as explained above, this exits the infinite
+ loop in :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`;
+- a timeout while waiting for the closing handshake to complete: this cancels
+ :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`;
+- a protocol error, including connection errors: depending on the exception,
+ :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` :ref:`fails the
+ connection <connection-failure>` with a suitable code and exits.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` is separate
+from :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to make it
+easier to implement the timeout on the closing handshake. Canceling
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` creates no risk
+of canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`
+and failing to close the TCP connection, thus leaking resources.
+
+Then :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` cancels
+:meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no
+protocol compliance responsibilities. Terminating it to avoid leaking it is
+the only concern.
+
+Terminating the TCP connection can take up to ``2 * close_timeout`` on the
+server side and ``3 * close_timeout`` on the client side. Clients start by
+waiting for the server to close the connection, hence the extra
+``close_timeout``. Then both sides go through the following steps until the
+TCP connection is lost: half-closing the connection (only for non-TLS
+connections), closing the connection, aborting the connection. At this point
+the connection drops regardless of what happens on the network.
+
+
+.. _connection-failure:
+
+Connection failure
+------------------
+
+If the opening handshake doesn't complete successfully, websockets fails the
+connection by closing the TCP connection.
+
+Once the opening handshake has completed, websockets fails the connection by
+canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`
+and sending a close frame if appropriate.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` exits, unblocking
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which closes
+the TCP connection.
+
+
+.. _server-shutdown:
+
+Server shutdown
+---------------
+
+:class:`~websockets.server.WebSocketServer` closes asynchronously like
+:class:`asyncio.Server`. The shutdown happen in two steps:
+
+1. Stop listening and accepting new connections;
+2. Close established connections with close code 1001 (going away) or, if
+ the opening handshake is still in progress, with HTTP status code 503
+ (Service Unavailable).
+
+The first call to :class:`~websockets.server.WebSocketServer.close` starts a
+task that performs this sequence. Further calls are ignored. This is the
+easiest way to make :class:`~websockets.server.WebSocketServer.close` and
+:class:`~websockets.server.WebSocketServer.wait_closed` idempotent.
+
+
+.. _cancellation:
+
+Cancellation
+------------
+
+User code
+.........
+
+websockets provides a WebSocket application server. It manages connections and
+passes them to user-provided connection handlers. This is an *inversion of
+control* scenario: library code calls user code.
+
+If a connection drops, the corresponding handler should terminate. If the
+server shuts down, all connection handlers must terminate. Canceling
+connection handlers would terminate them.
+
+However, using cancellation for this purpose would require all connection
+handlers to handle it properly. For example, if a connection handler starts
+some tasks, it should catch :exc:`~asyncio.CancelledError`, terminate or
+cancel these tasks, and then re-raise the exception.
+
+Cancellation is tricky in :mod:`asyncio` applications, especially when it
+interacts with finalization logic. In the example above, what if a handler
+gets interrupted with :exc:`~asyncio.CancelledError` while it's finalizing
+the tasks it started, after detecting that the connection dropped?
+
+websockets considers that cancellation may only be triggered by the caller of
+a coroutine when it doesn't care about the results of that coroutine anymore.
+(Source: `Guido van Rossum <https://groups.google.com/forum/#!msg
+/python-tulip/LZQe38CR3bg/7qZ1p_q5yycJ>`_). Since connection handlers run
+arbitrary user code, websockets has no way of deciding whether that code is
+still doing something worth caring about.
+
+For these reasons, websockets never cancels connection handlers. Instead it
+expects them to detect when the connection is closed, execute finalization
+logic if needed, and exit.
+
+Conversely, cancellation isn't a concern for WebSocket clients because they
+don't involve inversion of control.
+
+Library
+.......
+
+Most :doc:`public APIs <../reference/index>` of websockets are coroutines.
+They may be canceled, for example if the user starts a task that calls these
+coroutines and cancels the task later. websockets must handle this situation.
+
+Cancellation during the opening handshake is handled like any other exception:
+the TCP connection is closed and the exception is re-raised. This can only
+happen on the client side. On the server side, the opening handshake is
+managed by websockets and nothing results in a cancellation.
+
+Once the WebSocket connection is established, internal tasks
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` and
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` mustn't get
+accidentally canceled if a coroutine that awaits them is canceled. In other
+words, they must be shielded from cancellation.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` waits for the next message in
+the queue or for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`
+to terminate, whichever comes first. It relies on :func:`~asyncio.wait` for
+waiting on two futures in parallel. As a consequence, even though it's waiting
+on a :class:`~asyncio.Future` signaling the next message and on
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, it doesn't
+propagate cancellation to them.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ensure_open` is called by
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and
+:meth:`~legacy.protocol.WebSocketCommonProtocol.pong`. When the connection state is
+``CLOSING``, it waits for
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` but shields it to
+prevent cancellation.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` waits for the data transfer
+task to terminate with :func:`~asyncio.timeout`. If it's canceled or if the
+timeout elapses, :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`
+is canceled, which is correct at this point.
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` then waits for
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` but shields it
+to prevent cancellation.
+
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close` and
+:meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` are the only
+places where :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` may
+be canceled.
+
+:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` starts by
+waiting for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`. It
+catches :exc:`~asyncio.CancelledError` to prevent a cancellation of
+:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` from propagating
+to :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`.
+
+.. _backpressure:
+
+Backpressure
+------------
+
+.. note::
+
+ This section discusses backpressure from the perspective of a server but
+ the concept applies to clients symmetrically.
+
+With a naive implementation, if a server receives inputs faster than it can
+process them, or if it generates outputs faster than it can send them, data
+accumulates in buffers, eventually causing the server to run out of memory and
+crash.
+
+The solution to this problem is backpressure. Any part of the server that
+receives inputs faster than it can process them and send the outputs
+must propagate that information back to the previous part in the chain.
+
+websockets is designed to make it easy to get backpressure right.
+
+For incoming data, websockets builds upon :class:`~asyncio.StreamReader` which
+propagates backpressure to its own buffer and to the TCP stream. Frames are
+parsed from the input stream and added to a bounded queue. If the queue fills
+up, parsing halts until the application reads a frame.
+
+For outgoing data, websockets builds upon :class:`~asyncio.StreamWriter` which
+implements flow control. If the output buffers grow too large, it waits until
+they're drained. That's why all APIs that write frames are asynchronous.
+
+Of course, it's still possible for an application to create its own unbounded
+buffers and break the backpressure. Be careful with queues.
+
+
+.. _buffers:
+
+Buffers
+-------
+
+.. note::
+
+ This section discusses buffers from the perspective of a server but it
+ applies to clients as well.
+
+An asynchronous systems works best when its buffers are almost always empty.
+
+For example, if a client sends data too fast for a server, the queue of
+incoming messages will be constantly full. The server will always be 32
+messages (by default) behind the client. This consumes memory and increases
+latency for no good reason. The problem is called bufferbloat.
+
+If buffers are almost always full and that problem cannot be solved by adding
+capacity — typically because the system is bottlenecked by the output and
+constantly regulated by backpressure — reducing the size of buffers minimizes
+negative consequences.
+
+By default websockets has rather high limits. You can decrease them according
+to your application's characteristics.
+
+Bufferbloat can happen at every level in the stack where there is a buffer.
+For each connection, the receiving side contains these buffers:
+
+- OS buffers: tuning them is an advanced optimization.
+- :class:`~asyncio.StreamReader` bytes buffer: the default limit is 64 KiB.
+ You can set another limit by passing a ``read_limit`` keyword argument to
+ :func:`~client.connect()` or :func:`~server.serve`.
+- Incoming messages :class:`~collections.deque`: its size depends both on
+ the size and the number of messages it contains. By default the maximum
+ UTF-8 encoded size is 1 MiB and the maximum number is 32. In the worst case,
+ after UTF-8 decoding, a single message could take up to 4 MiB of memory and
+ the overall memory consumption could reach 128 MiB. You should adjust these
+ limits by setting the ``max_size`` and ``max_queue`` keyword arguments of
+ :func:`~client.connect()` or :func:`~server.serve` according to your
+ application's requirements.
+
+For each connection, the sending side contains these buffers:
+
+- :class:`~asyncio.StreamWriter` bytes buffer: the default size is 64 KiB.
+ You can set another limit by passing a ``write_limit`` keyword argument to
+ :func:`~client.connect()` or :func:`~server.serve`.
+- OS buffers: tuning them is an advanced optimization.
+
+Concurrency
+-----------
+
+Awaiting any combination of :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.send`,
+:meth:`~legacy.protocol.WebSocketCommonProtocol.close`
+:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, or
+:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` concurrently is safe, including
+multiple calls to the same method, with one exception and one limitation.
+
+* **Only one coroutine can receive messages at a time.** This constraint
+ avoids non-deterministic behavior (and simplifies the implementation). If a
+ coroutine is awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`,
+ awaiting it again in another coroutine raises :exc:`RuntimeError`.
+
+* **Sending a fragmented message forces serialization.** Indeed, the WebSocket
+ protocol doesn't support multiplexing messages. If a coroutine is awaiting
+ :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to send a fragmented message,
+ awaiting it again in another coroutine waits until the first call completes.
+ This will be transparent in many cases. It may be a concern if the
+ fragmented message is generated slowly by an asynchronous iterator.
+
+Receiving frames is independent from sending frames. This isolates
+:meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, which receives frames, from
+the other methods, which send frames.
+
+While the connection is open, each frame is sent with a single write. Combined
+with the concurrency model of :mod:`asyncio`, this enforces serialization. The
+only other requirement is to prevent interleaving other data frames in the
+middle of a fragmented message.
+
+After the connection is closed, sending a frame raises
+:exc:`~websockets.exceptions.ConnectionClosed`, which is safe.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst
new file mode 100644
index 0000000000..120a3dd327
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/index.rst
@@ -0,0 +1,18 @@
+Topic guides
+============
+
+Get a deeper understanding of how websockets is built and why.
+
+.. toctree::
+ :titlesonly:
+
+ deployment
+ logging
+ authentication
+ broadcast
+ compression
+ timeouts
+ design
+ memory
+ security
+ performance
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle
new file mode 100644
index 0000000000..bd888dcf31
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle
@@ -0,0 +1,25 @@
+��][W�8~~���}�G�%��a�.�0KC��N�����J���i�i���ߒs�5n����cUI咾������}틌b/ ~Y�:Z�d��� ~Y}��u�Z�����?v����j#ߋ����������j�G#_�Z;���`��� �Vk�pU[=O�ћV���R���CE���p$���2[�M�U(f�{A��zN����ƅ��l;��E�W2�\�u���B�$r �M�њ]�X�lϱ��t��Ɏ"[]��'<�&H����D�x�������}�7ZS�5�B'�OL�9ɑl�fYOD'a��s'�h,[��-۹PE.7:��9��٧d�"������
+�i�zM�&_�L,���*�mߎ������2K,�;�L�$�� Y'��eF9�x�Y�Yj.y��ٗ�X�`�Gyq@�sE̮���c�1 �K��m;�b�G�7�0Se��|Ef�/Y��k����a���AP� WeJ�;#ہ,�����M��"=�0K��V�0c�?�m��IE<�����:��o(~cp�����*�0*󷯆2��=@�U�*��x�ȷ�:��W��¡9C�m~]��WϗݫQ����i�f�:� �2��%�N�w��M
+�����<^ k`2}�0H��~���+��F�'�/2�T�s��\kS��*WO7��<&&{�v��s��(�yF[x�������J�%&Eq­���Ց�jXX:% ƅ!���f`�[#�-D-$�y]�݌���&���)Lr;wf�kM[E��톗yMe��m�}���pV�V�ݎ���a�fu1M͗3a=�۹*��m?NҜ�k{���Q]�<79/POZ��rͶ�l�f����`�=�LkM#��[Y&�%8��*ܤ��s�X)������a��0Ma0
+6�2�B�JKb@i���i"!8����u�;��H�����py2:��ǝwݥ��nz=(�P�������E����������֫�w�b�����QlC�)&$g��n�n1*A�Ж���{sF��,؊��٤��� O����7�X�b� Ģ&C�04�ʽ�ۻ�0
+dtb��8�� �Bp�X ��h��&Q~M7��K��%*�]�%Sd/J��g���8�&���� m��8����g�#���K����>����+���w��(� ���d}+��wݟ���Q`��{�t�+�����8���)տz���\����~odGn/��8�_�Iz�:Bӓ���0RC�$��$ �$��N�$Pc(�׋;G�����78OdзU�C++�1�z��P����v!����/��Tx}[��
+���:���GQr�����H�� �O��g�=u�i�V�})3tF
+Q
+� ��%«��c/�u�gO���O�t�>^ǂ�����.�͏a�wLę���R�D��#F����I��@J���dfE>��s�X����t�(� �C0�"��V)8���aA�+h��kL˄��&LsMc\�b�b!L�xAn��D�z`��D|W��'�OH���"Ne�ځ��#�,np��֒�ӗQo��p��������v�Cӹ�w�'Øb�#bppO ��2����
+�Y�c�YF:�����j`������ �:�A�V
+� �nzD܂S�8�X�B��qk6�mp��q�d�0O�ʦ^L��s����*���7��h�=�0�i>'a�k(�қ�kZE�;��rKW>àG�֌$�ʑ�����2lQ]�hnRaLM�b�*_�|�E���e��(*d��2�����%4�c�sv���z����)���H:_�����ӊR���4���4��ҁ/�=b��b�
+u�`����]�jʔ���]
+n\�]]
+rW�WӸ�׸�b:������R�fg��
+E:f&5-�H�ݯ�7���
+Ͳ��[4��zՑ����-r��O�o��&�P�u�b���~-�9�H+���P�z�1�[��5�7��q=��iV��t5[��I-{���{�[^ۂ�C�'M��.sS��U�_��o�{O��I������O���{#ǰt�N̤�p<��rj��4�yA���ENi�eYwANw��9/9�z̽�GNw��97��Vq",A *M�~����N��"���}!������,��Ŀ�a��&��:�;�8�Zywy9�>��Z������/���o��1] �0��Fj�nh�ԗ�ԣ���DŽ)�q��9�T��
+N_�G=:<���.�TK�W��<7?ZSܓZ���-����V�YWy����0�e�w�Q�n���e��*{Y��۩la� �Q�
+��/n'�v�2>���&.����{g�}�KŤ#!��@ƕ���[��#܍��E��R��aq7����dDEm�ߺ�z�,�ɭ�����S�h�J~վ�Z��i���#���8����.j� m8N7�v�
+s�k/$%u�� �y�Nn���X�Uj�)mGv��K�5KRz?s7xAU7L*�jOo5{ �An�jV�l)xOD���R�)�L.Ñ�}l ��"�T�6]u���9��UT��?GŻ����O,�F�ZG�:fo{c��m�'�;�~&r�':�$�`C&�/��2M[PM��
+͋qs��
+�$����V����Y�}?��"����Y�I {a���^W��;��T��ޭ-h���������#��-���^ئ�o����=o��G �}��w���=z�?����mH��e�.��������a��� 4�~�/���,���'Q�2*7� 1mT��5�ON����/����ZLa���4���o��7�aj�5S��[
+U��D����H���?QA�S�7=E�E��V�Q ��Xw�H�=
+���m�Ix��G3����������w��j�&�(*�!ԩ�[��n��%���Yv��;���c��5ox����
+/�I����f5�^]�Ŧ|{E��T��8�&Lߝ��>MT���2�;��
+�)�*}���)�k�4k�j�z���l��y����<r���̾-�8��9[T����o��I��R d��i�O�`��J�T���L�±���\Ө)��p���ɩ�Ŝ��Х�k�F��w
+�Β���01�٭�����ס6W�� �Ǵj
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg
new file mode 100644
index 0000000000..0a9818d293
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/lifecycle.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-14.3464565 112.653543 624.6929 372.69291" width="624.6929pt" height="372.69291pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2018-07-29 15:25:34 +0000</dc:date></metadata><defs><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(19.173228 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="1.5138254" y="10" textLength="72.01172">CONNECTING</tspan></text><text transform="translate(160.90551 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="23.117341" y="10" textLength="28.804688">OPEN</tspan></text><text transform="translate(359.3307 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">CLOSING</tspan></text><text transform="translate(501.063 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="15.916169" y="10" textLength="43.20703">CLOSED</tspan></text><line x1="198.4252" y1="170.07874" x2="198.4252" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="396.8504" y1="170.07874" x2="396.8504" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="538.58267" y1="170.07874" x2="538.58267" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="56.692913" y1="170.07874" x2="56.692913" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" fill="#dadada"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 248.11811)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="52.40498" y="10" textLength="93.615234">transfer_data</tspan></text><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" fill="#dadada"/><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 361.50393)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="107.469364" y="10" textLength="115.21875">close_connection</tspan></text><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" fill="#6f6"/><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">connect</tspan></text><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" fill="#6f6"/><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="17.912947" y="10" textLength="100.816406">recv / send / </tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="118.72935" y="10" textLength="93.615234">ping / pong /</tspan><tspan font-family="Courier New" font-size="12" font-weight="bold" x="212.34459" y="10" textLength="50.408203"> close </tspan></text><path d="M 170.07874 198.4252 L 183.97874 198.4252 L 198.4252 198.4252 L 198.4252 283.46457 L 198.4252 368.50393 L 212.87165 368.50393 L 215.37165 368.50393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(75.86614 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="27.760296" y="12" textLength="52.083984">opening </tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.164593" y="27" textLength="58.02539">handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="77.072796" y="27" textLength="7.1484375">e</tspan></text><text transform="translate(416.02362 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.182171" y="12" textLength="65.021484">connection</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="16.861858" y="27" textLength="69.66211">termination</tspan></text><text transform="translate(217.59842 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="41.03058" y="12" textLength="40.6875">data tr</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="81.507143" y="12" textLength="37.541016">ansfer</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="18.211245" y="27" textLength="116.625">&amp; closing handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="134.71906" y="27" textLength="7.1484375">e</tspan></text><path d="M 425.19685 255.11811 L 439.09685 255.11811 L 453.5433 255.11811 L 453.5433 342.9307" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" fill="#dadada"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 304.81102)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="48.804395" y="10" textLength="100.816406">keepalive_ping</tspan></text><line x1="198.4252" y1="255.11811" x2="214.62165" y2="255.11811" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="198.4252" y1="311.81102" x2="215.37165" y2="311.81102" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst
new file mode 100644
index 0000000000..e7abd96ce5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/logging.rst
@@ -0,0 +1,245 @@
+Logging
+=======
+
+.. currentmodule:: websockets
+
+Logs contents
+-------------
+
+When you run a WebSocket client, your code calls coroutines provided by
+websockets.
+
+If an error occurs, websockets tells you by raising an exception. For example,
+it raises a :exc:`~exceptions.ConnectionClosed` exception if the other side
+closes the connection.
+
+When you run a WebSocket server, websockets accepts connections, performs the
+opening handshake, runs the connection handler coroutine that you provided,
+and performs the closing handshake.
+
+Given this `inversion of control`_, if an error happens in the opening
+handshake or if the connection handler crashes, there is no way to raise an
+exception that you can handle.
+
+.. _inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control
+
+Logs tell you about these errors.
+
+Besides errors, you may want to record the activity of the server.
+
+In a request/response protocol such as HTTP, there's an obvious way to record
+activity: log one event per request/response. Unfortunately, this solution
+doesn't work well for a bidirectional protocol such as WebSocket.
+
+Instead, when running as a server, websockets logs one event when a
+`connection is established`_ and another event when a `connection is
+closed`_.
+
+.. _connection is established: https://www.rfc-editor.org/rfc/rfc6455.html#section-4
+.. _connection is closed: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.4
+
+By default, websockets doesn't log an event for every message. That would be
+excessive for many applications exchanging small messages at a fast rate. If
+you need this level of detail, you could add logging in your own code.
+
+Finally, you can enable debug logs to get details about everything websockets
+is doing. This can be useful when developing clients as well as servers.
+
+See :ref:`log levels <log-levels>` below for a list of events logged by
+websockets logs at each log level.
+
+Configure logging
+-----------------
+
+websockets relies on the :mod:`logging` module from the standard library in
+order to maximize compatibility and integrate nicely with other libraries::
+
+ import logging
+
+websockets logs to the ``"websockets.client"`` and ``"websockets.server"``
+loggers.
+
+websockets doesn't provide a default logging configuration because
+requirements vary a lot depending on the environment.
+
+Here's a basic configuration for a server in production::
+
+ logging.basicConfig(
+ format="%(asctime)s %(message)s",
+ level=logging.INFO,
+ )
+
+Here's how to enable debug logs for development::
+
+ logging.basicConfig(
+ format="%(message)s",
+ level=logging.DEBUG,
+ )
+
+Furthermore, websockets adds a ``websocket`` attribute to log records, so you
+can include additional information about the current connection in logs.
+
+You could attempt to add information with a formatter::
+
+ # this doesn't work!
+ logging.basicConfig(
+ format="{asctime} {websocket.id} {websocket.remote_address[0]} {message}",
+ level=logging.INFO,
+ style="{",
+ )
+
+However, this technique runs into two problems:
+
+* The formatter applies to all records. It will crash if it receives a record
+ without a ``websocket`` attribute. For example, this happens when logging
+ that the server starts because there is no current connection.
+
+* Even with :meth:`str.format` style, you're restricted to attribute and index
+ lookups, which isn't enough to implement some fairly simple requirements.
+
+There's a better way. :func:`~client.connect` and :func:`~server.serve` accept
+a ``logger`` argument to override the default :class:`~logging.Logger`. You
+can set ``logger`` to a :class:`~logging.LoggerAdapter` that enriches logs.
+
+For example, if the server is behind a reverse
+proxy, :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` gives
+the IP address of the proxy, which isn't useful. IP addresses of clients are
+provided in an HTTP header set by the proxy.
+
+Here's how to include them in logs, assuming they're in the
+``X-Forwarded-For`` header::
+
+ logging.basicConfig(
+ format="%(asctime)s %(message)s",
+ level=logging.INFO,
+ )
+
+ class LoggerAdapter(logging.LoggerAdapter):
+ """Add connection ID and client IP address to websockets logs."""
+ def process(self, msg, kwargs):
+ try:
+ websocket = kwargs["extra"]["websocket"]
+ except KeyError:
+ return msg, kwargs
+ xff = websocket.request_headers.get("X-Forwarded-For")
+ return f"{websocket.id} {xff} {msg}", kwargs
+
+ async with websockets.serve(
+ ...,
+ # Python < 3.10 requires passing None as the second argument.
+ logger=LoggerAdapter(logging.getLogger("websockets.server"), None),
+ ):
+ ...
+
+Logging to JSON
+---------------
+
+Even though :mod:`logging` predates structured logging, it's still possible to
+output logs as JSON with a bit of effort.
+
+First, we need a :class:`~logging.Formatter` that renders JSON:
+
+.. literalinclude:: ../../example/logging/json_log_formatter.py
+
+Then, we configure logging to apply this formatter::
+
+ handler = logging.StreamHandler()
+ handler.setFormatter(formatter)
+
+ logger = logging.getLogger()
+ logger.addHandler(handler)
+ logger.setLevel(logging.INFO)
+
+Finally, we populate the ``event_data`` custom attribute in log records with
+a :class:`~logging.LoggerAdapter`::
+
+ class LoggerAdapter(logging.LoggerAdapter):
+ """Add connection ID and client IP address to websockets logs."""
+ def process(self, msg, kwargs):
+ try:
+ websocket = kwargs["extra"]["websocket"]
+ except KeyError:
+ return msg, kwargs
+ kwargs["extra"]["event_data"] = {
+ "connection_id": str(websocket.id),
+ "remote_addr": websocket.request_headers.get("X-Forwarded-For"),
+ }
+ return msg, kwargs
+
+ async with websockets.serve(
+ ...,
+ # Python < 3.10 requires passing None as the second argument.
+ logger=LoggerAdapter(logging.getLogger("websockets.server"), None),
+ ):
+ ...
+
+Disable logging
+---------------
+
+If your application doesn't configure :mod:`logging`, Python outputs messages
+of severity ``WARNING`` and higher to :data:`~sys.stderr`. As a consequence,
+you will see a message and a stack trace if a connection handler coroutine
+crashes or if you hit a bug in websockets.
+
+If you want to disable this behavior for websockets, you can add
+a :class:`~logging.NullHandler`::
+
+ logging.getLogger("websockets").addHandler(logging.NullHandler())
+
+Additionally, if your application configures :mod:`logging`, you must disable
+propagation to the root logger, or else its handlers could output logs::
+
+ logging.getLogger("websockets").propagate = False
+
+Alternatively, you could set the log level to ``CRITICAL`` for the
+``"websockets"`` logger, as the highest level currently used is ``ERROR``::
+
+ logging.getLogger("websockets").setLevel(logging.CRITICAL)
+
+Or you could configure a filter to drop all messages::
+
+ logging.getLogger("websockets").addFilter(lambda record: None)
+
+.. _log-levels:
+
+Log levels
+----------
+
+Here's what websockets logs at each level.
+
+``ERROR``
+.........
+
+* Exceptions raised by connection handler coroutines in servers
+* Exceptions resulting from bugs in websockets
+
+``WARNING``
+...........
+
+* Failures in :func:`~websockets.broadcast`
+
+``INFO``
+........
+
+* Server starting and stopping
+* Server establishing and closing connections
+* Client reconnecting automatically
+
+``DEBUG``
+.........
+
+* Changes to the state of connections
+* Handshake requests and responses
+* All frames sent and received
+* Steps to close a connection
+* Keepalive pings and pongs
+* Errors handled transparently
+
+Debug messages have cute prefixes that make logs easier to scan:
+
+* ``>`` - send something
+* ``<`` - receive something
+* ``=`` - set connection state
+* ``x`` - shut down connection
+* ``%`` - manage pings and pongs
+* ``!`` - handle errors and timeouts
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst
new file mode 100644
index 0000000000..e44247a77c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/memory.rst
@@ -0,0 +1,48 @@
+Memory usage
+============
+
+.. currentmodule:: websockets
+
+In most cases, memory usage of a WebSocket server is proportional to the
+number of open connections. When a server handles thousands of connections,
+memory usage can become a bottleneck.
+
+Memory usage of a single connection is the sum of:
+
+1. the baseline amount of memory websockets requires for each connection,
+2. the amount of data held in buffers before the application processes it,
+3. any additional memory allocated by the application itself.
+
+Baseline
+--------
+
+Compression settings are the main factor affecting the baseline amount of
+memory used by each connection.
+
+With websockets' defaults, on the server side, a single connections uses
+70 KiB of memory.
+
+Refer to the :doc:`topic guide on compression <../topics/compression>` to
+learn more about tuning compression settings.
+
+Buffers
+-------
+
+Under normal circumstances, buffers are almost always empty.
+
+Under high load, if a server receives more messages than it can process,
+bufferbloat can result in excessive memory usage.
+
+By default websockets has generous limits. It is strongly recommended to adapt
+them to your application. When you call :func:`~server.serve`:
+
+- Set ``max_size`` (default: 1 MiB, UTF-8 encoded) to the maximum size of
+ messages your application generates.
+- Set ``max_queue`` (default: 32) to the maximum number of messages your
+ application expects to receive faster than it can process them. The queue
+ provides burst tolerance without slowing down the TCP connection.
+
+Furthermore, you can lower ``read_limit`` and ``write_limit`` (default:
+64 KiB) to reduce the size of buffers for incoming and outgoing data.
+
+The design document provides :ref:`more details about buffers <buffers>`.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst
new file mode 100644
index 0000000000..45e23b2390
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/performance.rst
@@ -0,0 +1,20 @@
+Performance
+===========
+
+Here are tips to optimize performance.
+
+uvloop
+------
+
+You can make a websockets application faster by running it with uvloop_.
+
+(This advice isn't specific to websockets. It applies to any :mod:`asyncio`
+application.)
+
+.. _uvloop: https://github.com/MagicStack/uvloop
+
+broadcast
+---------
+
+:func:`~websockets.broadcast` is the most efficient way to send a message to
+many clients.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle
new file mode 100644
index 0000000000..04a9e7acb5
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.graffle
@@ -0,0 +1,37 @@
+��]ks�6����
+���[Ӹ��q�t�n�xc'�v<ӡ%X�"U���f���J�x-J���ۉl�A��\q��_FQ�N�0�|�=����~2�Ꮟ���#���'�����ѯ/z�(�����g�_������Ǒ��}~��w����Q���}��q��Y����=??�C����!�v�d����5t�
+�A>x ���^|;���G�=��/�����~\��U<�_��o�b�z�ӧ���寗M��a?ȡ�Ӟ獂4
+�/�=���)��KFq8L���{��+
+NO#-���HJ�X(�P>��D�d����&y�7��$���Ӊ޽��,�2��p��Y؟�͞}Ff(��k�a|����o���b���=����2`�b?
+�f��$
+w�5y�|A9��<�S���"���1O/B�ɧ�������ip��b`oޖ�3:�����L�0ӿ%ɨÊ���A�6
+��b-\Nf1��W�hq����9o�J��(ދ�a�n���>}�bN�� zJ����'\*�d��p��0��y�7H�w��A��� �@E��~�5�$i����H�a��7��5Vm�<��Qpq��ƽ�����~���w�S飋q�qLS�-��?�8�s�|jIcng<3�c��ꊭ2�|<��X[e�I���O��un(,����>���猯J��|W�r�t�w_��.�C���R$�U�)�Qʐ���|I)�Xq�}�J� �ǂbd\}�
+��5#��JH�+�S /o�N+�4f5;�yy�v|i��\ڑ���K �Ҵ2���[�4�Z�Ϣi�gJo��W<���A��B|DY!C�DGA��ن�1�g�! ϯ���Κ@\g���* *Ʃ���1��+*��1�=)�/��1�2#�$�UT(.a�()a��Q&��>RJ�oߚ��¾�g�X6����$
+u�F��rԙ���\,,\��;N�(�&޸U��������g�5>�+�%o�劀/�2T}c3�
+�� +�h�q�A�i~���8���!&���~�K������=�z|
++7?��O���(�4>>�i�sԫ��|�~%)4��q����a�ul~9�&>���/������?��8���1&Ň������ U c �M[�����7L� ��Ad����Y��ӠZz��x����q��N�`��o~������"��+7t�J�K"|��`R��t��:t���"�v�{p�@��4?K�IDς�xlF`S�K�/]66�œ���'9<��WT16b�OC>�o) �p����y�Խ|�\~��f�Ad�������Ǖd�*�K�|�f�ՍB_�#5 �n� �M�P�kZm�N��ᵲ��������a
+.���f�̸=���*v,�����٤?�a�!BV�c_����?gYk��y��"�2���P�J���&�4f�/<N�""���Ķ�9����V�Oa׾*�|)�pG��BQ�)�P��%yO� �6}����Xs�ږkY��$��
+�><�DǏn�U��� tS&�5fQS{��a�����H� F�X>�v=��f����ĩ�v6�-�p+��5L8·�*�j]��%l-�� )@G�m�ȅ�eqΰ5|ykǍ���8Y�;B�M���"&����S˜����֎;F\��M
+��� +#.i��������Xu:)�ڛggnqS淍��``mb)}čJ�6�m��$W0H�" 1_��P�,�
+��W��!{�
+�}���m��l�����j{ɖ�@1,i��1�@n;\|�!_�Ta!�tr�H;ϒh�=�TŤNhl�� c��WV���֋��O�|m�yqUY$�7��
+��(�$'��sՄ�鏘}V�_��CKKl�K�w�ӎ1V�_h5�[��������D�f�wwCH��bE'&Z�fѢ(���e[�և\�4�
+wr��X�'VA�1���@�v�mp�F3
+֐�I'�K:�b��A�p?%�B.��xx�b�h�XI��¤s)�:���rb��'�X�W,�� {>%DA%gJ:�L�2��A���<`�L���ʀ��B�|ݐ?S�C�(�e�0
+r�OT�d�A-K�z����IM�@��w&u����6�4��+�u�,�c�����1]�����XpJ|�̨X��,��n3�$�c�{�+�Z�Y#�����š�ԝ�-����4�hl�� uO�ϖ.�+��yɱ�}���t'י�u���N�ui}���x�\殺
+��|A}_ N)�;�V.Q�"�3��8ج
+��[����1Y���pmKҟW�߅7��\_��n]� ��E�\t�E�pt-����][K����!7�{�{F�T�3�8�fG�:��<�b�>�Ew`|��G�+0�[���[�GZ��:�sK�����;�|P
+0����_V=�/���=�L�Fj��Jj�M�E�=�VqYر�
+��x;͗������u�4�e�j����F�Z3�m�f���:��1t�S{���HgY0���MB�6,||R�h9�L��u,A�\��f���Q`N}�*��AA�|y �Z�H*�m�B�7��#�杫���*�I�R��0��u�8�i]�u�]���ǹ��sm���:L��Xv=a��B��u�,���&���7��]�C���YE��ݺ�D�#��SX��_�xh`[U�z��OX��Y\_$����֊{��\�H�� O���RM 偙!1�����C�mtmR�pv5��
+:o�c=��� ��L��:�r�iy$�P����0��<��HR%WBZ�6�=�'8��J�"���d���,�R��И�������3.��/�X��0׿'sm���m[����N�� ,u�»�ݩ�ypC'�wǻ�;�aw��u���t�=�Y�9�s@瀮#Н\䷈s������}�܄m`G\P�&tn‡�/1�>K�ޠ�Њ�ͤV��m8�v�;�u8��p��47��Am=�i��HK�:�uH����&���g��N�a��ʠ,�ԫ"*PJ�h��
+�x������Aׇ��jٕ%��Ʒd��[��5��y�8_#�f �#�j�N+8vi���� s�.���0���W�1\�a`>G�pj�bk|�mk�8���xz��J�y�~��cE��\wJ��$#�4)@�Y�;׽�!��*��G�Ŵ��IX�n��曳��7߶
+��7L|SHh+�v�Hh�2,���ۥ���p
+�Զ�n��"ugQ��,|eJ�տj�Rw�r����?�-aXT`�HܽJY���-����O�X��A8ɚ 3u胪L�l�c�<��A{/�w���&v�ᙧ0��:���� �4+lֻ��j�#U�`�j���G����s�֐���}��[�X�µ���� ��˟��R)����q�e�e�)
+�q)�,�����X��T�LF��M�n�,�ܒKzsn�N��݉�eq ��afw�3�aƲ�#��:��*��� ��WU�KeA��G �K0�i���1��qRA����D�l5��&����ܮ&���}MB�٘+ՏH�%y��R},F�*u�qJ�`0�(�}��"�8��D��N$߁�ֻ+���s"��H����̷��(��ڜ���\���b��Zw�$ҫ���}]��]r��q0o@�%ð���6�lV�i�JKm�-�J�VK7 �C��8�i
+%��>ɒ�'0�����G}rX|���FI|�&9�at�I5\���]T8����K�Ψp��=N�s*�}B�T��\�t<�l��
+���8�<ʠ31���������dՖ],����Ijm>��֐|�ܓ�̴Z�۳���q0Ύ��/�jt�ڈ�yC+��?�Yx�����X��9�C�P;BP�۴A����z|����(����L��B��;���5�h|�odJ�&Y���mz�I~�Z�uX����Y�a|���n�8�6���K|/GA���+2W�a�O~ ��a��ء|�ւ�P�V]VZA�|ø9�A�7� O�ވ�:�����6���9'��nX�_a��y2;�W�
+����{��6�Ѭ@����R`g���\�K2OgF��қ�� �v�������Y�nf�q�����.�]�t�{{��$�ø��
+�s�'z����9��v���ӹsM��5}�:�nn�7�ϒ<OF��0,-�RTk��i�y��g�3\��bM��2I�?ͮ.w��]�F}���=�����9��#F��Ϣ�^�L������ዳ�����'���}�{��=�����h���GG��?~������b�����Kt������Z��ez �\���i�-�O?����N��1l�MŒ�ݦ�n��W�����%\H0�TuÜ�f��ӟA�iK܇����m������ut_���E������Ä�����rLy��W*�^��iMu�J��4g*#`�Q�7�o?�?YEQ���N] ø���^�ī:hwEc�q_���}�1������q�WM������0#���'i
+3Q�dSO���/f&���).�q�;�X�xZ��2@��"�' �������A�)��X<��n-�r�^�I��rռ]\dyz`��(8y�
+�� Jkj�L4���
+z-�|<n �"�G�ݝ�)�4��$��V��!���Y����ba��,��|/e0j��s�<}�������
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg
new file mode 100644
index 0000000000..51bfd982be
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/protocol.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 624.34646 822.34646" width="624.34646pt" height="822.34646pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2019-07-07 08:38:24 +0000</dc:date></metadata><defs><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="10" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><radialGradient cx="0" cy="0" r="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#a5a5a5"/></radialGradient><radialGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(311.81102 708.6614) scale(145.75703)"/><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_2" viewBox="-1 -6 14 12" markerWidth="14" markerHeight="12" color="black"><g><path d="M 12 0 L 0 0 M 0 -4.5 L 12 0 L 0 4.5" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_3" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.9253333 0 L 0 0 M 0 -2.222 L 5.9253333 0 L 0 2.222" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="1314" height="1698"/><g><title>Layer 1</title><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" fill="#6cf"/><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 772.02755)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="228.50753" y="12" textLength="99.91406">remote endpoint</tspan></text><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" fill="white"/><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 90.03937)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="243.79171" y="12" textLength="51.333984">websock</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="295.00851" y="12" textLength="18.128906">ets</tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="195.65109" y="25" textLength="165.62695">WebSocketCommonProtocol</tspan></text><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" fill="#6f6"/><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 35.019685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="230.0046" y="12" textLength="96.91992">application logic</tspan></text><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165" fill="#fc6"/><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165 M 238.11023 586.77165 C 228.72189 586.77165 221.10236 596.93102 221.10236 609.4488 C 221.10236 621.9666 228.72189 632.12598 238.11023 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(125.33071 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">reader</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamReader</tspan></text><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165" fill="#fc6"/><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165 M 521.5748 586.77165 C 512.18645 586.77165 504.56693 596.93102 504.56693 609.4488 C 504.56693 621.9666 512.18645 632.12598 521.5748 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(408.79527 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">writer</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamWriter</tspan></text><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756" fill="#fecc66"/><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756 M 481.88976 374.17323 C 481.88976 377.30267 469.19055 379.84252 453.5433 379.84252 C 437.89606 379.84252 425.19685 377.30267 425.19685 374.17323" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(429.19685 387.18504)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="6.343527" y="10" textLength="36.00586">pings</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="12.3445034" y="22" textLength="24.003906">dict</tspan></text><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" fill="#dadada"/><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(61.692913 288.46457)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="43.57528" y="10" textLength="129.62109">transfer_data_task</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="96.383873" y="22" textLength="24.003906">Task</tspan></text><path d="M 297.6378 765.35433 L 297.6378 609.4488 L 255.11811 609.4488 L 269.01811 609.4488 L 266.51811 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 368.50393 609.4488 L 354.60393 609.4488 L 325.98425 609.4488 L 325.98425 753.95433" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" fill="url(#Obj_Gradient)"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(217.59842 701.1614)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="69.81416" y="12" textLength="48.796875">network</tspan></text><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 460.71653)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="44.03351" y="10" textLength="72.01172">read_frame</tspan></text><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 404.02362)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="26.03058" y="10" textLength="108.01758">read_data_frame</tspan></text><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 347.3307)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="36.832338" y="10" textLength="86.41406">read_message</tspan></text><text transform="translate(178.07874 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(178.07874 433.87008)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><text transform="translate(178.07874 371.67716)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="24.003906">data</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="19" textLength="36.00586">frames</tspan></text><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" fill="#ff6"/><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 517.40945)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="40.432924" y="10" textLength="79.21289">write_frame</tspan></text><path d="M 85.03937 609.4488 L 71.13937 609.4488 L 56.692913 609.4488 L 56.692913 595.2756 L 56.692913 566.92913 L 113.385826 566.92913 L 170.07874 566.92913 L 170.07874 495.78976 L 170.07874 494.03976" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 453.5433 539.33267 L 453.5433 552.48267 L 453.5433 566.92913 L 510.23622 566.92913 L 569.76378 566.92913 L 569.76378 595.2756 L 569.76378 609.4488 L 552.48267 609.4488 L 549.98267 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="453.5433" x2="170.07874" y2="437.34685" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="396.8504" x2="170.07874" y2="380.65393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449" fill="#fc6"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449 M 238.11023 204.09449 C 228.72189 204.09449 221.10236 214.25386 221.10236 226.77165 C 221.10236 239.28945 228.72189 249.44882 238.11023 249.44882" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(132.33071 214.27165)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x=".1953125" y="10" textLength="57.609375">messages</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="13.997559" y="22" textLength="30.004883">deque</tspan></text><path d="M 255.11811 354.3307 L 269.01811 354.3307 L 297.6378 354.3307 L 297.6378 328.8189 L 297.6378 226.77165 L 269.01811 226.77165 L 266.51811 226.77165" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">recv</tspan></text><path d="M 85.03937 226.77165 L 71.13937 226.77165 L 42.519685 226.77165 L 42.519685 209.76378 L 42.519685 155.90551 L 71.13937 155.90551 L 73.63937 155.90551" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="141.73228" x2="170.07874" y2="68.092913" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="453.5433" y1="56.692913" x2="453.5433" y2="130.33228" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="467.71653" y1="56.692913" x2="467.71653" y2="187.8752" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="481.88976" y1="56.692913" x2="481.88976" y2="244.56811" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="496.063" y1="56.692913" x2="496.063" y2="300.32302" marker-end="url(#StickArrow_Marker_3)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">send</tspan></text><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 205.59842)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">ping</tspan></text><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 262.29134)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">pong</tspan></text><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 318.98425)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="62.03644" y="10" textLength="36.00586">close</tspan></text><path d="M 538.58267 155.90551 L 552.48267 155.90551 L 566.92913 155.90551 L 566.92913 481.88976 L 453.5433 481.88976 L 453.5433 496.33622 L 453.5433 498.08622" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="538.58267" y1="212.59842" x2="566.92913" y2="212.59842" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="269.29134" x2="566.92913" y2="269.29134" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="325.98425" x2="566.92913" y2="325.98425" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 255.86811 411.02362 L 262.61811 411.02362 L 340.15748 411.02362 L 340.15748 481.88976 L 453.5433 481.88976" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(291.94527 399.02362)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="42.006836">control</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="21" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="411.02362" x2="414.64685" y2="411.02362" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><path d="M 368.50393 212.59842 L 361.75393 212.59842 L 340.15748 212.59842 L 340.15748 340.15748 L 340.15748 382.67716" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(461.5433 547.2559)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(461.5433 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="382.67716" x2="414.64685" y2="382.67716" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></g></g></svg>
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst
new file mode 100644
index 0000000000..d3dec21bd1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/security.rst
@@ -0,0 +1,41 @@
+Security
+========
+
+Encryption
+----------
+
+For production use, a server should require encrypted connections.
+
+See this example of :ref:`encrypting connections with TLS
+<secure-server-example>`.
+
+Memory usage
+------------
+
+.. warning::
+
+ An attacker who can open an arbitrary number of connections will be able
+ to perform a denial of service by memory exhaustion. If you're concerned
+ by denial of service attacks, you must reject suspicious connections
+ before they reach websockets, typically in a reverse proxy.
+
+With the default settings, opening a connection uses 70 KiB of memory.
+
+Sending some highly compressed messages could use up to 128 MiB of memory with
+an amplification factor of 1000 between network traffic and memory usage.
+
+Configuring a server to :doc:`optimize memory usage <memory>` will improve
+security in addition to improving performance.
+
+Other limits
+------------
+
+websockets implements additional limits on the amount of data it accepts in
+order to minimize exposure to security vulnerabilities.
+
+In the opening handshake, websockets limits the number of HTTP headers to 256
+and the size of an individual header to 4096 bytes. These limits are 10 to 20
+times larger than what's expected in standard use cases. They're hard-coded.
+
+If you need to change these limits, you can monkey-patch the constants in
+``websockets.http11``.
diff --git a/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst
new file mode 100644
index 0000000000..633fc1ab43
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/websockets/docs/topics/timeouts.rst
@@ -0,0 +1,116 @@
+Timeouts
+========
+
+.. currentmodule:: websockets
+
+Long-lived connections
+----------------------
+
+Since the WebSocket protocol is intended for real-time communications over
+long-lived connections, it is desirable to ensure that connections don't
+break, and if they do, to report the problem quickly.
+
+Connections can drop as a consequence of temporary network connectivity issues,
+which are very common, even within data centers.
+
+Furthermore, WebSocket builds on top of HTTP/1.1 where connections are
+short-lived, even with ``Connection: keep-alive``. Typically, HTTP/1.1
+infrastructure closes idle connections after 30 to 120 seconds.
+
+As a consequence, proxies may terminate WebSocket connections prematurely when
+no message was exchanged in 30 seconds.
+
+.. _keepalive:
+
+Keepalive in websockets
+-----------------------
+
+To avoid these problems, websockets runs a keepalive and heartbeat mechanism
+based on WebSocket Ping_ and Pong_ frames, which are designed for this purpose.
+
+.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2
+.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3
+
+It loops through these steps:
+
+1. Wait 20 seconds.
+2. Send a Ping frame.
+3. Receive a corresponding Pong frame within 20 seconds.
+
+If the Pong frame isn't received, websockets considers the connection broken and
+closes it.
+
+This mechanism serves two purposes:
+
+1. It creates a trickle of traffic so that the TCP connection isn't idle and
+ network infrastructure along the path keeps it open ("keepalive").
+2. It detects if the connection drops or becomes so slow that it's unusable in
+ practice ("heartbeat"). In that case, it terminates the connection and your
+ application gets a :exc:`~exceptions.ConnectionClosed` exception.
+
+Timings are configurable with the ``ping_interval`` and ``ping_timeout``
+arguments of :func:`~client.connect` and :func:`~server.serve`. Shorter values
+will detect connection drops faster but they will increase network traffic and
+they will be more sensitive to latency.
+
+Setting ``ping_interval`` to :obj:`None` disables the whole keepalive and
+heartbeat mechanism.
+
+Setting ``ping_timeout`` to :obj:`None` disables only timeouts. This enables
+keepalive, to keep idle connections open, and disables heartbeat, to support large
+latency spikes.
+
+.. admonition:: Why doesn't websockets rely on TCP keepalive?
+ :class: hint
+
+ TCP keepalive is disabled by default on most operating systems. When
+ enabled, the default interval is two hours or more, which is far too much.
+
+Keepalive in browsers
+---------------------
+
+Browsers don't enable a keepalive mechanism like websockets by default. As a
+consequence, they can fail to notice that a WebSocket connection is broken for
+an extended period of time, until the TCP connection times out.
+
+In this scenario, the ``WebSocket`` object in the browser doesn't fire a
+``close`` event. If you have a reconnection mechanism, it doesn't kick in
+because it believes that the connection is still working.
+
+If your browser-based app mysteriously and randomly fails to receive events,
+this is a likely cause. You need a keepalive mechanism in the browser to avoid
+this scenario.
+
+Unfortunately, the WebSocket API in browsers doesn't expose the native Ping and
+Pong functionality in the WebSocket protocol. You have to roll your own in the
+application layer.
+
+Latency issues
+--------------
+
+Latency between a client and a server may increase for two reasons:
+
+* Network connectivity is poor. When network packets are lost, TCP attempts to
+ retransmit them, which manifests as latency. Excessive packet loss makes
+ the connection unusable in practice. At some point, timing out is a
+ reasonable choice.
+
+* Traffic is high. For example, if a client sends messages on the connection
+ faster than a server can process them, this manifests as latency as well,
+ because data is waiting in flight, mostly in OS buffers.
+
+ If the server is more than 20 seconds behind, it doesn't see the Pong before
+ the default timeout elapses. As a consequence, it closes the connection.
+ This is a reasonable choice to prevent overload.
+
+ If traffic spikes cause unwanted timeouts and you're confident that the server
+ will catch up eventually, you can increase ``ping_timeout`` or you can set it
+ to :obj:`None` to disable heartbeat entirely.
+
+ The same reasoning applies to situations where the server sends more traffic
+ than the client can accept.
+
+The latency measured during the last exchange of Ping and Pong frames is
+available in the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency`
+attribute. Alternatively, you can measure the latency at any time with the
+:attr:`~legacy.protocol.WebSocketCommonProtocol.ping` method.