summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party/websockets/docs/howto
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/websockets/docs/howto')
-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
15 files changed, 2123 insertions, 0 deletions
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.
+