diff --git a/docs/conf.py b/docs/conf.py index d5f527bf6..d9e3cd598 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,6 +31,28 @@ # -- 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. @@ -38,7 +60,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.linkcode", - "sphinx_autodoc_typehints", + "sphinx.ext.napoleon", "sphinx_copybutton", "sphinx_inline_tabs", "sphinxcontrib.spelling", @@ -46,6 +68,15 @@ "sphinxext.opengraph", ] +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 diff --git a/docs/howto/cheatsheet.rst b/docs/howto/cheatsheet.rst index edfb00baa..95b551f67 100644 --- a/docs/howto/cheatsheet.rst +++ b/docs/howto/cheatsheet.rst @@ -26,27 +26,27 @@ Server :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't needed in general. -* Create a server with :func:`~legacy.server.serve` which is similar to asyncio's - :meth:`~asyncio.AbstractEventLoop.create_server`. You can also use it as an - asynchronous context manager. +* 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:`~legacy.server.WebSocketServerProtocol` and pass either this subclass or + :class:`~server.WebSocketServerProtocol` and pass either this subclass or a factory function as the ``create_protocol`` argument. Client ------ -* Create a client with :func:`~legacy.client.connect` which is similar to asyncio's - :meth:`~asyncio.BaseEventLoop.create_connection`. You can also use it as an +* 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:`~legacy.server.WebSocketClientProtocol` and pass either this subclass or + :class:`~client.WebSocketClientProtocol` and pass either this subclass or a factory function as the ``create_protocol`` argument. * Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and @@ -57,7 +57,7 @@ Client :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't needed in general. -* If you aren't using :func:`~legacy.client.connect` as a context manager, call +* If you aren't using :func:`~client.connect` as a context manager, call :meth:`~legacy.protocol.WebSocketCommonProtocol.close` to terminate the connection. .. _debugging: diff --git a/docs/howto/django.rst b/docs/howto/django.rst index c87c3821c..67bf582f1 100644 --- a/docs/howto/django.rst +++ b/docs/howto/django.rst @@ -124,7 +124,7 @@ 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.serve`. +Finally, we start a server with :func:`~websockets.server.serve`. We're ready to test! diff --git a/docs/howto/extensions.rst b/docs/howto/extensions.rst index 9c49de172..2baead3f0 100644 --- a/docs/howto/extensions.rst +++ b/docs/howto/extensions.rst @@ -4,8 +4,10 @@ Writing 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_ 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: @@ -23,11 +25,8 @@ As a consequence, writing an extension requires implementing several classes: Extensions are initialized by extension factories, so they don't need to be part of the public API of an extension. -websockets provides abstract base classes for extension factories and -extensions. See the API documentation for details on their methods: - -* :class:`ClientExtensionFactory` and :class:`ServerExtensionFactory` for - extension factories, -* :class:`Extension` for extensions. +websockets provides base classes for extension factories and extensions. +See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`, +and :class:`Extension` for details. diff --git a/docs/howto/faq.rst b/docs/howto/faq.rst index d43d5fd67..ec657bb06 100644 --- a/docs/howto/faq.rst +++ b/docs/howto/faq.rst @@ -5,7 +5,7 @@ FAQ .. note:: - Many questions asked in :mod:`websockets`' issue tracker are actually + Many questions asked in websockets' issue tracker are actually about :mod:`asyncio`. Python's documentation about `developing with asyncio`_ is a good complement. @@ -99,13 +99,13 @@ How do I get access HTTP headers, for example cookies? ...................................................... To access HTTP headers during the WebSocket handshake, you can override -:attr:`~legacy.server.WebSocketServerProtocol.process_request`:: +:attr:`~server.WebSocketServerProtocol.process_request`:: async def process_request(self, path, request_headers): cookies = request_header["Cookie"] Once the connection is established, they're available in -:attr:`~legacy.protocol.WebSocketServerProtocol.request_headers`:: +:attr:`~server.WebSocketServerProtocol.request_headers`:: async def handler(websocket, path): cookies = websocket.request_headers["Cookie"] @@ -123,7 +123,7 @@ How do I set which IP addresses my server listens to? Look at the ``host`` argument of :meth:`~asyncio.loop.create_server`. -:func:`serve` accepts the same arguments as +:func:`~server.serve` accepts the same arguments as :meth:`~asyncio.loop.create_server`. How do I close a connection properly? @@ -143,7 +143,7 @@ Providing a 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:`~legacy.server.WebSocketServerProtocol.process_request` hook. +:attr:`~server.WebSocketServerProtocol.process_request` hook. If you need more, pick a HTTP server and run it separately. @@ -169,7 +169,7 @@ change it to:: How do I close a connection properly? ..................................... -The easiest is to use :func:`connect` as a context manager:: +The easiest is to use :func:`~client.connect` as a context manager:: async with connect(...) as websocket: ... @@ -196,7 +196,7 @@ How do I disable TLS/SSL certificate verification? Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`. -:func:`connect` accepts the same arguments as +:func:`~client.connect` accepts the same arguments as :meth:`~asyncio.loop.create_connection`. asyncio usage @@ -449,4 +449,4 @@ I'm having problems with threads You shouldn't use threads. Use tasks instead. -:func:`~asyncio.AbstractEventLoop.call_soon_threadsafe` may help. +:meth:`~asyncio.loop.call_soon_threadsafe` may help. diff --git a/docs/intro/index.rst b/docs/intro/index.rst index ff3ae0ffd..c8426719c 100644 --- a/docs/intro/index.rst +++ b/docs/intro/index.rst @@ -82,7 +82,7 @@ This client needs a context because the server uses a self-signed certificate. A client connecting to a secure WebSocket server with a valid certificate (i.e. signed by a CA that your Python installation trusts) can simply pass -``ssl=True`` to :func:`connect` instead of building a context. +``ssl=True`` to :func:`~client.connect` instead of building a context. Browser-based example --------------------- diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index b9329d7b5..073514ecd 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -37,10 +37,10 @@ They may change at any time. .. note:: **Version 10.0 enables a timeout of 10 seconds on** - :func:`~legacy.client.connect` **by default.** + :func:`~client.connect` **by default.** You can adjust the timeout with the ``open_timeout`` parameter. Set it to - ``None`` to disable the timeout entirely. + :obj:`None` to disable the timeout entirely. .. note:: @@ -51,7 +51,8 @@ They may change at any time. .. note:: - **Version 10.0 changes parameters of** ``ConnectionClosed.__init__`` **.** + **Version 10.0 changes arguments of** + :exc:`~exceptions.ConnectionClosed` **.** If you raise :exc:`~exceptions.ConnectionClosed` or a subclass — rather than catch them when websockets raises them — you must change your code. @@ -70,27 +71,30 @@ Also: * Added :func:`~websockets.broadcast` to send a message to many clients. * Added support for reconnecting automatically by using - :func:`~legacy.client.connect` as an asynchronous iterator. + :func:`~client.connect` as an asynchronous iterator. -* Added ``open_timeout`` to :func:`~legacy.client.connect`. +* Added ``open_timeout`` to :func:`~client.connect`. * Improved logging. -* Provided additional information in :exc:`ConnectionClosed` exceptions. +* Provided additional information in :exc:`~exceptions.ConnectionClosed` + exceptions. * Optimized default compression settings to reduce memory usage. * Made it easier to customize authentication with :meth:`~auth.BasicAuthWebSocketServerProtocol.check_credentials`. -* Fixed handling of relative redirects in :func:`~legacy.client.connect`. +* Fixed handling of relative redirects in :func:`~client.connect`. + +* Improved API documentation. 9.1 ... *May 27, 2021* -.. note:: +.. caution:: **Version 9.1 fixes a security issue introduced in version 8.0.** @@ -197,7 +201,7 @@ Also: *July 31, 2019* * Restored the ability to pass a socket with the ``sock`` parameter of - :func:`~legacy.server.serve`. + :func:`~server.serve`. * Removed an incorrect assertion when a connection drops. @@ -224,9 +228,9 @@ Also: Previously, it could be a function or a coroutine. If you're passing a ``process_request`` argument to - :func:`~legacy.server.serve` - or :class:`~legacy.server.WebSocketServerProtocol`, or if you're overriding - :meth:`~legacy.server.WebSocketServerProtocol.process_request` in a subclass, + :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``. For backwards compatibility, functions are still mostly supported, but @@ -274,15 +278,15 @@ Also: :exc:`~exceptions.ConnectionClosed` to tell apart normal connection termination from errors. -* Added :func:`~legacy.auth.basic_auth_protocol_factory` to enforce HTTP +* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP Basic Auth on the server side. -* :func:`~legacy.client.connect` handles redirects from the server during the +* :func:`~client.connect` handles redirects from the server during the handshake. -* :func:`~legacy.client.connect` supports overriding ``host`` and ``port``. +* :func:`~client.connect` supports overriding ``host`` and ``port``. -* Added :func:`~legacy.client.unix_connect` for connecting to Unix sockets. +* Added :func:`~client.unix_connect` for connecting to Unix sockets. * Improved support for sending fragmented messages by accepting asynchronous iterators in :meth:`~legacy.protocol.WebSocketCommonProtocol.send`. @@ -292,10 +296,10 @@ Also: as a workaround, you can remove it. * Changed :meth:`WebSocketServer.close() - ` to perform a proper closing handshake + ` to perform a proper closing handshake instead of failing the connection. -* Avoided a crash when a ``extra_headers`` callable returns ``None``. +* Avoided a crash when a ``extra_headers`` callable returns :obj:`None`. * Improved error messages when HTTP parsing fails. @@ -327,7 +331,7 @@ Also: **Version 7.0 changes how a server terminates connections when it's closed with** :meth:`WebSocketServer.close() - ` **.** + ` **.** Previously, connections handlers were canceled. Now, connections are closed with close code 1001 (going away). From the perspective of the @@ -345,7 +349,7 @@ Also: .. note:: **Version 7.0 renames the** ``timeout`` **argument of** - :func:`~legacy.server.serve` **and** :func:`~legacy.client.connect` **to** + :func:`~server.serve` **and** :func:`~client.connect` **to** ``close_timeout`` **.** This prevents confusion with ``ping_timeout``. @@ -375,11 +379,11 @@ Also: Also: * Added ``process_request`` and ``select_subprotocol`` arguments to - :func:`~legacy.server.serve` and - :class:`~legacy.server.WebSocketServerProtocol` to customize - :meth:`~legacy.server.WebSocketServerProtocol.process_request` and - :meth:`~legacy.server.WebSocketServerProtocol.select_subprotocol` without - subclassing :class:`~legacy.server.WebSocketServerProtocol`. + :func:`~server.serve` and + :class:`~server.WebSocketServerProtocol` to customize + :meth:`~server.WebSocketServerProtocol.process_request` and + :meth:`~server.WebSocketServerProtocol.select_subprotocol` without + subclassing :class:`~server.WebSocketServerProtocol`. * Added support for sending fragmented messages. @@ -389,7 +393,7 @@ Also: * Added an interactive client: ``python -m websockets ``. * Changed the ``origins`` argument to represent the lack of an origin with - ``None`` rather than ``''``. + :obj:`None` rather than ``''``. * Fixed a data loss bug in :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`: @@ -409,7 +413,7 @@ Also: **Version 6.0 introduces the** :class:`~datastructures.Headers` **class for managing HTTP headers and changes several public APIs:** - * :meth:`~legacy.server.WebSocketServerProtocol.process_request` now + * :meth:`~server.WebSocketServerProtocol.process_request` now receives a :class:`~datastructures.Headers` instead of a ``http.client.HTTPMessage`` in the ``request_headers`` argument. @@ -445,14 +449,14 @@ Also: *May 24, 2018* * Fixed a regression in 5.0 that broke some invocations of - :func:`~legacy.server.serve` and :func:`~legacy.client.connect`. + :func:`~server.serve` and :func:`~client.connect`. 5.0 ... *May 22, 2018* -.. note:: +.. caution:: **Version 5.0 fixes a security issue introduced in version 4.0.** @@ -465,14 +469,14 @@ Also: .. note:: **Version 5.0 adds a** ``user_info`` **field to the return value of** - :func:`~uri.parse_uri` **and** :class:`~uri.WebSocketURI` **.** + ``parse_uri`` **and** ``WebSocketURI`` **.** - If you're unpacking :class:`~uri.WebSocketURI` into four variables, adjust - your code to account for that fifth field. + If you're unpacking ``WebSocketURI`` into four variables, adjust your code + to account for that fifth field. Also: -* :func:`~legacy.client.connect` performs HTTP Basic Auth when the URI contains +* :func:`~client.connect` performs HTTP Basic Auth when the URI contains credentials. * Iterating on incoming messages no longer raises an exception when the @@ -481,7 +485,7 @@ Also: * A plain HTTP request now receives a 426 Upgrade Required response and doesn't log a stack trace. -* :func:`~legacy.server.unix_serve` can be used as an asynchronous context +* :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 @@ -536,7 +540,7 @@ Also: Compression should improve performance but it increases RAM and CPU use. If you want to disable compression, add ``compression=None`` when calling - :func:`~legacy.server.serve` or :func:`~legacy.client.connect`. + :func:`~server.serve` or :func:`~client.connect`. .. note:: @@ -549,10 +553,10 @@ Also: * :class:`~legacy.protocol.WebSocketCommonProtocol` instances can be used as asynchronous iterators on Python ≥ 3.6. They yield incoming messages. -* Added :func:`~legacy.server.unix_serve` for listening on Unix sockets. +* Added :func:`~server.unix_serve` for listening on Unix sockets. -* Added the :attr:`~legacy.server.WebSocketServer.sockets` attribute to the - return value of :func:`~legacy.server.serve`. +* Added the :attr:`~server.WebSocketServer.sockets` attribute to the + return value of :func:`~server.serve`. * Reorganized and extended documentation. @@ -572,15 +576,15 @@ Also: *August 20, 2017* -* Renamed :func:`~legacy.server.serve` and :func:`~legacy.client.connect`'s +* 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. -* :func:`~legacy.server.serve` can be used as an asynchronous context manager +* :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:`~legacy.server.WebSocketServerProtocol.process_request`. + :meth:`~server.WebSocketServerProtocol.process_request`. * Made read and write buffer sizes configurable. @@ -588,10 +592,10 @@ Also: * Added an optional C extension to speed up low-level operations. -* An invalid response status code during :func:`~legacy.client.connect` now +* An invalid response status code during :func:`~client.connect` now raises :class:`~exceptions.InvalidStatusCode`. -* Providing a ``sock`` argument to :func:`~legacy.client.connect` no longer +* Providing a ``sock`` argument to :func:`~client.connect` no longer crashes. 3.3 @@ -611,7 +615,7 @@ Also: *August 17, 2016* * Added ``timeout``, ``max_size``, and ``max_queue`` arguments to - :func:`~legacy.client.connect` and :func:`~legacy.server.serve`. + :func:`~client.connect` and :func:`~server.serve`. * Made server shutdown more robust. @@ -637,8 +641,8 @@ Also: **If you're upgrading from 2.x or earlier, please read this carefully.** :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` used to return - ``None`` when the connection was closed. This required checking the return - value of every call:: + :obj:`None` when the connection was closed. This required checking the + return value of every call:: message = await websocket.recv() if message is None: @@ -655,14 +659,14 @@ Also: In order to avoid stranding projects built upon an earlier version, the previous behavior can be restored by passing ``legacy_recv=True`` to - :func:`~legacy.server.serve`, :func:`~legacy.client.connect`, - :class:`~legacy.server.WebSocketServerProtocol`, or - :class:`~legacy.client.WebSocketClientProtocol`. ``legacy_recv`` isn't + :func:`~server.serve`, :func:`~client.connect`, + :class:`~server.WebSocketServerProtocol`, or + :class:`~client.WebSocketClientProtocol`. ``legacy_recv`` isn't documented in their signatures but isn't scheduled for deprecation either. Also: -* :func:`~legacy.client.connect` can be used as an asynchronous context +* :func:`~client.connect` can be used as an asynchronous context manager on Python ≥ 3.5.1. * Updated documentation with ``await`` and ``async`` syntax from Python 3.5. @@ -732,8 +736,8 @@ Also: * Added support for subprotocols. -* Added ``loop`` argument to :func:`~legacy.client.connect` and - :func:`~legacy.server.serve`. +* Added ``loop`` argument to :func:`~client.connect` and + :func:`~server.serve`. 2.3 ... diff --git a/docs/reference/client.rst b/docs/reference/client.rst index 84f66a19a..eaa6cdd76 100644 --- a/docs/reference/client.rst +++ b/docs/reference/client.rst @@ -6,67 +6,55 @@ Client Opening a connection -------------------- - .. autofunction:: connect(uri, *, create_protocol=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, compression='deflate', origin=None, extensions=None, subprotocols=None, extra_headers=None, logger=None, **kwds) + .. autofunction:: connect(uri, *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, 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, 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, compression='deflate', origin=None, extensions=None, subprotocols=None, extra_headers=None, logger=None, **kwds) + .. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, 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(*, 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, origin=None, extensions=None, subprotocols=None, extra_headers=None, logger=None) + .. autoclass:: WebSocketClientProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=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) - .. attribute:: id - - UUID for the connection. - - Useful for identifying connections in logs. - - .. autoattribute:: local_address - - .. autoattribute:: remote_address - - .. autoattribute:: open - - .. autoattribute:: closed - - .. attribute:: path + .. automethod:: recv - Path of the HTTP request. + .. automethod:: send - Available once the connection is open. + .. automethod:: close - .. attribute:: request_headers + .. automethod:: wait_closed - HTTP request headers as a :class:`~websockets.http.Headers` instance. + .. automethod:: ping - Available once the connection is open. + .. automethod:: pong - .. attribute:: response_headers + WebSocket connection objects also provide these attributes: - HTTP response headers as a :class:`~websockets.http.Headers` instance. + .. autoattribute:: id - Available once the connection is open. + .. autoproperty:: local_address - .. attribute:: subprotocol + .. autoproperty:: remote_address - Subprotocol, if one was negotiated. + .. autoproperty:: open - Available once the connection is open. + .. autoproperty:: closed - .. autoattribute:: close_code + The following attributes are available after the opening handshake, + once the WebSocket connection is open: - .. autoattribute:: close_reason + .. autoattribute:: path - .. automethod:: recv + .. autoattribute:: request_headers - .. automethod:: send + .. autoattribute:: response_headers - .. automethod:: ping + .. autoattribute:: subprotocol - .. automethod:: pong + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: - .. automethod:: close + .. autoproperty:: close_code - .. automethod:: wait_closed + .. autoproperty:: close_reason diff --git a/docs/reference/common.rst b/docs/reference/common.rst new file mode 100644 index 000000000..3b9f34a57 --- /dev/null +++ b/docs/reference/common.rst @@ -0,0 +1,51 @@ +Both sides +========== + +.. automodule:: websockets.legacy.protocol + + Using a connection + ------------------ + + .. 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 + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + 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/docs/reference/exceptions.rst b/docs/reference/exceptions.rst new file mode 100644 index 000000000..907a650d2 --- /dev/null +++ b/docs/reference/exceptions.rst @@ -0,0 +1,6 @@ +Exceptions +========== + +.. automodule:: websockets.exceptions + :members: + diff --git a/docs/reference/extensions.rst b/docs/reference/extensions.rst index bae583a21..a70f1b1e5 100644 --- a/docs/reference/extensions.rst +++ b/docs/reference/extensions.rst @@ -6,7 +6,7 @@ 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, specified in :rfc:`7692`. +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 @@ -16,21 +16,45 @@ 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 -Abstract classes ----------------- +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 - :members: + + .. autoattribute:: name + + .. automethod:: decode + + .. automethod:: encode .. autoclass:: ClientExtensionFactory - :members: + + .. autoattribute:: name + + .. automethod:: get_request_params + + .. automethod:: process_response_params .. autoclass:: ServerExtensionFactory - :members: + .. automethod:: process_request_params diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 8d01c5b40..385beab29 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -1,24 +1,27 @@ API reference ============= -websockets provides complete client and server implementations, as shown in +.. currentmodule:: websockets + +websockets provides client and server implementations, as shown in the :doc:`getting started guide <../intro/index>`. The process for opening and closing a WebSocket connection depends on which side you're implementing. -* On the client side, connecting to a server with :class:`~websockets.connect` +* On the client side, connecting to a server with :func:`~client.connect` yields a connection object that provides methods for interacting with the connection. Your code can open a connection, then send or receive messages. - If you use :class:`~websockets.connect` as an asynchronous context manager, + If you use :func:`~client.connect` as an asynchronous context manager, then websockets closes the connection on exit. If not, then your code is responsible for closing the connection. -* On the server side, :class:`~websockets.serve` starts listening for client - connections and yields an server object that supports closing the server. +* On the server side, :func:`~server.serve` starts listening for client + connections and yields an server object that you can use to shut down + the server. - Then, when clients connects, the server initializes a connection object and + Then, when a client connects, the server initializes a connection object and passes it to a handler coroutine, which is where your code can send or receive messages. This pattern is called `inversion of control`_. It's common in frameworks implementing servers. @@ -29,28 +32,35 @@ side you're implementing. .. _inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control Once the connection is open, the WebSocket protocol is symmetrical, except for -low-level details that websockets manages under the hood. The same methods are -available on client connections created with :class:`~websockets.connect` and -on server connections passed to the connection handler in the arguments. +low-level details that websockets manages under the hood. The same methods +are available on client connections created with :func:`~client.connect` and +on server connections received in argument by the connection handler +of :func:`~server.serve`. -At this point, websockets provides the same API — and uses the same code — for -client and server connections. For convenience, common methods are documented -both in the client API and server API. +Since websockets provides the same API — and uses the same code — for client +and server connections, common methods are documented in a "Both sides" page. .. toctree:: :titlesonly: client server - extensions + common utilities + exceptions + types + extensions limitations -All public APIs can be imported from the :mod:`websockets` package, unless -noted otherwise. This convenience feature is incompatible with static code -analysis tools such as mypy_, though. +Public API documented in the API reference are subject to the +:ref:`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. + +For convenience, many public APIs can be imported from the ``websockets`` +package. This feature is incompatible with static code analysis tools such as +mypy_, though. If you're using such tools, use the full import path. .. _mypy: https://github.com/python/mypy -Anything that isn't listed in this API documentation is a private API. There's -no guarantees of behavior or backwards-compatibility for private APIs. diff --git a/docs/reference/limitations.rst b/docs/reference/limitations.rst index 81f1445b5..3304bdb8c 100644 --- a/docs/reference/limitations.rst +++ b/docs/reference/limitations.rst @@ -1,12 +1,14 @@ Limitations =========== +.. currentmodule:: websockets + Client ------ The client 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:`~websockets.connect()` isn't the +`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/docs/reference/server.rst b/docs/reference/server.rst index 667c0b9d0..0a5a060f3 100644 --- a/docs/reference/server.rst +++ b/docs/reference/server.rst @@ -6,10 +6,10 @@ Server Starting a server ----------------- - .. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=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, compression='deflate', origins=None, extensions=None, subprotocols=None, extra_headers=None, process_request=None, select_subprotocol=None, logger=None, **kwds) + .. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, 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, *, create_protocol=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, compression='deflate', origins=None, extensions=None, subprotocols=None, extra_headers=None, process_request=None, select_subprotocol=None, logger=None, **kwds) + .. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, 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 @@ -17,92 +17,80 @@ Server .. autoclass:: WebSocketServer - .. autoattribute:: sockets - .. automethod:: close + .. automethod:: wait_closed + .. autoattribute:: sockets + Using a connection ------------------ - .. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, 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, origins=None, extensions=None, subprotocols=None, extra_headers=None, process_request=None, select_subprotocol=None, logger=None) - - .. attribute:: id - - UUID for the connection. - - Useful for identifying connections in logs. + .. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, 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) - .. autoattribute:: local_address - - .. autoattribute:: remote_address - - .. autoattribute:: open + .. automethod:: recv - .. autoattribute:: closed + .. automethod:: send - .. attribute:: path + .. automethod:: close - Path of the HTTP request. + .. automethod:: wait_closed - Available once the connection is open. + .. automethod:: ping - .. attribute:: request_headers + .. automethod:: pong - HTTP request headers as a :class:`~websockets.http.Headers` instance. + You can customize the opening handshake in a subclass by overriding these methods: - Available once the connection is open. + .. automethod:: process_request - .. attribute:: response_headers + .. automethod:: select_subprotocol - HTTP response headers as a :class:`~websockets.http.Headers` instance. + WebSocket connection objects also provide these attributes: - Available once the connection is open. + .. autoattribute:: id - .. attribute:: subprotocol + .. autoproperty:: local_address - Subprotocol, if one was negotiated. + .. autoproperty:: remote_address - Available once the connection is open. + .. autoproperty:: open - .. autoattribute:: close_code + .. autoproperty:: closed - .. autoattribute:: close_reason + The following attributes are available after the opening handshake, + once the WebSocket connection is open: - .. automethod:: process_request + .. autoattribute:: path - .. automethod:: select_subprotocol + .. autoattribute:: request_headers - .. automethod:: recv + .. autoattribute:: response_headers - .. automethod:: send + .. autoattribute:: subprotocol - .. automethod:: ping + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: - .. automethod:: pong + .. autoproperty:: close_code - .. automethod:: close + .. autoproperty:: close_reason - .. automethod:: wait_closed Basic authentication -------------------- .. automodule:: websockets.auth + websockets supports HTTP Basic Authentication according to + :rfc:`7235` and :rfc:`7617`. + .. autofunction:: basic_auth_protocol_factory .. autoclass:: BasicAuthWebSocketServerProtocol - .. attribute:: realm - - Scope of protection. - - If provided, it should contain only ASCII characters because the - encoding of non-ASCII characters is undefined. - - .. attribute:: username + .. autoattribute:: realm - Username of the authenticated user. + .. autoattribute:: username .. automethod:: check_credentials diff --git a/docs/reference/types.rst b/docs/reference/types.rst new file mode 100644 index 000000000..3dab553af --- /dev/null +++ b/docs/reference/types.rst @@ -0,0 +1,20 @@ +Types +===== + +.. autodata:: websockets.datastructures.HeadersLike + +.. automodule:: websockets.typing + + .. autodata:: Data + + .. autodata:: LoggerLike + + .. autodata:: Origin + + .. autodata:: Subprotocol + + .. autodata:: ExtensionName + + .. autodata:: ExtensionParameter + + diff --git a/docs/reference/utilities.rst b/docs/reference/utilities.rst index e7f489fbd..dc6333847 100644 --- a/docs/reference/utilities.rst +++ b/docs/reference/utilities.rst @@ -6,36 +6,32 @@ Broadcast .. autofunction:: websockets.broadcast -Data structures ---------------- +WebSocket events +---------------- -.. automodule:: websockets.datastructures - - .. autoclass:: Headers +.. automodule:: websockets.frames - .. autodata:: HeadersLike + .. autoclass:: Frame - .. autoexception:: MultipleValuesError + .. autoclass:: Opcode -Exceptions ----------- + .. autoclass:: Close -.. automodule:: websockets.exceptions - :members: +HTTP events +----------- -Types ------ +.. automodule:: websockets.http11 -.. automodule:: websockets.typing + .. autoclass:: Request - .. autodata:: Data + .. autoclass:: Response - .. autodata:: LoggerLike +.. automodule:: websockets.datastructures - .. autodata:: Origin + .. autoclass:: Headers - .. autodata:: Subprotocol + .. automethod:: get_all - .. autodata:: ExtensionName + .. automethod:: raw_items - .. autodata:: ExtensionParameter + .. autoexception:: MultipleValuesError diff --git a/docs/requirements.txt b/docs/requirements.txt index b9c371228..bcd1d7114 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,6 @@ furo sphinx sphinx-autobuild -sphinx-autodoc-typehints sphinx-copybutton sphinx-inline-tabs sphinxcontrib-spelling diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 3d05752d5..b57d3c77f 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -35,6 +35,7 @@ linkerd liveness lookups MiB +mypy nginx permessage pid @@ -59,6 +60,7 @@ tox unregister uple uvicorn +uvloop virtualenv WebSocket websocket diff --git a/docs/topics/broadcast.rst b/docs/topics/broadcast.rst index f9cd9e281..a90cc2d70 100644 --- a/docs/topics/broadcast.rst +++ b/docs/topics/broadcast.rst @@ -1,13 +1,13 @@ Broadcasting messages ===================== -.. currentmodule: websockets +.. currentmodule:: websockets .. note:: If you just want to send a message to all connected clients, use - :func:`~websockets.broadcast`. + :func:`broadcast`. If you want to learn about its design in depth, continue reading this document. @@ -16,7 +16,7 @@ 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:`~websockets.broadcast`, and discuss alternatives. +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 @@ -122,7 +122,7 @@ 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:`~websockets.broadcast` function instead. +built-in :func:`broadcast` function instead. The concurrent way ------------------ @@ -207,11 +207,11 @@ 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:`~websockets.broadcast` works ---------------------------------------- +How :func:`broadcast` works +--------------------------- -The built-in :func:`~websockets.broadcast` function is similar to the naive -way. The main difference is that it doesn't apply backpressure. +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. @@ -321,9 +321,9 @@ the asynchronous iterator returned by ``subscribe()``. Performance considerations -------------------------- -The built-in :func:`~websockets.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. +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. @@ -343,7 +343,7 @@ However, this isn't possible in general for two reasons: 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:`~websockets.broadcast` function. +than the built-in :func:`broadcast` function. There is no major difference between the performance of per-message queues and publish–subscribe. diff --git a/docs/topics/compression.rst b/docs/topics/compression.rst index f78e32748..d40c4257d 100644 --- a/docs/topics/compression.rst +++ b/docs/topics/compression.rst @@ -1,6 +1,8 @@ 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. @@ -29,9 +31,8 @@ If you want to disable compression, set ``compression=None``:: websockets.serve(..., compression=None) If you want to customize compression settings, you can enable the Per-Message -Deflate extension explicitly with -:class:`~permessage_deflate.ClientPerMessageDeflateFactory` or -:class:`~permessage_deflate.ServerPerMessageDeflateFactory`:: +Deflate extension explicitly with :class:`ClientPerMessageDeflateFactory` or +:class:`ServerPerMessageDeflateFactory`:: import websockets from websockets.extensions import permessage_deflate diff --git a/docs/topics/deployment.rst b/docs/topics/deployment.rst index d30c5568e..ac0a8ed4c 100644 --- a/docs/topics/deployment.rst +++ b/docs/topics/deployment.rst @@ -78,7 +78,7 @@ Option 2 almost always combines with option 3. How do I start a process? ......................... -Run a Python program that invokes :func:`~serve`. That's it. +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. diff --git a/docs/topics/design.rst b/docs/topics/design.rst index 2c9d505aa..b5c55afc9 100644 --- a/docs/topics/design.rst +++ b/docs/topics/design.rst @@ -35,7 +35,7 @@ Transitions happen in the following places: :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` which runs when the :ref:`opening handshake ` completes and the WebSocket connection is established — not to be confused with - :meth:`~asyncio.Protocol.connection_made` which runs when the TCP connection + :meth:`~asyncio.BaseProtocol.connection_made` which runs when the TCP connection is established; - ``OPEN -> CLOSING``: in :meth:`~legacy.protocol.WebSocketCommonProtocol.write_frame` immediately before @@ -58,7 +58,7 @@ connection lifecycle on the client side. :target: _images/lifecycle.svg The lifecycle is identical on the server side, except inversion of control -makes the equivalent of :meth:`~legacy.client.connect` implicit. +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. @@ -113,7 +113,7 @@ Opening handshake ----------------- websockets performs the opening handshake when establishing a WebSocket -connection. On the client side, :meth:`~legacy.client.connect` executes it +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. @@ -123,26 +123,26 @@ 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:`~legacy.client.WebSocketClientProtocol.handshake`: +On the client side, :meth:`~client.WebSocketClientProtocol.handshake`: - builds a HTTP request based on the ``uri`` and parameters passed to - :meth:`~legacy.client.connect`; + :meth:`~client.connect`; - writes the HTTP request to the network; - reads a 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:`~legacy.server.WebSocketServerProtocol.handshake`: +On the server side, :meth:`~server.WebSocketServerProtocol.handshake`: - reads a HTTP request from the network; -- calls :meth:`~legacy.server.WebSocketServerProtocol.process_request` which may +- calls :meth:`~server.WebSocketServerProtocol.process_request` which may abort the WebSocket handshake and return a 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 a HTTP response based on the above and parameters passed to - :meth:`~legacy.server.serve`; + :meth:`~server.serve`; - writes the HTTP response to the network; - moves to the ``OPEN`` state; - returns the ``path`` part of the ``uri``. @@ -186,8 +186,8 @@ in the same class, :class:`~legacy.protocol.WebSocketCommonProtocol`. The :attr:`~legacy.protocol.WebSocketCommonProtocol.is_client` attribute tells which side a protocol instance is managing. This attribute is defined on the -:attr:`~legacy.server.WebSocketServerProtocol` and -:attr:`~legacy.client.WebSocketClientProtocol` classes. +:attr:`~server.WebSocketServerProtocol` and +:attr:`~client.WebSocketClientProtocol` classes. Data flow ......... @@ -264,14 +264,14 @@ 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 ``None``, causing +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 ``None``, also causing +``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 @@ -313,7 +313,7 @@ of canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_ta and failing to close the TCP connection, thus leaking resources. Then :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` cancels -:attr:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no +:meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no protocol compliance responsibilities. Terminating it to avoid leaking it is the only concern. @@ -445,15 +445,15 @@ is canceled, which is correct at this point. to prevent cancellation. :meth:`~legacy.protocol.WebSocketCommonProtocol.close` and -:func:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` are the only +: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_connnection_task` starts by +: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_connnection_task`. +to :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`. .. _backpressure: @@ -520,21 +520,21 @@ 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:`~legacy.client.connect()` or :func:`~legacy.server.serve`. + :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:`~legacy.client.connect()` or :func:`~legacy.server.serve` according to your + :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:`~legacy.client.connect()` or :func:`~legacy.server.serve`. + :func:`~client.connect()` or :func:`~server.serve`. - OS buffers: tuning them is an advanced optimization. Concurrency diff --git a/docs/topics/memory.rst b/docs/topics/memory.rst index c880d5579..ee0109c35 100644 --- a/docs/topics/memory.rst +++ b/docs/topics/memory.rst @@ -1,6 +1,8 @@ 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. @@ -17,8 +19,8 @@ Baseline Compression settings are the main factor affecting the baseline amount of memory used by each connection. -Read to the topic guide on :doc:`../topics/compression` to learn more about -tuning compression settings. +Refer to the :doc:`topic guide on compression <../topics/compression>` to +learn more about tuning compression settings. Buffers ------- @@ -29,7 +31,7 @@ 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:`~legacy.server.serve`: +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. @@ -40,4 +42,4 @@ them to your application. When you call :func:`~legacy.server.serve`: 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`. +The design document provides :ref:`more details about buffers `. diff --git a/docs/topics/timeouts.rst b/docs/topics/timeouts.rst index 8febfce9f..51666ceea 100644 --- a/docs/topics/timeouts.rst +++ b/docs/topics/timeouts.rst @@ -1,6 +1,8 @@ Timeouts ======== +.. currentmodule:: websockets + 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. @@ -13,15 +15,18 @@ As a consequence, proxies may terminate WebSocket connections prematurely, when no message was exchanged in 30 seconds. In order to avoid this problem, websockets implements a keepalive mechanism -based on WebSocket Ping and Pong frames. Ping and Pong are designed for this +based on WebSocket Ping_ and Pong_ frames. Ping and Pong 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 + By default, websockets waits 20 seconds, then sends a Ping frame, and expects to receive the corresponding Pong frame within 20 seconds. Else, it considers the connection broken and closes it. Timings are configurable with the ``ping_interval`` and ``ping_timeout`` -arguments of :func:`~websockets.connect` and :func:`~websockets.serve`. +arguments of :func:`~client.connect` and :func:`~server.serve`. While WebSocket runs on top of TCP, websockets doesn't rely on TCP keepalive because it's disabled by default and, if enabled, the default interval is no diff --git a/src/websockets/__init__.py b/src/websockets/__init__.py index 5883c3d65..ec3484124 100644 --- a/src/websockets/__init__.py +++ b/src/websockets/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .imports import lazy_import from .version import version as __version__ # noqa diff --git a/src/websockets/__main__.py b/src/websockets/__main__.py index 785d2c3c9..860e4b1fa 100644 --- a/src/websockets/__main__.py +++ b/src/websockets/__main__.py @@ -183,7 +183,7 @@ def main() -> None: # Due to zealous removal of the loop parameter in the Queue constructor, # we need a factory coroutine to run in the freshly created event loop. - async def queue_factory() -> "asyncio.Queue[str]": + async def queue_factory() -> asyncio.Queue[str]: return asyncio.Queue() # Create a queue of user inputs. There's no need to limit its size. diff --git a/src/websockets/auth.py b/src/websockets/auth.py index f97c1feb0..afcb38cff 100644 --- a/src/websockets/auth.py +++ b/src/websockets/auth.py @@ -1,2 +1,4 @@ +from __future__ import annotations + # See #940 for why lazy_import isn't used here for backwards compatibility. from .legacy.auth import * # noqa diff --git a/src/websockets/client.py b/src/websockets/client.py index e21fd36c0..13c8a8ad0 100644 --- a/src/websockets/client.py +++ b/src/websockets/client.py @@ -68,7 +68,7 @@ def __init__( def connect(self) -> Request: # noqa: F811 """ - Create a WebSocket handshake request event to send to the server. + Create a WebSocket handshake request event to open a connection. """ headers = Headers() @@ -107,12 +107,13 @@ def connect(self) -> Request: # noqa: F811 def process_response(self, response: Response) -> None: """ - Check a handshake response received from the server. + Check a handshake response. - :param response: response - :param key: comes from :func:`build_request` - :raises ~websockets.exceptions.InvalidHandshake: if the handshake response - is invalid + Args: + request: WebSocket handshake response received from the server. + + Raises: + InvalidHandshake: if the handshake response is invalid. """ @@ -162,11 +163,6 @@ def process_extensions(self, headers: Headers) -> List[Extension]: Check that each extension is supported, as well as its parameters. - Return the list of accepted extensions. - - Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the - connection. - :rfc:`6455` leaves the rules up to the specification of each extension. @@ -182,6 +178,15 @@ def process_extensions(self, headers: Headers) -> List[Extension]: Other requirements, for example related to mandatory extensions or the order of extensions, may be implemented by overriding this method. + Args: + headers: WebSocket handshake response headers. + + Returns: + List[Extension]: List of accepted extensions. + + Raises: + InvalidHandshake: to abort the handshake. + """ accepted_extensions: List[Extension] = [] @@ -232,9 +237,13 @@ def process_subprotocol(self, headers: Headers) -> Optional[Subprotocol]: """ Handle the Sec-WebSocket-Protocol HTTP response header. - Check that it contains exactly one supported subprotocol. + If provided, check that it contains exactly one supported subprotocol. - Return the selected subprotocol. + Args: + headers: WebSocket handshake response headers. + + Returns: + Optional[Subprotocol]: Subprotocol, if one was selected. """ subprotocol: Optional[Subprotocol] = None @@ -263,7 +272,10 @@ def process_subprotocol(self, headers: Headers) -> Optional[Subprotocol]: def send_request(self, request: Request) -> None: """ - Send a WebSocket handshake request to the server. + Send a handshake request to the server. + + Args: + request: WebSocket handshake request event to send. """ if self.debug: diff --git a/src/websockets/connection.py b/src/websockets/connection.py index 52fd9bb81..684664860 100644 --- a/src/websockets/connection.py +++ b/src/websockets/connection.py @@ -212,7 +212,8 @@ def receive_data(self, data: bytes) -> None: - You must call :meth:`data_to_send` and send this data. - You should call :meth:`events_received` and process these events. - :raises EOFError: if :meth:`receive_eof` was called before + Raises: + EOFError: if :meth:`receive_eof` was called before. """ self.reader.feed_data(data) @@ -228,7 +229,8 @@ def receive_eof(self) -> None: - You aren't exepcted to call :meth:`events_received` as it won't return any new events. - :raises EOFError: if :meth:`receive_eof` was called before + Raises: + EOFError: if :meth:`receive_eof` was called before. """ self.reader.feed_eof() @@ -367,8 +369,8 @@ def close_expected(self) -> bool: Tell whether the TCP connection is expected to close soon. Call this method immediately after calling any of the ``receive_*()`` - or ``fail_*()`` methods and, if it returns ``True``, schedule closing - the TCP connection after a short timeout. + or ``fail_*()`` methods and, if it returns :obj:`True`, schedule + closing the TCP connection after a short timeout. """ # We already got a TCP Close if and only if the state is CLOSED. diff --git a/src/websockets/datastructures.py b/src/websockets/datastructures.py index 65c5d4115..1ff586abd 100644 --- a/src/websockets/datastructures.py +++ b/src/websockets/datastructures.py @@ -1,8 +1,3 @@ -""" -:mod:`websockets.datastructures` defines a class for manipulating HTTP headers. - -""" - from __future__ import annotations from typing import ( @@ -141,7 +136,7 @@ def clear(self) -> None: def update(self, *args: HeadersLike, **kwargs: str) -> None: """ - Update from a Headers instance and/or keyword arguments. + Update from a :class:`Headers` instance and/or keyword arguments. """ args = tuple( @@ -155,7 +150,8 @@ def get_all(self, key: str) -> List[str]: """ Return the (possibly empty) list of all values for a header. - :param key: header name + Args: + key: header name. """ return self._dict.get(key.lower(), []) diff --git a/src/websockets/exceptions.py b/src/websockets/exceptions.py index 6bbea324c..0c4fc5185 100644 --- a/src/websockets/exceptions.py +++ b/src/websockets/exceptions.py @@ -67,7 +67,7 @@ class WebSocketException(Exception): """ - Base class for all exceptions defined by :mod:`websockets`. + Base class for all exceptions defined by websockets. """ @@ -76,17 +76,14 @@ class ConnectionClosed(WebSocketException): """ Raised when trying to interact with a closed connection. - If a close frame was received, its code and reason are available in the - ``rcvd.code`` and ``rcvd.reason`` attributes. Else, the ``rcvd`` - attribute is ``None``. - - Likewise, if a close frame was sent, its code and reason are available in - the ``sent.code`` and ``sent.reason`` attributes. Else, the ``sent`` - attribute is ``None``. - - If close frames were received and sent, the ``rcvd_then_sent`` attribute - tells in which order this happened, from the perspective of this side of - the connection. + Attributes: + rcvd (Optional[Close]): if a close frame was received, its code and + reason are available in ``rcvd.code`` and ``rcvd.reason``. + sent (Optional[Close]): if a close frame was sent, its code and reason + are available in ``sent.code`` and ``sent.reason``. + rcvd_then_sent (Optional[bool]): if close frames were received and + sent, this attribute tells in which order this happened, from the + perspective of this side of the connection. """ @@ -249,9 +246,6 @@ class InvalidStatusCode(InvalidHandshake): """ Raised when a handshake response status code is invalid. - The integer status code is available in the ``status_code`` attribute and - HTTP headers in the ``headers`` attribute. - """ def __init__(self, status_code: int, headers: datastructures.Headers) -> None: @@ -320,8 +314,13 @@ class AbortHandshake(InvalidHandshake): This exception is an implementation detail. - The public API is :meth:`~legacy.server.WebSocketServerProtocol.process_request`. + The public API + is :meth:`~websockets.server.WebSocketServerProtocol.process_request`. + Attributes: + status (~http.HTTPStatus): HTTP status code. + headers (Headers): HTTP response headers. + body (bytes): HTTP response body. """ def __init__( diff --git a/src/websockets/extensions/base.py b/src/websockets/extensions/base.py index 7217aa513..060967618 100644 --- a/src/websockets/extensions/base.py +++ b/src/websockets/extensions/base.py @@ -1,13 +1,3 @@ -""" -:mod:`websockets.extensions.base` defines abstract classes for implementing -extensions. - -See `section 9 of RFC 6455`_. - -.. _section 9 of RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 - -""" - from __future__ import annotations from typing import List, Optional, Sequence, Tuple @@ -21,16 +11,12 @@ class Extension: """ - Abstract class for extensions. + Base class for extensions. """ - @property - def name(self) -> ExtensionName: - """ - Extension identifier. - - """ + name: ExtensionName + """Extension identifier.""" def decode( self, @@ -41,8 +27,15 @@ def decode( """ Decode an incoming frame. - :param frame: incoming frame - :param max_size: maximum payload size in bytes + Args: + frame (Frame): incoming frame. + max_size: maximum payload size in bytes. + + Returns: + Frame: Decoded frame. + + Raises: + PayloadTooBig: if decoding the payload exceeds ``max_size``. """ @@ -50,29 +43,30 @@ def encode(self, frame: frames.Frame) -> frames.Frame: """ Encode an outgoing frame. - :param frame: outgoing frame + Args: + frame (Frame): outgoing frame. + + Returns: + Frame: Encoded frame. """ class ClientExtensionFactory: """ - Abstract class for client-side extension factories. + Base class for client-side extension factories. """ - @property - def name(self) -> ExtensionName: - """ - Extension identifier. - - """ + name: ExtensionName + """Extension identifier.""" def get_request_params(self) -> List[ExtensionParameter]: """ - Build request parameters. + Build parameters to send to the server for this extension. - Return a list of ``(name, value)`` pairs. + Returns: + List[ExtensionParameter]: Parameters to send to the server. """ @@ -82,28 +76,31 @@ def process_response_params( accepted_extensions: Sequence[Extension], ) -> Extension: """ - Process response parameters received from the server. + Process parameters received from the server. + + Args: + params (Sequence[ExtensionParameter]): parameters received from + the server for this extension. + accepted_extensions (Sequence[Extension]): list of previously + accepted extensions. - :param params: list of ``(name, value)`` pairs. - :param accepted_extensions: list of previously accepted extensions. - :raises ~websockets.exceptions.NegotiationError: if parameters aren't - acceptable + Returns: + Extension: An extension instance. + + Raises: + NegotiationError: if parameters aren't acceptable. """ class ServerExtensionFactory: """ - Abstract class for server-side extension factories. + Base class for server-side extension factories. """ - @property - def name(self) -> ExtensionName: - """ - Extension identifier. - - """ + name: ExtensionName + """Extension identifier.""" def process_request_params( self, @@ -111,16 +108,21 @@ def process_request_params( accepted_extensions: Sequence[Extension], ) -> Tuple[List[ExtensionParameter], Extension]: """ - Process request parameters received from the client. + Process parameters received from the client. - To accept the offer, return a 2-uple containing: + Args: + params (Sequence[ExtensionParameter]): parameters received from + the client for this extension. + accepted_extensions (Sequence[Extension]): list of previously + accepted extensions. - - response parameters: a list of ``(name, value)`` pairs - - an extension: an instance of a subclass of :class:`Extension` + Returns: + Tuple[List[ExtensionParameter], Extension]: To accept the offer, + parameters to send to the client for this extension and an + extension instance. - :param params: list of ``(name, value)`` pairs. - :param accepted_extensions: list of previously accepted extensions. - :raises ~websockets.exceptions.NegotiationError: to reject the offer, - if parameters aren't acceptable + Raises: + NegotiationError: to reject the offer, if parameters received from + the client aren't acceptable. """ diff --git a/src/websockets/extensions/permessage_deflate.py b/src/websockets/extensions/permessage_deflate.py index a377abb55..da2bc153e 100644 --- a/src/websockets/extensions/permessage_deflate.py +++ b/src/websockets/extensions/permessage_deflate.py @@ -1,9 +1,3 @@ -""" -:mod:`websockets.extensions.permessage_deflate` implements the Compression -Extensions for WebSocket as specified in :rfc:`7692`. - -""" - from __future__ import annotations import dataclasses @@ -204,8 +198,8 @@ def _extract_parameters( """ Extract compression parameters from a list of ``(name, value)`` pairs. - If ``is_server`` is ``True``, ``client_max_window_bits`` may be provided - without a value. This is only allow in handshake requests. + If ``is_server`` is :obj:`True`, ``client_max_window_bits`` may be + provided without a value. This is only allowed in handshake requests. """ server_no_context_takeover: bool = False @@ -264,18 +258,23 @@ class ClientPerMessageDeflateFactory(ClientExtensionFactory): """ Client-side extension factory for the Per-Message Deflate extension. - Parameters behave as described in `section 7.1 of RFC 7692`_. Set them to - ``True`` to include them in the negotiation offer without a value or to an - integer value to include them with this value. + Parameters behave as described in `section 7.1 of RFC 7692`_. .. _section 7.1 of RFC 7692: https://www.rfc-editor.org/rfc/rfc7692.html#section-7.1 - :param server_no_context_takeover: defaults to ``False`` - :param client_no_context_takeover: defaults to ``False`` - :param server_max_window_bits: optional, defaults to ``None`` - :param client_max_window_bits: optional, defaults to ``None`` - :param compress_settings: optional, keyword arguments for - :func:`zlib.compressobj`, excluding ``wbits`` + Set them to :obj:`True` to include them in the negotiation offer without a + value or to an integer value to include them with this value. + + Args: + server_no_context_takeover: prevent server from using context takeover. + client_no_context_takeover: prevent client from using context takeover. + server_max_window_bits: maximum size of the server's LZ77 sliding window + in bits, between 8 and 15. + client_max_window_bits: maximum size of the client's LZ77 sliding window + in bits, between 8 and 15, or :obj:`True` to indicate support without + setting a limit. + compress_settings: additional keyword arguments for :func:`zlib.compressobj`, + excluding ``wbits``. """ @@ -440,7 +439,6 @@ def enable_client_permessage_deflate( If the extension is already present, perhaps with non-default settings, the configuration isn't changed. - """ if extensions is None: extensions = [] @@ -462,18 +460,23 @@ class ServerPerMessageDeflateFactory(ServerExtensionFactory): """ Server-side extension factory for the Per-Message Deflate extension. - Parameters behave as described in `section 7.1 of RFC 7692`_. Set them to - ``True`` to include them in the negotiation offer without a value or to an - integer value to include them with this value. + Parameters behave as described in `section 7.1 of RFC 7692`_. .. _section 7.1 of RFC 7692: https://www.rfc-editor.org/rfc/rfc7692.html#section-7.1 - :param server_no_context_takeover: defaults to ``False`` - :param client_no_context_takeover: defaults to ``False`` - :param server_max_window_bits: optional, defaults to ``None`` - :param client_max_window_bits: optional, defaults to ``None`` - :param compress_settings: optional, keyword arguments for - :func:`zlib.compressobj`, excluding ``wbits`` + Set them to :obj:`True` to include them in the negotiation offer without a + value or to an integer value to include them with this value. + + Args: + server_no_context_takeover: prevent server from using context takeover. + client_no_context_takeover: prevent client from using context takeover. + server_max_window_bits: maximum size of the server's LZ77 sliding window + in bits, between 8 and 15. + client_max_window_bits: maximum size of the client's LZ77 sliding window + in bits, between 8 and 15, or :obj:`True` to indicate support without + setting a limit. + compress_settings: additional keyword arguments for :func:`zlib.compressobj`, + excluding ``wbits``. """ diff --git a/src/websockets/frames.py b/src/websockets/frames.py index a0ce1d350..9a97f2530 100644 --- a/src/websockets/frames.py +++ b/src/websockets/frames.py @@ -1,8 +1,3 @@ -""" -Parse and serialize WebSocket frames. - -""" - from __future__ import annotations import dataclasses @@ -104,15 +99,16 @@ class Frame: """ WebSocket frame. - :param int opcode: opcode - :param bytes data: payload data - :param bool fin: FIN bit - :param bool rsv1: RSV1 bit - :param bool rsv2: RSV2 bit - :param bool rsv3: RSV3 bit + Args: + opcode: opcode. + data: payload data. + fin: FIN bit. + rsv1: RSV1 bit. + rsv2: RSV2 bit. + rsv3: RSV3 bit. Only these fields are needed. The MASK bit, payload length and masking-key - are handled on the fly by :meth:`parse` and :meth:`serialize`. + are handled on the fly when parsing and serializing frames. """ @@ -176,22 +172,23 @@ def parse( mask: bool, max_size: Optional[int] = None, extensions: Optional[Sequence[extensions.Extension]] = None, - ) -> Generator[None, None, "Frame"]: + ) -> Generator[None, None, Frame]: """ - Read a WebSocket frame. - - :param read_exact: generator-based coroutine that reads the requested - number of bytes or raises an exception if there isn't enough data - :param mask: whether the frame should be masked i.e. whether the read - happens on the server side - :param max_size: maximum payload size in bytes - :param extensions: list of classes with a ``decode()`` method that - transforms the frame and return a new frame; extensions are applied - in reverse order - :raises ~websockets.exceptions.PayloadTooBig: if the frame exceeds - ``max_size`` - :raises ~websockets.exceptions.ProtocolError: if the frame - contains incorrect values + Parse a WebSocket frame. + + This is a generator-based coroutine. + + Args: + read_exact: generator-based coroutine that reads the requested + bytes or raises an exception if there isn't enough data. + mask: whether the frame should be masked i.e. whether the read + happens on the server side. + max_size: maximum payload size in bytes. + extensions: list of extensions, applied in reverse order. + + Raises: + PayloadTooBig: if the frame's payload size exceeds ``max_size``. + ProtocolError: if the frame contains incorrect values. """ # Read the header. @@ -249,16 +246,15 @@ def serialize( extensions: Optional[Sequence[extensions.Extension]] = None, ) -> bytes: """ - Write a WebSocket frame. + Serialize a WebSocket frame. + + Args: + mask: whether the frame should be masked i.e. whether the write + happens on the client side. + extensions: list of extensions, applied in order. - :param frame: frame to write - :param mask: whether the frame should be masked i.e. whether the write - happens on the client side - :param extensions: list of classes with an ``encode()`` method that - transform the frame and return a new frame; extensions are applied - in order - :raises ~websockets.exceptions.ProtocolError: if the frame - contains incorrect values + Raises: + ProtocolError: if the frame contains incorrect values. """ self.check() @@ -306,8 +302,8 @@ def check(self) -> None: """ Check that reserved bits and opcode have acceptable values. - :raises ~websockets.exceptions.ProtocolError: if a reserved - bit or the opcode is invalid + Raises: + ProtocolError: if a reserved bit or the opcode is invalid. """ if self.rsv1 or self.rsv2 or self.rsv3: @@ -332,7 +328,8 @@ def prepare_data(data: Data) -> Tuple[int, bytes]: If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like object. - :raises TypeError: if ``data`` doesn't have a supported type + Raises: + TypeError: if ``data`` doesn't have a supported type. """ if isinstance(data, str): @@ -354,7 +351,8 @@ def prepare_ctrl(data: Data) -> bytes: If ``data`` is a bytes-like object, return a :class:`bytes` object. - :raises TypeError: if ``data`` doesn't have a supported type + Raises: + TypeError: if ``data`` doesn't have a supported type. """ if isinstance(data, str): @@ -398,8 +396,12 @@ def parse(cls, data: bytes) -> Close: """ Parse the payload of a close frame. - :raises ~websockets.exceptions.ProtocolError: if data is ill-formed - :raises UnicodeDecodeError: if the reason isn't valid UTF-8 + Args: + data: payload of the close frame. + + Raises: + ProtocolError: if data is ill-formed. + UnicodeDecodeError: if the reason isn't valid UTF-8. """ if len(data) >= 2: @@ -415,9 +417,7 @@ def parse(cls, data: bytes) -> Close: def serialize(self) -> bytes: """ - Serialize the payload for a close frame. - - This is the reverse of :meth:`parse`. + Serialize the payload of a close frame. """ self.check() @@ -427,8 +427,8 @@ def check(self) -> None: """ Check that the close code has a valid value for a close frame. - :raises ~websockets.exceptions.ProtocolError: if the close code - is invalid + Raises: + ProtocolError: if the close code is invalid. """ if not (self.code in EXTERNAL_CLOSE_CODES or 3000 <= self.code < 5000): diff --git a/src/websockets/headers.py b/src/websockets/headers.py index ee6dd1672..a2fdfdd30 100644 --- a/src/websockets/headers.py +++ b/src/websockets/headers.py @@ -1,9 +1,3 @@ -""" -:mod:`websockets.headers` provides parsers and serializers for HTTP headers -used in WebSocket handshake messages. - -""" - from __future__ import annotations import base64 @@ -48,7 +42,7 @@ def peek_ahead(header: str, pos: int) -> Optional[str]: """ Return the next character from ``header`` at the given position. - Return ``None`` at the end of ``header``. + Return :obj:`None` at the end of ``header``. We never need to peek more than one character ahead. @@ -83,7 +77,8 @@ def parse_token(header: str, pos: int, header_name: str) -> Tuple[str, int]: Return the token value and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ match = _token_re.match(header, pos) @@ -106,7 +101,8 @@ def parse_quoted_string(header: str, pos: int, header_name: str) -> Tuple[str, i Return the unquoted value and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ match = _quoted_string_re.match(header, pos) @@ -158,7 +154,8 @@ def parse_list( Return a list of items. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ # Per https://www.rfc-editor.org/rfc/rfc7230.html#section-7, "a recipient @@ -211,7 +208,8 @@ def parse_connection_option( Return the protocol value and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ item, pos = parse_token(header, pos, header_name) @@ -224,8 +222,11 @@ def parse_connection(header: str) -> List[ConnectionOption]: Return a list of HTTP connection options. - :param header: value of the ``Connection`` header - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Args + header: value of the ``Connection`` header. + + Raises: + InvalidHeaderFormat: on invalid inputs. """ return parse_list(parse_connection_option, header, 0, "Connection") @@ -244,7 +245,8 @@ def parse_upgrade_protocol( Return the protocol value and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ match = _protocol_re.match(header, pos) @@ -261,8 +263,11 @@ def parse_upgrade(header: str) -> List[UpgradeProtocol]: Return a list of HTTP protocols. - :param header: value of the ``Upgrade`` header - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Args: + header: value of the ``Upgrade`` header. + + Raises: + InvalidHeaderFormat: on invalid inputs. """ return parse_list(parse_upgrade_protocol, header, 0, "Upgrade") @@ -276,7 +281,8 @@ def parse_extension_item_param( Return a ``(name, value)`` pair and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ # Extract parameter name. @@ -312,7 +318,8 @@ def parse_extension_item( Return an ``(extension name, parameters)`` pair, where ``parameters`` is a list of ``(name, value)`` pairs, and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ # Extract extension name. @@ -344,9 +351,10 @@ def parse_extension(header: str) -> List[ExtensionHeader]: ... ] - Parameter values are ``None`` when no value is provided. + Parameter values are :obj:`None` when no value is provided. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ return parse_list(parse_extension_item, header, 0, "Sec-WebSocket-Extensions") @@ -397,7 +405,8 @@ def parse_subprotocol_item( Return the subprotocol value and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ item, pos = parse_token(header, pos, header_name) @@ -410,7 +419,8 @@ def parse_subprotocol(header: str) -> List[Subprotocol]: Return a list of WebSocket subprotocols. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ return parse_list(parse_subprotocol_item, header, 0, "Sec-WebSocket-Protocol") @@ -450,7 +460,8 @@ def build_www_authenticate_basic(realm: str) -> str: """ Build a ``WWW-Authenticate`` header for HTTP Basic Auth. - :param realm: authentication realm + Args: + realm: identifier of the protection space. """ # https://www.rfc-editor.org/rfc/rfc7617.html#section-2 @@ -468,7 +479,8 @@ def parse_token68(header: str, pos: int, header_name: str) -> Tuple[str, int]: Return the token value and the new position. - :raises ~websockets.exceptions.InvalidHeaderFormat: on invalid inputs. + Raises: + InvalidHeaderFormat: on invalid inputs. """ match = _token68_re.match(header, pos) @@ -494,9 +506,12 @@ def parse_authorization_basic(header: str) -> Tuple[str, str]: Return a ``(username, password)`` tuple. - :param header: value of the ``Authorization`` header - :raises InvalidHeaderFormat: on invalid inputs - :raises InvalidHeaderValue: on unsupported inputs + Args: + header: value of the ``Authorization`` header. + + Raises: + InvalidHeaderFormat: on invalid inputs. + InvalidHeaderValue: on unsupported inputs. """ # https://www.rfc-editor.org/rfc/rfc7235.html#section-2.1 diff --git a/src/websockets/http11.py b/src/websockets/http11.py index daa0efffb..b82a0bfdc 100644 --- a/src/websockets/http11.py +++ b/src/websockets/http11.py @@ -44,38 +44,46 @@ class Request: """ WebSocket handshake request. - :param path: path and optional query - :param headers: + Attributes: + path: Request path, including optional query. + headers: Request headers. + exception: If processing the response triggers an exception, + the exception is stored in this attribute. """ path: str headers: datastructures.Headers - # body isn't useful is the context of this library + # body isn't useful is the context of this library. - # If processing the request triggers an exception, it's stored here. exception: Optional[Exception] = None @classmethod def parse( - cls, read_line: Callable[[], Generator[None, None, bytes]] - ) -> Generator[None, None, "Request"]: + cls, + read_line: Callable[[], Generator[None, None, bytes]], + ) -> Generator[None, None, Request]: """ - Parse an HTTP/1.1 GET request and return ``(path, headers)``. + Parse a WebSocket handshake request. + + This is a generator-based coroutine. - ``path`` isn't URL-decoded or validated in any way. + The request path isn't URL-decoded or validated in any way. - ``path`` and ``headers`` are expected to contain only ASCII characters. - Other characters are represented with surrogate escapes. + The request path and headers are expected to contain only ASCII + characters. Other characters are represented with surrogate escapes. - :func:`parse_request` doesn't attempt to read the request body because + :meth:`parse` doesn't attempt to read the request body because WebSocket handshake requests don't have one. If the request contains a - body, it may be read from ``stream`` after this coroutine returns. + body, it may be read from the data stream after :meth:`parse` returns. - :param read_line: generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data - :raises EOFError: if the connection is closed without a full HTTP request - :raises exceptions.SecurityError: if the request exceeds a security limit - :raises ValueError: if the request isn't well formatted + Args: + read_line: generator-based coroutine that reads a LF-terminated + line or raises an exception if there isn't enough data + + Raises: + EOFError: if the connection is closed without a full HTTP request. + SecurityError: if the request exceeds a security limit. + ValueError: if the request isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1 @@ -114,10 +122,10 @@ def parse( def serialize(self) -> bytes: """ - Serialize an HTTP/1.1 GET request. + Serialize a WebSocket handshake request. """ - # Since the path and headers only contain ASCII characters, + # Since the request line and headers only contain ASCII characters, # we can keep this simple. request = f"GET {self.path} HTTP/1.1\r\n".encode() request += self.headers.serialize() @@ -129,6 +137,14 @@ class Response: """ WebSocket handshake response. + Attributes: + status_code: Response code. + reason_phrase: Response reason. + headers: Response headers. + body: Response body, if any. + exception: if processing the response triggers an exception, + the exception is stored in this attribute. + """ status_code: int @@ -136,7 +152,6 @@ class Response: headers: datastructures.Headers body: Optional[bytes] = None - # If processing the response triggers an exception, it's stored here. exception: Optional[Exception] = None @classmethod @@ -145,32 +160,32 @@ def parse( read_line: Callable[[], Generator[None, None, bytes]], read_exact: Callable[[int], Generator[None, None, bytes]], read_to_eof: Callable[[], Generator[None, None, bytes]], - ) -> Generator[None, None, "Response"]: + ) -> Generator[None, None, Response]: """ - Parse an HTTP/1.1 response and return ``(status_code, reason, headers)``. + Parse a WebSocket handshake response. - ``reason`` and ``headers`` are expected to contain only ASCII characters. - Other characters are represented with surrogate escapes. + This is a generator-based coroutine. - :func:`parse_request` doesn't attempt to read the response body because - WebSocket handshake responses don't have one. If the response contains a - body, it may be read from ``stream`` after this coroutine returns. + The reason phrase and headers are expected to contain only ASCII + characters. Other characters are represented with surrogate escapes. - :param read_line: generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data - :param read_exact: generator-based coroutine that reads the requested - number of bytes or raises an exception if there isn't enough data - :raises EOFError: if the connection is closed without a full HTTP response - :raises exceptions.SecurityError: if the response exceeds a security limit - :raises LookupError: if the response isn't well formatted - :raises ValueError: if the response isn't well formatted + Args: + read_line: generator-based coroutine that reads a LF-terminated + line or raises an exception if there isn't enough data. + read_exact: generator-based coroutine that reads the requested + bytes or raises an exception if there isn't enough data. + read_to_eof: generator-based coroutine that reads until the end + of the strem. + + Raises: + EOFError: if the connection is closed without a full HTTP response. + SecurityError: if the response exceeds a security limit. + LookupError: if the response isn't well formatted. + ValueError: if the response isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2 - # As in parse_request, parsing is simple because a fixed value is expected - # for version, status_code is a 3-digit number, and reason can be ignored. - try: status_line = yield from parse_line(read_line) except EOFError as exc: @@ -227,7 +242,7 @@ def parse( def serialize(self) -> bytes: """ - Serialize an HTTP/1.1 GET response. + Serialize a WebSocket handshake response. """ # Since the status line and headers only contain ASCII characters, @@ -240,15 +255,21 @@ def serialize(self) -> bytes: def parse_headers( - read_line: Callable[[], Generator[None, None, bytes]] + read_line: Callable[[], Generator[None, None, bytes]], ) -> Generator[None, None, datastructures.Headers]: """ Parse HTTP headers. Non-ASCII characters are represented with surrogate escapes. - :param read_line: generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data + Args: + read_line: generator-based coroutine that reads a LF-terminated line + or raises an exception if there isn't enough data. + + Raises: + EOFError: if the connection is closed without complete headers. + SecurityError: if the request exceeds a security limit. + ValueError: if the request isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2 @@ -285,15 +306,20 @@ def parse_headers( def parse_line( - read_line: Callable[[], Generator[None, None, bytes]] + read_line: Callable[[], Generator[None, None, bytes]], ) -> Generator[None, None, bytes]: """ Parse a single line. CRLF is stripped from the return value. - :param read_line: generator-based coroutine that reads a LF-terminated - line or raises an exception if there isn't enough data + Args: + read_line: generator-based coroutine that reads a LF-terminated line + or raises an exception if there isn't enough data. + + Raises: + EOFError: if the connection is closed without a CRLF. + SecurityError: if the response exceeds a security limit. """ # Security: TODO: add a limit here diff --git a/src/websockets/imports.py b/src/websockets/imports.py index c9508d188..a6a59d4c2 100644 --- a/src/websockets/imports.py +++ b/src/websockets/imports.py @@ -9,15 +9,15 @@ def import_name(name: str, source: str, namespace: Dict[str, Any]) -> Any: """ - Import from in . + Import ``name`` from ``source`` in ``namespace``. - There are two cases: + There are two use cases: - - is an object defined in - - is a submodule of source + - ``name`` is an object defined in ``source``; + - ``name`` is a submodule of ``source``. - Neither __import__ nor importlib.import_module does exactly this. - __import__ is closer to the intended behavior. + Neither :func:`__import__` nor :func:`~importlib.import_module` does + exactly this. :func:`__import__` is closer to the intended behavior. """ level = 0 @@ -49,7 +49,7 @@ def lazy_import( } ) - This function defines __getattr__ and __dir__ per PEP 562. + This function defines ``__getattr__`` and ``__dir__`` per :pep:`562`. """ if aliases is None: diff --git a/src/websockets/legacy/auth.py b/src/websockets/legacy/auth.py index 5f2b1311a..8825c14ec 100644 --- a/src/websockets/legacy/auth.py +++ b/src/websockets/legacy/auth.py @@ -1,9 +1,3 @@ -""" -:mod:`websockets.legacy.auth` provides HTTP Basic Authentication according to -:rfc:`7235` and :rfc:`7617`. - -""" - from __future__ import annotations import functools @@ -37,7 +31,16 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): """ - realm = "" + realm: str = "" + """ + Scope of protection. + + If provided, it should contain only ASCII characters because the + encoding of non-ASCII characters is undefined. + """ + + username: Optional[str] = None + """Username of the authenticated user.""" def __init__( self, @@ -55,13 +58,17 @@ async def check_credentials(self, username: str, password: str) -> bool: """ Check whether credentials are authorized. - If ``check_credentials`` returns ``True``, the WebSocket handshake - continues. If it returns ``False``, the handshake fails with a HTTP - 401 error. - This coroutine may be overridden in a subclass, for example to authenticate against a database or an external service. + Args: + username: HTTP Basic Auth username. + password: HTTP Basic Auth password. + + Returns: + bool: :obj:`True` if the handshake should continue; + :obj:`False` if it should fail with a HTTP 401 error. + """ if self._check_credentials is not None: return await self._check_credentials(username, password) @@ -116,8 +123,8 @@ def basic_auth_protocol_factory( """ Protocol factory that enforces HTTP Basic Auth. - ``basic_auth_protocol_factory`` is designed to integrate with - :func:`~websockets.legacy.server.serve` like this:: + :func:`basic_auth_protocol_factory` is designed to integrate with + :func:`~websockets.server.serve` like this:: websockets.serve( ..., @@ -127,28 +134,22 @@ def basic_auth_protocol_factory( ) ) - ``realm`` indicates the scope of protection. It should contain only ASCII - characters because the encoding of non-ASCII characters is undefined. - Refer to section 2.2 of :rfc:`7235` for details. - - ``credentials`` defines hard coded authorized credentials. It can be a - ``(username, password)`` pair or a list of such pairs. - - ``check_credentials`` defines a coroutine that checks whether credentials - are authorized. This coroutine receives ``username`` and ``password`` - arguments and returns a :class:`bool`. - - One of ``credentials`` or ``check_credentials`` must be provided but not - both. - - By default, ``basic_auth_protocol_factory`` creates a factory for building - :class:`BasicAuthWebSocketServerProtocol` instances. You can override this - with the ``create_protocol`` parameter. - - :param realm: scope of protection - :param credentials: hard coded credentials - :param check_credentials: coroutine that verifies credentials - :raises TypeError: if the credentials argument has the wrong type + Args: + realm: indicates the scope of protection. It should contain only ASCII + characters because the encoding of non-ASCII characters is + undefined. Refer to section 2.2 of :rfc:`7235` for details. + credentials: defines hard coded authorized credentials. It can be a + ``(username, password)`` pair or a list of such pairs. + check_credentials: defines a coroutine that verifies credentials. + This coroutine receives ``username`` and ``password`` arguments + and returns a :class:`bool`. One of ``credentials`` or + ``check_credentials`` must be provided but not both. + create_protocol: factory that creates the protocol. By default, this + is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced + by a subclass. + Raises: + TypeError: if the ``credentials`` or ``check_credentials`` argument is + wrong. """ if (credentials is None) == (check_credentials is None): diff --git a/src/websockets/legacy/client.py b/src/websockets/legacy/client.py index 6d976e0df..e5743cc0e 100644 --- a/src/websockets/legacy/client.py +++ b/src/websockets/legacy/client.py @@ -1,8 +1,3 @@ -""" -:mod:`websockets.legacy.client` defines the WebSocket client APIs. - -""" - from __future__ import annotations import asyncio @@ -57,99 +52,27 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): """ - :class:`~asyncio.Protocol` subclass implementing a WebSocket client. - - :class:`WebSocketClientProtocol`: + WebSocket client connection. - * performs the opening handshake to establish the connection; - * provides :meth:`recv` and :meth:`send` coroutines for receiving and - sending messages; - * deals with control frames automatically; - * performs the closing handshake to terminate the connection. + :class:`WebSocketClientProtocol` provides :meth:`recv` and :meth:`send` + coroutines for receiving and sending messages. - :class:`WebSocketClientProtocol` supports asynchronous iteration:: + It supports asynchronous iteration to receive incoming messages:: async for message in websocket: await process(message) - The iterator yields incoming messages. It exits normally when the - connection is closed with the close code 1000 (OK) or 1001 (going away). - It raises a :exc:`~websockets.exceptions.ConnectionClosedError` exception - when the connection is closed with any other code. - - Once the connection is open, a `Ping frame`_ is sent every - ``ping_interval`` seconds. This serves as a keepalive. It helps keeping - the connection open, especially in the presence of proxies with short - timeouts on inactive connections. Set ``ping_interval`` to ``None`` to - disable this behavior. - - .. _Ping frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 - - If the corresponding `Pong frame`_ isn't received within ``ping_timeout`` - seconds, the connection is considered unusable and is closed with - code 1011. This ensures that the remote endpoint remains responsive. Set - ``ping_timeout`` to ``None`` to disable this behavior. - - .. _Pong frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 - - The ``close_timeout`` parameter defines a maximum wait time for completing - the closing handshake and terminating the TCP connection. For legacy - reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds. - - ``close_timeout`` needs to be a parameter of the protocol because - websockets usually calls :meth:`close` implicitly upon exit when - :func:`connect` is used as a context manager. - - To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`. - - The ``max_size`` parameter enforces the maximum size for incoming messages - in bytes. The default value is 1 MiB. ``None`` disables the limit. If a - message larger than the maximum size is received, :meth:`recv` will - raise :exc:`~websockets.exceptions.ConnectionClosedError` and the - connection will be closed with code 1009. - - The ``max_queue`` parameter sets the maximum length of the queue that - holds incoming messages. The default value is ``32``. ``None`` disables - the limit. Messages are added to an in-memory queue when they're received; - then :meth:`recv` pops from that queue. In order to prevent excessive - memory consumption when messages are received faster than they can be - processed, the queue must be bounded. If the queue fills up, the protocol - stops processing incoming data until :meth:`recv` is called. In this - situation, various receive buffers (at least in :mod:`asyncio` and in the - OS) will fill up, then the TCP receive window will shrink, slowing down - transmission to avoid packet loss. - - Since Python can use up to 4 bytes of memory to represent a single - character, each connection may use up to ``4 * max_size * max_queue`` - bytes of memory to store incoming messages. By default, this is 128 MiB. - You may want to lower the limits, depending on your application's - requirements. - - The ``read_limit`` argument sets the high-water limit of the buffer for - incoming bytes. The low-water limit is half the high-water limit. The - default value is 64 KiB, half of asyncio's default (based on the current - implementation of :class:`~asyncio.StreamReader`). - - The ``write_limit`` argument sets the high-water limit of the buffer for - outgoing bytes. The low-water limit is a quarter of the high-water limit. - The default value is 64 KiB, equal to asyncio's default (based on the - current implementation of ``FlowControlMixin``). - - As soon as the HTTP request and response in the opening handshake are - processed: - - * the request path is available in the :attr:`path` attribute; - * the request and response HTTP headers are available in the - :attr:`request_headers` and :attr:`response_headers` attributes, - which are :class:`~websockets.http.Headers` instances. - - If a subprotocol was negotiated, it's available in the :attr:`subprotocol` - attribute. - - Once the connection is closed, the code is available in the - :attr:`close_code` attribute and the reason in :attr:`close_reason`. - - All attributes must be treated as read-only. + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away). It raises + a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection + is closed with any other code. + + See :func:`connect` for the documentation of ``logger``, ``origin``, + ``extensions``, ``subprotocols``, and ``extra_headers``. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. """ @@ -159,11 +82,11 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): def __init__( self, *, + logger: Optional[LoggerLike] = None, origin: Optional[Origin] = None, extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLike] = None, - logger: Optional[LoggerLike] = None, **kwargs: Any, ) -> None: if logger is None: @@ -201,8 +124,9 @@ async def read_http_response(self) -> Tuple[int, Headers]: If the response contains a body, it may be read from ``self.reader`` after this coroutine returns. - :raises ~websockets.exceptions.InvalidMessage: if the HTTP message is - malformed or isn't an HTTP/1.1 GET response + Raises: + InvalidMessage: if the HTTP message is malformed or isn't an + HTTP/1.1 GET response. """ try: @@ -346,17 +270,17 @@ async def handshake( """ Perform the client side of the opening handshake. - :param origin: sets the Origin HTTP header - :param available_extensions: list of supported extensions in the order - in which they should be used - :param available_subprotocols: list of supported subprotocols in order - of decreasing preference - :param extra_headers: sets additional HTTP request headers; it must be - a :class:`~websockets.http.Headers` instance, a - :class:`~collections.abc.Mapping`, or an iterable of ``(name, - value)`` pairs - :raises ~websockets.exceptions.InvalidHandshake: if the handshake - fails + Args: + wsuri: URI of the WebSocket server. + origin: value of the ``Origin`` header. + available_extensions: list of supported extensions, in order in + which they should be tried. + available_subprotocols: list of supported subprotocols, in order + of decreasing preference. + extra_headers: arbitrary HTTP headers to add to the request. + + Raises: + InvalidHandshake: if the handshake fails. """ request_headers = Headers() @@ -416,14 +340,14 @@ async def handshake( class Connect: """ - Connect to the WebSocket server at the given ``uri``. + Connect to the WebSocket server at ``uri``. Awaiting :func:`connect` yields a :class:`WebSocketClientProtocol` which can then be used to send and receive messages. :func:`connect` can be used as a asynchronous context manager:: - async with connect(...) as websocket: + async with websockets.connect(...) as websocket: ... The connection is closed automatically when exiting the context. @@ -431,59 +355,71 @@ class Connect: :func:`connect` can be used as an infinite asynchronous iterator to reconnect automatically on errors:: - async for websocket in connect(...): - ... + async for websocket in websockets.connect(...): + try: + ... + except websockets.ConnectionClosed: + continue - You must catch all exceptions, or else you will exit the loop prematurely. - As above, connections are closed automatically. Connection attempts are - delayed with exponential backoff, starting at three seconds and - increasing up to one minute. - - :func:`connect` is a wrapper around the event loop's - :meth:`~asyncio.loop.create_connection` method. Unknown keyword arguments - are passed to :meth:`~asyncio.loop.create_connection`. - - For example, you can set the ``ssl`` keyword argument to a - :class:`~ssl.SSLContext` to enforce some TLS settings. When connecting to - a ``wss://`` URI, if this argument isn't provided explicitly, - :func:`ssl.create_default_context` is called to create a context. - - You can connect to a different host and port from those found in ``uri`` - by setting ``host`` and ``port`` keyword arguments. This only changes the - destination of the TCP connection. The host name from ``uri`` is still - used in the TLS handshake for secure connections and in the ``Host`` HTTP - header. - - ``create_protocol`` defaults to :class:`WebSocketClientProtocol`. It may - be replaced by a wrapper or a subclass to customize the protocol that - manages the connection. - - If the WebSocket connection isn't established within ``open_timeout`` - seconds, :func:`connect` raises :exc:`~asyncio.TimeoutError`. The default - is 10 seconds. Set ``open_timeout`` to ``None`` to disable the timeout. - - The behavior of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit`` is - described in :class:`WebSocketClientProtocol`. - - :func:`connect` also accepts the following optional arguments: - - * ``compression`` is a shortcut to configure compression extensions; - by default it enables the "permessage-deflate" extension; set it to - ``None`` to disable compression. - * ``origin`` sets the Origin HTTP header. - * ``extensions`` is a list of supported extensions in order of - decreasing preference. - * ``subprotocols`` is a list of supported subprotocols in order of - decreasing preference. - * ``extra_headers`` sets additional HTTP request headers; it can be a - :class:`~websockets.http.Headers` instance, a - :class:`~collections.abc.Mapping`, or an iterable of ``(name, value)`` - pairs. - - :raises ~websockets.uri.InvalidURI: if ``uri`` is invalid - :raises ~websockets.handshake.InvalidHandshake: if the opening handshake - fails + The connection is closed automatically after each iteration of the loop. + + If an error occurs while establishing the connection, :func:`connect` + retries with exponential backoff. The backoff delay starts at three + seconds and increases up to one minute. + + If an error occurs in the body of the loop, you can handle the exception + and :func:`connect` will reconnect with the next iteration; or you can + let the exception bubble up and break out of the loop. This lets you + decide which errors trigger a reconnection and which errors are fatal. + + Args: + uri: URI of the WebSocket server. + create_protocol: factory for the :class:`asyncio.Protocol` managing + the connection; defaults to :class:`WebSocketClientProtocol`; may + be set to a wrapper or a subclass to customize connection handling. + logger: logger for this connection; + defaults to ``logging.getLogger("websockets.client")``; + see the :doc:`logging guide <../topics/logging>` for details. + compression: shortcut that enables the "permessage-deflate" extension + by default; may be set to :obj:`None` to disable compression; + see the :doc:`compression guide <../topics/compression>` for details. + origin: value of the ``Origin`` header. This is useful when connecting + to a server that validates the ``Origin`` header to defend against + Cross-Site WebSocket Hijacking attacks. + extensions: list of supported extensions, in order in which they + should be tried. + subprotocols: list of supported subprotocols, in order of decreasing + preference. + extra_headers: arbitrary HTTP headers to add to the request. + open_timeout: timeout for opening the connection in seconds; + :obj:`None` to disable the timeout + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. + + Any other keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_connection` method. + + For example: + + * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enforce TLS + settings. When connecting to a ``wss://`` URI, if ``ssl`` isn't + provided, a TLS context is created + with :func:`~ssl.create_default_context`. + + * You can set ``host`` and ``port`` to connect to a different host and + port from those found in ``uri``. This only changes the destination of + the TCP connection. The host name from ``uri`` is still used in the TLS + handshake for secure connections and in the ``Host`` header. + + Returns: + WebSocketClientProtocol: WebSocket connection. + + Raises: + InvalidURI: if ``uri`` isn't a valid WebSocket URI. + InvalidHandshake: if the opening handshake fails. + ~asyncio.TimeoutError: if the opening handshake times out. """ @@ -494,6 +430,12 @@ def __init__( uri: str, *, create_protocol: Optional[Callable[[Any], WebSocketClientProtocol]] = None, + logger: Optional[LoggerLike] = None, + compression: Optional[str] = "deflate", + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLike] = None, open_timeout: Optional[float] = 10, ping_interval: Optional[float] = 20, ping_timeout: Optional[float] = 20, @@ -502,12 +444,6 @@ def __init__( max_queue: Optional[int] = 2 ** 5, read_limit: int = 2 ** 16, write_limit: int = 2 ** 16, - compression: Optional[str] = "deflate", - origin: Optional[Origin] = None, - extensions: Optional[Sequence[ClientExtensionFactory]] = None, - subprotocols: Optional[Sequence[Subprotocol]] = None, - extra_headers: Optional[HeadersLike] = None, - logger: Optional[LoggerLike] = None, **kwargs: Any, ) -> None: # Backwards compatibility: close_timeout used to be called timeout. @@ -560,6 +496,11 @@ def __init__( factory = functools.partial( create_protocol, + logger=logger, + origin=origin, + extensions=extensions, + subprotocols=subprotocols, + extra_headers=extra_headers, ping_interval=ping_interval, ping_timeout=ping_timeout, close_timeout=close_timeout, @@ -567,16 +508,11 @@ def __init__( max_queue=max_queue, read_limit=read_limit, write_limit=write_limit, - loop=_loop, host=wsuri.host, port=wsuri.port, secure=wsuri.secure, legacy_recv=legacy_recv, - origin=origin, - extensions=extensions, - subprotocols=subprotocols, - extra_headers=extra_headers, - logger=logger, + loop=_loop, ) if kwargs.pop("unix", False): @@ -743,15 +679,17 @@ def unix_connect( """ Similar to :func:`connect`, but for connecting to a Unix socket. - This function calls the event loop's + This function builds upon the event loop's :meth:`~asyncio.loop.create_unix_connection` method. It is only available on Unix. It's mainly useful for debugging servers listening on Unix sockets. - :param path: file system path to the Unix socket - :param uri: WebSocket URI + Args: + path: file system path to the Unix socket. + uri: URI of the WebSocket server; the host is used in the TLS + handshake for secure connections and in the ``Host`` header. """ return connect(uri=uri, path=path, unix=True, **kwargs) diff --git a/src/websockets/legacy/framing.py b/src/websockets/legacy/framing.py index 40cbd41bf..c4de7eb28 100644 --- a/src/websockets/legacy/framing.py +++ b/src/websockets/legacy/framing.py @@ -1,15 +1,3 @@ -""" -:mod:`websockets.legacy.framing` reads and writes WebSocket frames. - -It deals with a single frame at a time. Anything that depends on the sequence -of frames is implemented in :mod:`websockets.legacy.protocol`. - -See `section 5 of RFC 6455`_. - -.. _section 5 of RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-5 - -""" - from __future__ import annotations import dataclasses @@ -60,22 +48,21 @@ async def read( mask: bool, max_size: Optional[int] = None, extensions: Optional[Sequence[extensions.Extension]] = None, - ) -> "Frame": + ) -> Frame: """ Read a WebSocket frame. - :param reader: coroutine that reads exactly the requested number of - bytes, unless the end of file is reached - :param mask: whether the frame should be masked i.e. whether the read - happens on the server side - :param max_size: maximum payload size in bytes - :param extensions: list of classes with a ``decode()`` method that - transforms the frame and return a new frame; extensions are applied - in reverse order - :raises ~websockets.exceptions.PayloadTooBig: if the frame exceeds - ``max_size`` - :raises ~websockets.exceptions.ProtocolError: if the frame - contains incorrect values + Args: + reader: coroutine that reads exactly the requested number of + bytes, unless the end of file is reached. + mask: whether the frame should be masked i.e. whether the read + happens on the server side. + max_size: maximum payload size in bytes. + extensions: list of extensions, applied in reverse order. + + Raises: + PayloadTooBig: if the frame exceeds ``max_size``. + ProtocolError: if the frame contains incorrect values. """ @@ -142,15 +129,15 @@ def write( """ Write a WebSocket frame. - :param frame: frame to write - :param write: function that writes bytes - :param mask: whether the frame should be masked i.e. whether the write - happens on the client side - :param extensions: list of classes with an ``encode()`` method that - transform the frame and return a new frame; extensions are applied - in order - :raises ~websockets.exceptions.ProtocolError: if the frame - contains incorrect values + Args: + frame: frame to write. + write: function that writes bytes. + mask: whether the frame should be masked i.e. whether the write + happens on the client side. + extensions: list of extensions, applied in order. + + Raises: + ProtocolError: if the frame contains incorrect values. """ # The frame is written in a single call to write in order to prevent @@ -168,10 +155,12 @@ def parse_close(data: bytes) -> Tuple[int, str]: """ Parse the payload from a close frame. - Return ``(code, reason)``. + Returns: + Tuple[int, str]: close code and reason. - :raises ~websockets.exceptions.ProtocolError: if data is ill-formed - :raises UnicodeDecodeError: if the reason isn't valid UTF-8 + Raises: + ProtocolError: if data is ill-formed. + UnicodeDecodeError: if the reason isn't valid UTF-8. """ return dataclasses.astuple(Close.parse(data)) # type: ignore @@ -181,7 +170,5 @@ def serialize_close(code: int, reason: str) -> bytes: """ Serialize the payload for a close frame. - This is the reverse of :func:`parse_close`. - """ return Close(code, reason).serialize() diff --git a/src/websockets/legacy/handshake.py b/src/websockets/legacy/handshake.py index 7cde58ac1..569937bb9 100644 --- a/src/websockets/legacy/handshake.py +++ b/src/websockets/legacy/handshake.py @@ -1,30 +1,3 @@ -""" -:mod:`websockets.legacy.handshake` provides helpers for the WebSocket handshake. - -See `section 4 of RFC 6455`_. - -.. _section 4 of RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4 - -Some checks cannot be performed because they depend too much on the -context; instead, they're documented below. - -To accept a connection, a server must: - -- Read the request, check that the method is GET, and check the headers with - :func:`check_request`, -- Send a 101 response to the client with the headers created by - :func:`build_response` if the request is valid; otherwise, send an - appropriate HTTP error code. - -To open a connection, a client must: - -- Send a GET request to the server with the headers created by - :func:`build_request`, -- Read the response, check that the status code is 101, and check the headers - with :func:`check_response`. - -""" - from __future__ import annotations import base64 @@ -47,8 +20,11 @@ def build_request(headers: Headers) -> str: Update request headers passed in argument. - :param headers: request headers - :returns: ``key`` which must be passed to :func:`check_response` + Args: + headers: handshake request headers. + + Returns: + str: ``key`` that must be passed to :func:`check_response`. """ key = generate_key() @@ -68,10 +44,15 @@ def check_request(headers: Headers) -> str: are usually performed earlier in the HTTP request handling code. They're the responsibility of the caller. - :param headers: request headers - :returns: ``key`` which must be passed to :func:`build_response` - :raises ~websockets.exceptions.InvalidHandshake: if the handshake request - is invalid; then the server must return 400 Bad Request error + Args: + headers: handshake request headers. + + Returns: + str: ``key`` that must be passed to :func:`build_response`. + + Raises: + InvalidHandshake: if the handshake request is invalid; + then the server must return 400 Bad Request error. """ connection: List[ConnectionOption] = sum( @@ -128,8 +109,9 @@ def build_response(headers: Headers, key: str) -> None: Update response headers passed in argument. - :param headers: response headers - :param key: comes from :func:`check_request` + Args: + headers: handshake response headers. + key: returned by :func:`check_request`. """ headers["Upgrade"] = "websocket" @@ -145,10 +127,12 @@ def check_response(headers: Headers, key: str) -> None: response with a 101 status code. These controls are the responsibility of the caller. - :param headers: response headers - :param key: comes from :func:`build_request` - :raises ~websockets.exceptions.InvalidHandshake: if the handshake response - is invalid + Args: + headers: handshake response headers. + key: returned by :func:`build_request`. + + Raises: + InvalidHandshake: if the handshake response is invalid. """ connection: List[ConnectionOption] = sum( diff --git a/src/websockets/legacy/http.py b/src/websockets/legacy/http.py index 3725fa9c3..cc2ef1f06 100644 --- a/src/websockets/legacy/http.py +++ b/src/websockets/legacy/http.py @@ -55,10 +55,13 @@ async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]: WebSocket handshake requests don't have one. If the request contains a body, it may be read from ``stream`` after this coroutine returns. - :param stream: input to read the request from - :raises EOFError: if the connection is closed without a full HTTP request - :raises SecurityError: if the request exceeds a security limit - :raises ValueError: if the request isn't well formatted + Args: + stream: input to read the request from + + Raises: + EOFError: if the connection is closed without a full HTTP request + SecurityError: if the request exceeds a security limit + ValueError: if the request isn't well formatted """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1 @@ -99,10 +102,13 @@ async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers WebSocket handshake responses don't have one. If the response contains a body, it may be read from ``stream`` after this coroutine returns. - :param stream: input to read the response from - :raises EOFError: if the connection is closed without a full HTTP response - :raises SecurityError: if the response exceeds a security limit - :raises ValueError: if the response isn't well formatted + Args: + stream: input to read the response from + + Raises: + EOFError: if the connection is closed without a full HTTP response + SecurityError: if the response exceeds a security limit + ValueError: if the response isn't well formatted """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2 diff --git a/src/websockets/legacy/protocol.py b/src/websockets/legacy/protocol.py index 99a821be6..4631151e6 100644 --- a/src/websockets/legacy/protocol.py +++ b/src/websockets/legacy/protocol.py @@ -1,12 +1,3 @@ -""" -:mod:`websockets.legacy.protocol` handles WebSocket control and data frames. - -See `sections 4 to 8 of RFC 6455`_. - -.. _sections 4 to 8 of RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4 - -""" - from __future__ import annotations import asyncio @@ -71,17 +62,99 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ - :class:`~asyncio.Protocol` subclass implementing the data transfer phase. - - Once the WebSocket connection is established, during the data transfer - phase, the protocol is almost symmetrical between the server side and the - client side. :class:`WebSocketCommonProtocol` implements logic that's - shared between servers and clients. - - Subclasses such as - :class:`~websockets.legacy.server.WebSocketServerProtocol` and - :class:`~websockets.legacy.client.WebSocketClientProtocol` implement the - opening handshake, which is different between servers and clients. + WebSocket connection. + + :class:`WebSocketCommonProtocol` provides APIs shared between WebSocket + servers and clients. You shouldn't use it directly. Instead, use + :class:`~websockets.client.WebSocketClientProtocol` or + :class:`~websockets.server.WebSocketServerProtocol`. + + This documentation focuses on low-level details that aren't covered in the + documentation of :class:`~websockets.client.WebSocketClientProtocol` and + :class:`~websockets.server.WebSocketServerProtocol` for the sake of + simplicity. + + Once the connection is open, a Ping_ frame is sent every ``ping_interval`` + seconds. This serves as a keepalive. It helps keeping the connection + open, especially in the presence of proxies with short timeouts on + inactive connections. Set ``ping_interval`` to :obj:`None` to disable + this behavior. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 + + If the corresponding Pong_ frame isn't received within ``ping_timeout`` + seconds, the connection is considered unusable and is closed with code + 1011. This ensures that the remote endpoint remains responsive. Set + ``ping_timeout`` to :obj:`None` to disable this behavior. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + + The ``close_timeout`` parameter defines a maximum wait time for completing + the closing handshake and terminating the TCP connection. For legacy + reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds + for clients and ``4 * close_timeout`` for servers. + + See the discussion of :doc:`timeouts <../topics/timeouts>` for details. + + ``close_timeout`` needs to be a parameter of the protocol because + websockets usually calls :meth:`close` implicitly upon exit: + + * on the client side, when :func:`~websockets.client.connect` is used as a + context manager; + * on the server side, when the connection handler terminates; + + To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`. + + The ``max_size`` parameter enforces the maximum size for incoming messages + in bytes. The default value is 1 MiB. If a larger message is received, + :meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError` + and the connection will be closed with code 1009. + + The ``max_queue`` parameter sets the maximum length of the queue that + holds incoming messages. The default value is ``32``. Messages are added + to an in-memory queue when they're received; then :meth:`recv` pops from + that queue. In order to prevent excessive memory consumption when + messages are received faster than they can be processed, the queue must + be bounded. If the queue fills up, the protocol stops processing incoming + data until :meth:`recv` is called. In this situation, various receive + buffers (at least in :mod:`asyncio` and in the OS) will fill up, then the + TCP receive window will shrink, slowing down transmission to avoid packet + loss. + + Since Python can use up to 4 bytes of memory to represent a single + character, each connection may use up to ``4 * max_size * max_queue`` + bytes of memory to store incoming messages. By default, this is 128 MiB. + You may want to lower the limits, depending on your application's + requirements. + + The ``read_limit`` argument sets the high-water limit of the buffer for + incoming bytes. The low-water limit is half the high-water limit. The + default value is 64 KiB, half of asyncio's default (based on the current + implementation of :class:`~asyncio.StreamReader`). + + The ``write_limit`` argument sets the high-water limit of the buffer for + outgoing bytes. The low-water limit is a quarter of the high-water limit. + The default value is 64 KiB, equal to asyncio's default (based on the + current implementation of ``FlowControlMixin``). + + See the discussion of :doc:`memory usage <../topics/memory>` for details. + + Args: + logger: logger for this connection; + defaults to ``logging.getLogger("websockets.protocol")``; + see the :doc:`logging guide <../topics/logging>` for details. + ping_interval: delay between keepalive pings in seconds; + :obj:`None` to disable keepalive pings. + ping_timeout: timeout for keepalive pings in seconds; + :obj:`None` to disable timeouts. + close_timeout: timeout for closing the connection in seconds; + for legacy reasons, the actual timeout is 4 or 5 times larger. + max_size: maximum size of incoming messages in bytes; + :obj:`None` to disable the limit. + max_queue: maximum number of incoming messages in receive buffer; + :obj:`None` to disable the limit. + read_limit: high-water mark of read buffer in bytes. + write_limit: high-water mark of write buffer in bytes. """ @@ -94,6 +167,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): def __init__( self, *, + logger: Optional[LoggerLike] = None, ping_interval: Optional[float] = 20, ping_timeout: Optional[float] = 20, close_timeout: Optional[float] = None, @@ -101,14 +175,13 @@ def __init__( max_queue: Optional[int] = 2 ** 5, read_limit: int = 2 ** 16, write_limit: int = 2 ** 16, - logger: Optional[LoggerLike] = None, # The following arguments are kept only for backwards compatibility. host: Optional[str] = None, port: Optional[int] = None, secure: Optional[bool] = None, - timeout: Optional[float] = None, legacy_recv: bool = False, loop: Optional[asyncio.AbstractEventLoop] = None, + timeout: Optional[float] = None, ) -> None: # Backwards compatibility: close_timeout used to be called timeout. if timeout is None: @@ -134,7 +207,8 @@ def __init__( self.write_limit = write_limit # Unique identifier. For logs. - self.id = uuid.uuid4() + self.id: uuid.UUID = uuid.uuid4() + """Unique identifier of the connection. Useful in logs.""" # Logger or LoggerAdapter for this connection. if logger is None: @@ -175,12 +249,16 @@ def __init__( # HTTP protocol parameters. self.path: str + """Path of the opening handshake request.""" self.request_headers: Headers + """Opening handshake request headers.""" self.response_headers: Headers + """Opening handshake response headers.""" # WebSocket protocol parameters. self.extensions: List[Extension] = [] self.subprotocol: Optional[Subprotocol] = None + """Subprotocol, if one was negotiated.""" # Close code and reason, set when a close frame is sent or received. self.close_rcvd: Optional[Close] = None @@ -286,9 +364,14 @@ def secure(self) -> Optional[bool]: @property def local_address(self) -> Any: """ - Local address of the connection as a ``(host, port)`` tuple. + Local address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family; + see :meth:`~socket.socket.getsockname`. - When the connection isn't open, ``local_address`` is ``None``. + :obj:`None` if the TCP connection isn't established yet. """ try: @@ -301,9 +384,14 @@ def local_address(self) -> Any: @property def remote_address(self) -> Any: """ - Remote address of the connection as a ``(host, port)`` tuple. + Remote address of the connection. - When the connection isn't open, ``remote_address`` is ``None``. + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family; + see :meth:`~socket.socket.getpeername`. + + :obj:`None` if the TCP connection isn't established yet. """ try: @@ -316,13 +404,11 @@ def remote_address(self) -> Any: @property def open(self) -> bool: """ - ``True`` when the connection is usable. + :obj:`True` when the connection is open; :obj:`False` otherwise. - It may be used to detect disconnections. However, this approach is - discouraged per the EAFP_ principle. - - When ``open`` is ``False``, using the connection raises a - :exc:`~websockets.exceptions.ConnectionClosed` exception. + This attribute may be used to detect disconnections. However, this + approach is discouraged per the EAFP_ principle. Instead, you should + handle :exc:`~websockets.exceptions.ConnectionClosed` exceptions. .. _EAFP: https://docs.python.org/3/glossary.html#term-eafp @@ -332,10 +418,10 @@ def open(self) -> bool: @property def closed(self) -> bool: """ - ``True`` once the connection is closed. + :obj:`True` when the connection is closed; :obj:`False` otherwise. - Be aware that both :attr:`open` and :attr:`closed` are ``False`` during - the opening and closing sequences. + Be aware that both :attr:`open` and :attr:`closed` are :obj:`False` + during the opening and closing sequences. """ return self.state is State.CLOSED @@ -343,9 +429,12 @@ def closed(self) -> bool: @property def close_code(self) -> Optional[int]: """ - WebSocket close code received in a close frame. + WebSocket close code, defined in `section 7.1.5 of RFC 6455`_. + + .. _section 7.1.5 of RFC 6455: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 - Available once the connection is closed. + :obj:`None` if the connection isn't closed yet. """ if self.state is not State.CLOSED: @@ -358,9 +447,12 @@ def close_code(self) -> Optional[int]: @property def close_reason(self) -> Optional[str]: """ - WebSocket close reason received in a close frame. + WebSocket close reason, defined in `section 7.1.6 of RFC 6455`_. - Available once the connection is closed. + .. _section 7.1.6 of RFC 6455: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 + + :obj:`None` if the connection isn't closed yet. """ if self.state is not State.CLOSED: @@ -370,25 +462,14 @@ def close_reason(self) -> Optional[str]: else: return self.close_rcvd.reason - async def wait_closed(self) -> None: - """ - Wait until the connection is closed. - - This is identical to :attr:`closed`, except it can be awaited. - - This can make it easier to handle connection termination, regardless - of its cause, in tasks that interact with the WebSocket connection. - - """ - await asyncio.shield(self.connection_lost_waiter) - async def __aiter__(self) -> AsyncIterator[Data]: """ - Iterate on received messages. - - Exit normally when the connection is closed with code 1000 or 1001. + Iterate on incoming messages. - Raise an exception in other cases. + The iterator exits normally when the connection is closed with the + close code 1000 (OK) or 1001(going away). It raises + a :exc:`~websockets.exceptions.ConnectionClosedError` exception when + the connection is closed with any other code. """ try: @@ -401,24 +482,30 @@ async def recv(self) -> Data: """ Receive the next message. - Return a :class:`str` for a text frame and :class:`bytes` for a binary - frame. - - When the end of the message stream is reached, :meth:`recv` raises + When the connection is closed, :meth:`recv` raises :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal connection closure and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol - error or a network failure. + error or a network failure. This is how you detect the end of the + message stream. Canceling :meth:`recv` is safe. There's no risk of losing the next - message. The next invocation of :meth:`recv` will return it. This - makes it possible to enforce a timeout by wrapping :meth:`recv` in - :func:`~asyncio.wait_for`. + message. The next invocation of :meth:`recv` will return it. + + This makes it possible to enforce a timeout by wrapping :meth:`recv` + in :func:`~asyncio.wait_for`. - :raises ~websockets.exceptions.ConnectionClosed: when the - connection is closed - :raises RuntimeError: if two coroutines call :meth:`recv` concurrently + Returns: + Data: A string (:class:`str`) for a Text_ frame. A bytestring + (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: when the connection is closed. + RuntimeError: if two coroutines call :meth:`recv` concurrently. """ if self._pop_message_waiter is not None: @@ -471,43 +558,58 @@ async def recv(self) -> Data: return message async def send( - self, message: Union[Data, Iterable[Data], AsyncIterable[Data]] + self, + message: Union[Data, Iterable[Data], AsyncIterable[Data]], ) -> None: """ Send a message. - A string (:class:`str`) is sent as a `Text frame`_. A bytestring or + A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a `Binary frame`_. + :class:`memoryview`) is sent as a Binary_ frame. - .. _Text frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 - .. _Binary frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 :meth:`send` also accepts an iterable or an asynchronous iterable of - strings, bytestrings, or bytes-like objects. In that case the message - is fragmented. Each item is treated as a message fragment and sent in - its own frame. All items must be of the same type, or else - :meth:`send` will raise a :exc:`TypeError` and the connection will be - closed. + strings, bytestrings, or bytes-like objects to enable fragmentation_. + Each item is treated as a message fragment and sent in its own frame. + All items must be of the same type, or else :meth:`send` will raise a + :exc:`TypeError` and the connection will be closed. + + .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4 :meth:`send` rejects dict-like objects because this is often an error. - If you wish to send the keys of a dict-like object as fragments, call - its :meth:`~dict.keys` method and pass the result to :meth:`send`. + (If you want to send the keys of a dict-like object as fragments, call + its :meth:`~dict.keys` method and pass the result to :meth:`send`.) Canceling :meth:`send` is discouraged. Instead, you should close the connection with :meth:`close`. Indeed, there are only two situations - where :meth:`send` may yield control to the event loop: + where :meth:`send` may yield control to the event loop and then get + canceled; in both cases, :meth:`close` has the same effect and is + more clear: 1. The write buffer is full. If you don't want to wait until enough data is sent, your only alternative is to close the connection. :meth:`close` will likely time out then abort the TCP connection. 2. ``message`` is an asynchronous iterator that yields control. Stopping in the middle of a fragmented message will cause a - protocol error. Closing the connection has the same effect. + protocol error and the connection will be closed. + + When the connection is closed, :meth:`send` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it + raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + connection closure and + :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. - :raises ~websockets.exceptions.ConnectionClosed: when the - connection is closed - :raises TypeError: if ``message`` doesn't have a supported type + Args: + message (Union[Data, Iterable[Data], AsyncIterable[Data]): message + to send. + + Raises: + ConnectionClosed: when the connection is closed. + TypeError: if ``message`` doesn't have a supported type. """ await self.ensure_open() @@ -621,7 +723,7 @@ async def close(self, code: int = 1000, reason: str = "") -> None: :meth:`close` waits for the other end to complete the handshake and for the TCP connection to terminate. As a consequence, there's no need - to await :meth:`wait_closed`; :meth:`close` already does it. + to await :meth:`wait_closed` after :meth:`close`. :meth:`close` is idempotent: it doesn't do anything once the connection is closed. @@ -631,10 +733,12 @@ async def close(self, code: int = 1000, reason: str = "") -> None: Canceling :meth:`close` is discouraged. If it takes too long, you can set a shorter ``close_timeout``. If you don't want to wait, let the - Python process exit, then the OS will close the TCP connection. + Python process exit, then the OS will take care of closing the TCP + connection. - :param code: WebSocket close code - :param reason: WebSocket close reason + Args: + code: WebSocket close code. + reason: WebSocket close reason. """ try: @@ -653,7 +757,7 @@ async def close(self, code: int = 1000, reason: str = "") -> None: # the data transfer task and raises TimeoutError. # If close() is called multiple times concurrently and one of these - # calls hits the timeout, the data transfer task will be cancelled. + # calls hits the timeout, the data transfer task will be canceled. # Other calls will receive a CancelledError here. try: @@ -670,23 +774,27 @@ async def close(self, code: int = 1000, reason: str = "") -> None: # Wait for the close connection task to close the TCP connection. await asyncio.shield(self.close_connection_task) - async def ping(self, data: Optional[Data] = None) -> Awaitable[None]: + async def wait_closed(self) -> None: """ - Send a ping. + Wait until the connection is closed. - Return a :class:`~asyncio.Future` that will be completed when the - corresponding pong is received. You can ignore it if you don't intend - to wait. + This coroutine is identical to the :attr:`closed` attribute, except it + can be awaited. - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point:: + This can make it easier to detect connection termination, regardless + of its cause, in tasks that interact with the WebSocket connection. - pong_waiter = await ws.ping() - await pong_waiter # only if you want to wait for the pong + """ + await asyncio.shield(self.connection_lost_waiter) + + async def ping(self, data: Optional[Data] = None) -> Awaitable[None]: + """ + Send a Ping_. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 - By default, the ping contains four random bytes. This payload may be - overridden with the optional ``data`` argument which must be a string - (which will be encoded to UTF-8) or a bytes-like object. + A ping may serve as a keepalive or as a check that the remote endpoint + received all messages up to this point Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return immediately, it means the write buffer is full. If you don't want to @@ -695,10 +803,25 @@ async def ping(self, data: Optional[Data] = None) -> Awaitable[None]: Canceling the :class:`~asyncio.Future` returned by :meth:`ping` has no effect. - :raises ~websockets.exceptions.ConnectionClosed: when the - connection is closed - :raises RuntimeError: if another ping was sent with the same data and - the corresponding pong wasn't received yet + Args: + data (Optional[Data]): payload of the ping; a string will be + encoded to UTF-8; or :obj:`None` to generate a payload + containing four random bytes. + + Returns: + ~asyncio.Future: A future that will be completed when the + corresponding pong is received. You can ignore it if you + don't intend to wait. + + :: + + pong_waiter = await ws.ping() + await pong_waiter # only if you want to wait for the pong + + Raises: + ConnectionClosed: when the connection is closed. + RuntimeError: if another ping was sent with the same data and + the corresponding pong wasn't received yet. """ await self.ensure_open() @@ -722,18 +845,22 @@ async def ping(self, data: Optional[Data] = None) -> Awaitable[None]: async def pong(self, data: Data = b"") -> None: """ - Send a pong. + Send a Pong_. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 An unsolicited pong may serve as a unidirectional heartbeat. - The payload may be set with the optional ``data`` argument which must - be a string (which will be encoded to UTF-8) or a bytes-like object. + Canceling :meth:`pong` is discouraged. If :meth:`pong` doesn't return + immediately, it means the write buffer is full. If you don't want to + wait, you should close the connection. - Canceling :meth:`pong` is discouraged for the same reason as - :meth:`ping`. + Args: + data (Data): payload of the pong; a string will be encoded to + UTF-8. - :raises ~websockets.exceptions.ConnectionClosed: when the - connection is closed + Raises: + ConnectionClosed: when the connection is closed. """ await self.ensure_open() @@ -876,7 +1003,7 @@ async def read_message(self) -> Optional[Data]: Re-assemble data frames if the message is fragmented. - Return ``None`` when the closing handshake is started. + Return :obj:`None` when the closing handshake is started. """ frame = await self.read_data_frame(max_size=self.max_size) @@ -950,7 +1077,7 @@ async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]: Process control frames received before the next data frame. - Return ``None`` if a close frame is encountered before any data frame. + Return :obj:`None` if a close frame is encountered before any data frame. """ # 6.2. Receiving Data @@ -1224,7 +1351,8 @@ async def wait_for_connection_lost(self) -> bool: """ Wait until the TCP connection is closed or ``self.close_timeout`` elapses. - Return ``True`` if the connection is closed and ``False`` otherwise. + Return :obj:`True` if the connection is closed and :obj:`False` + otherwise. """ if not self.connection_lost_waiter.done(): @@ -1404,7 +1532,7 @@ def eof_received(self) -> None: the TCP or TLS connection after sending and receiving a close frame. As a consequence, they never need to write after receiving EOF, so - there's no reason to keep the transport open by returning ``True``. + there's no reason to keep the transport open by returning :obj:`True`. Besides, that doesn't work on TLS connections. @@ -1416,15 +1544,15 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N """ Broadcast a message to several WebSocket connections. - A string (:class:`str`) is sent as a `Text frame`_. A bytestring or + A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a `Binary frame`_. + :class:`memoryview`) is sent as a Binary_ frame. - .. _Text frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 - .. _Binary frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 :func:`broadcast` pushes the message synchronously to all connections even - if their write buffers overflow ``write_limit``. There's no backpressure. + if their write buffers are overflowing. There's no backpressure. :func:`broadcast` skips silently connections that aren't open in order to avoid errors on connections where the closing handshake is in progress. @@ -1440,8 +1568,14 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N them in memory, while :func:`broadcast` buffers one copy per connection as fast as possible. - :raises RuntimeError: if a connection is busy sending a fragmented message - :raises TypeError: if ``message`` doesn't have a supported type + Args: + websockets (Iterable[WebSocketCommonProtocol]): WebSocket connections + to which the message will be sent. + message (Data): message to send. + + Raises: + RuntimeError: if a connection is busy sending a fragmented message. + TypeError: if ``message`` doesn't have a supported type. """ if not isinstance(message, (str, bytes, bytearray, memoryview)): diff --git a/src/websockets/legacy/server.py b/src/websockets/legacy/server.py index ddf6d9f87..673888c3d 100644 --- a/src/websockets/legacy/server.py +++ b/src/websockets/legacy/server.py @@ -1,8 +1,3 @@ -""" -:mod:`websockets.legacy.server` defines the WebSocket server APIs. - -""" - from __future__ import annotations import asyncio @@ -65,109 +60,33 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): """ - :class:`~asyncio.Protocol` subclass implementing a WebSocket server. + WebSocket server connection. - :class:`WebSocketServerProtocol`: + :class:`WebSocketServerProtocol` provides :meth:`recv` and :meth:`send` + coroutines for receiving and sending messages. - * performs the opening handshake to establish the connection; - * provides :meth:`recv` and :meth:`send` coroutines for receiving and - sending messages; - * deals with control frames automatically; - * performs the closing handshake to terminate the connection. + It supports asynchronous iteration to receive messages:: - You may customize the opening handshake by subclassing - :class:`WebSocketServer` and overriding: + async for message in websocket: + await process(message) - * :meth:`process_request` to intercept the client request before any - processing and, if appropriate, to abort the WebSocket request and - return a HTTP response instead; - * :meth:`select_subprotocol` to select a subprotocol, if the client and - the server have multiple subprotocols in common and the default logic - for choosing one isn't suitable (this is rarely needed). + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away). It raises + a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection + is closed with any other code. - :class:`WebSocketServerProtocol` supports asynchronous iteration:: + You may customize the opening handshake in a subclass by + overriding :meth:`process_request` or :meth:`select_subprotocol`. - async for message in websocket: - await process(message) + Args: + ws_server: WebSocket server that created this connection. - The iterator yields incoming messages. It exits normally when the - connection is closed with the close code 1000 (OK) or 1001 (going away). - It raises a :exc:`~websockets.exceptions.ConnectionClosedError` exception - when the connection is closed with any other code. - - Once the connection is open, a `Ping frame`_ is sent every - ``ping_interval`` seconds. This serves as a keepalive. It helps keeping - the connection open, especially in the presence of proxies with short - timeouts on inactive connections. Set ``ping_interval`` to ``None`` to - disable this behavior. - - .. _Ping frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 - - If the corresponding `Pong frame`_ isn't received within ``ping_timeout`` - seconds, the connection is considered unusable and is closed with - code 1011. This ensures that the remote endpoint remains responsive. Set - ``ping_timeout`` to ``None`` to disable this behavior. - - .. _Pong frame: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 - - The ``close_timeout`` parameter defines a maximum wait time for completing - the closing handshake and terminating the TCP connection. For legacy - reasons, :meth:`close` completes in at most ``4 * close_timeout`` seconds. - - ``close_timeout`` needs to be a parameter of the protocol because - websockets usually calls :meth:`close` implicitly when the connection - handler terminates. - - To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`. - - The ``max_size`` parameter enforces the maximum size for incoming messages - in bytes. The default value is 1 MiB. ``None`` disables the limit. If a - message larger than the maximum size is received, :meth:`recv` will - raise :exc:`~websockets.exceptions.ConnectionClosedError` and the - connection will be closed with code 1009. - - The ``max_queue`` parameter sets the maximum length of the queue that - holds incoming messages. The default value is ``32``. ``None`` disables - the limit. Messages are added to an in-memory queue when they're received; - then :meth:`recv` pops from that queue. In order to prevent excessive - memory consumption when messages are received faster than they can be - processed, the queue must be bounded. If the queue fills up, the protocol - stops processing incoming data until :meth:`recv` is called. In this - situation, various receive buffers (at least in :mod:`asyncio` and in the - OS) will fill up, then the TCP receive window will shrink, slowing down - transmission to avoid packet loss. - - Since Python can use up to 4 bytes of memory to represent a single - character, each connection may use up to ``4 * max_size * max_queue`` - bytes of memory to store incoming messages. By default, this is 128 MiB. - You may want to lower the limits, depending on your application's - requirements. - - The ``read_limit`` argument sets the high-water limit of the buffer for - incoming bytes. The low-water limit is half the high-water limit. The - default value is 64 KiB, half of asyncio's default (based on the current - implementation of :class:`~asyncio.StreamReader`). - - The ``write_limit`` argument sets the high-water limit of the buffer for - outgoing bytes. The low-water limit is a quarter of the high-water limit. - The default value is 64 KiB, equal to asyncio's default (based on the - current implementation of ``FlowControlMixin``). - - As soon as the HTTP request and response in the opening handshake are - processed: - - * the request path is available in the :attr:`path` attribute; - * the request and response HTTP headers are available in the - :attr:`request_headers` and :attr:`response_headers` attributes, - which are :class:`~websockets.http.Headers` instances. - - If a subprotocol was negotiated, it's available in the :attr:`subprotocol` - attribute. - - Once the connection is closed, the code is available in the - :attr:`close_code` attribute and the reason in :attr:`close_reason`. - - All attributes must be treated as read-only. + See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``, + ``extensions``, ``subprotocols``, and ``extra_headers``. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. """ @@ -179,6 +98,7 @@ def __init__( ws_handler: Callable[[WebSocketServerProtocol, str], Awaitable[Any]], ws_server: WebSocketServer, *, + logger: Optional[LoggerLike] = None, origins: Optional[Sequence[Optional[Origin]]] = None, extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, @@ -189,7 +109,6 @@ def __init__( select_subprotocol: Optional[ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] ] = None, - logger: Optional[LoggerLike] = None, **kwargs: Any, ) -> None: if logger is None: @@ -339,8 +258,9 @@ async def read_http_request(self) -> Tuple[str, Headers]: If the request contains a body, it may be read from ``self.reader`` after this coroutine returns. - :raises ~websockets.exceptions.InvalidMessage: if the HTTP message is - malformed or isn't an HTTP/1.1 GET request + Raises: + InvalidMessage: if the HTTP message is malformed or isn't an + HTTP/1.1 GET request. """ try: @@ -394,18 +314,7 @@ async def process_request( """ Intercept the HTTP request and return an HTTP response if appropriate. - If ``process_request`` returns ``None``, the WebSocket handshake - continues. If it returns 3-uple containing a status code, response - headers and a response body, that HTTP response is sent and the - connection is closed. In that case: - - * The HTTP status must be a :class:`~http.HTTPStatus`. - * HTTP headers must be a :class:`~websockets.http.Headers` instance, a - :class:`~collections.abc.Mapping`, or an iterable of ``(name, - value)`` pairs. - * The HTTP response body must be :class:`bytes`. It may be empty. - - This coroutine may be overridden in a :class:`WebSocketServerProtocol` + You may override this method in a :class:`WebSocketServerProtocol` subclass, for example: * to return a HTTP 200 OK response on a given path; then a load @@ -413,19 +322,27 @@ async def process_request( * to authenticate the request and return a HTTP 401 Unauthorized or a HTTP 403 Forbidden when authentication fails. - Instead of subclassing, it is possible to override this method by - passing a ``process_request`` argument to the :func:`serve` function - or the :class:`WebSocketServerProtocol` constructor. This is - equivalent, except ``process_request`` won't have access to the + You may also override this method with the ``process_request`` + argument of :func:`serve` and :class:`WebSocketServerProtocol`. This + is equivalent, except ``process_request`` won't have access to the protocol instance, so it can't store information for later use. - ``process_request`` is expected to complete quickly. If it may run for - a long time, then it should await :meth:`wait_closed` and exit if + :meth:`process_request` is expected to complete quickly. If it may run + for a long time, then it should await :meth:`wait_closed` and exit if :meth:`wait_closed` completes, or else it could prevent the server from shutting down. - :param path: request path, including optional query string - :param request_headers: request headers + Args: + path: request path, including optional query string. + request_headers: request headers. + + Returns: + Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]: :obj:`None` + to continue the WebSocket handshake normally. + + An HTTP response, represented by a 3-uple of the response status, + headers, and body, to abort the WebSocket handshake and return + that HTTP response instead. """ if self._process_request is not None: @@ -447,10 +364,12 @@ def process_origin( """ Handle the Origin HTTP request header. - :param headers: request headers - :param origins: optional list of acceptable origins - :raises ~websockets.exceptions.InvalidOrigin: if the origin isn't - acceptable + Args: + headers: request headers. + origins: optional list of acceptable origins. + + Raises: + InvalidOrigin: if the origin isn't acceptable. """ # "The user agent MUST NOT include more than one Origin header field" @@ -496,10 +415,12 @@ def process_extensions( Other requirements, for example related to mandatory extensions or the order of extensions, may be implemented by overriding this method. - :param headers: request headers - :param extensions: optional list of supported extensions - :raises ~websockets.exceptions.InvalidHandshake: to abort the - handshake with an HTTP 400 error code + Args: + headers: request headers. + extensions: optional list of supported extensions. + + Raises: + InvalidHandshake: to abort the handshake with an HTTP 400 error. """ response_header_value: Optional[str] = None @@ -557,10 +478,12 @@ def process_subprotocol( Return Sec-WebSocket-Protocol HTTP response header, which is the same as the selected subprotocol. - :param headers: request headers - :param available_subprotocols: optional list of supported subprotocols - :raises ~websockets.exceptions.InvalidHandshake: to abort the - handshake with an HTTP 400 error code + Args: + headers: request headers. + available_subprotocols: optional list of supported subprotocols. + + Raises: + InvalidHandshake: to abort the handshake with an HTTP 400 error. """ subprotocol: Optional[Subprotocol] = None @@ -588,22 +511,27 @@ def select_subprotocol( Pick a subprotocol among those offered by the client. If several subprotocols are supported by the client and the server, - the default implementation selects the preferred subprotocols by + the default implementation selects the preferred subprotocol by giving equal value to the priorities of the client and the server. - If no subprotocol is supported by the client and the server, it proceeds without a subprotocol. - This is unlikely to be the most useful implementation in practice, as - many servers providing a subprotocol will require that the client uses - that subprotocol. Such rules can be implemented in a subclass. + This is unlikely to be the most useful implementation in practice. + Many servers providing a subprotocol will require that the client + uses that subprotocol. Such rules can be implemented in a subclass. + + You may also override this method with the ``select_subprotocol`` + argument of :func:`serve` and :class:`WebSocketServerProtocol`. - Instead of subclassing, it is possible to override this method by - passing a ``select_subprotocol`` argument to the :func:`serve` - function or the :class:`WebSocketServerProtocol` constructor. + Args: + client_subprotocols: list of subprotocols offered by the client. + server_subprotocols: list of subprotocols available on the server. + + Returns: + Optional[Subprotocol]: Selected subprotocol. + + :obj:`None` to continue without a subprotocol. - :param client_subprotocols: list of subprotocols offered by the client - :param server_subprotocols: list of subprotocols available on the server """ if self._select_subprotocol is not None: @@ -629,19 +557,18 @@ async def handshake( Return the path of the URI of the request. - :param origins: list of acceptable values of the Origin HTTP header; - include ``None`` if the lack of an origin is acceptable - :param available_extensions: list of supported extensions in the order - in which they should be used - :param available_subprotocols: list of supported subprotocols in order - of decreasing preference - :param extra_headers: sets additional HTTP response headers when the - handshake succeeds; it can be a :class:`~websockets.http.Headers` - instance, a :class:`~collections.abc.Mapping`, an iterable of - ``(name, value)`` pairs, or a callable taking the request path and - headers in arguments and returning one of the above. - :raises ~websockets.exceptions.InvalidHandshake: if the handshake - fails + Args: + origins: list of acceptable values of the Origin HTTP header; + include :obj:`None` if the lack of an origin is acceptable. + extensions: list of supported extensions, in order in which they + should be tried. + subprotocols: list of supported subprotocols, in order of + decreasing preference. + extra_headers: arbitrary HTTP headers to add to the response when + the handshake succeeds. + + Raises: + InvalidHandshake: if the handshake fails. """ path, request_headers = await self.read_http_request() @@ -714,24 +641,24 @@ class WebSocketServer: """ WebSocket server returned by :func:`serve`. - This class provides the same interface as - :class:`~asyncio.AbstractServer`, namely the - :meth:`~asyncio.AbstractServer.close` and - :meth:`~asyncio.AbstractServer.wait_closed` methods. + This class provides the same interface as :class:`~asyncio.Server`, + notably the :meth:`~asyncio.Server.close` + and :meth:`~asyncio.Server.wait_closed` methods. It keeps track of WebSocket connections in order to close them properly when shutting down. - Instances of this class store a reference to the :class:`~asyncio.Server` - object returned by :meth:`~asyncio.loop.create_server` rather than inherit - from :class:`~asyncio.Server` in part because - :meth:`~asyncio.loop.create_server` doesn't support passing a custom - :class:`~asyncio.Server` class. + Args: + logger: logger for this server; + defaults to ``logging.getLogger("websockets.server")``; + see the :doc:`logging guide <../topics/logging>` for details. """ def __init__( - self, loop: asyncio.AbstractEventLoop, logger: Optional[LoggerLike] = None + self, + loop: asyncio.AbstractEventLoop, + logger: Optional[LoggerLike] = None, ) -> None: # Store a reference to loop to avoid relying on self.server._loop. self.loop = loop @@ -874,15 +801,26 @@ async def wait_closed(self) -> None: When :meth:`wait_closed` returns, all TCP connections are closed and all connection handlers have returned. + To ensure a fast shutdown, a connection handler should always be + awaiting at least one of: + + * :meth:`~WebSocketServerProtocol.recv`: when the connection is closed, + it raises :exc:`~websockets.exceptions.ConnectionClosedOK`; + * :meth:`~WebSocketServerProtocol.wait_closed`: when the connection is + closed, it returns. + + Then the connection handler is immediately notified of the shutdown; + it can clean up and exit. + """ await asyncio.shield(self.closed_waiter) @property def sockets(self) -> Optional[List[socket.socket]]: """ - List of :class:`~socket.socket` objects the server is listening to. + List of :obj:`~socket.socket` objects the server is listening on. - ``None`` if the server is closed. + :obj:`None` if the server is closed. """ return self.server.sockets @@ -890,25 +828,21 @@ def sockets(self) -> Optional[List[socket.socket]]: class Serve: """ + Start a WebSocket server listening on ``host`` and ``port``. - Create, start, and return a WebSocket server on ``host`` and ``port``. - - Whenever a client connects, the server accepts the connection, creates a + Whenever a client connects, the server creates a :class:`WebSocketServerProtocol`, performs the opening handshake, and - delegates to the connection handler defined by ``ws_handler``. Once the - handler completes, either normally or with an exception, the server - performs the closing handshake and closes the connection. + delegates to the connection handler, ``ws_handler``. - Awaiting :func:`serve` yields a :class:`WebSocketServer`. This instance - provides :meth:`~WebSocketServer.close` and - :meth:`~WebSocketServer.wait_closed` methods for terminating the server - and cleaning up its resources. + The handler receives the :class:`WebSocketServerProtocol` and uses it to + send and receive messages. + + Once the handler completes, either normally or with an exception, the + server performs the closing handshake and closes the connection. - When a server is closed with :meth:`~WebSocketServer.close`, it closes all - connections with close code 1001 (going away). Connections handlers, which - are running the ``ws_handler`` coroutine, will receive a - :exc:`~websockets.exceptions.ConnectionClosedOK` exception on their - current or next interaction with the WebSocket connection. + Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object + provides :meth:`~WebSocketServer.close` and + :meth:`~WebSocketServer.wait_closed` methods for shutting down the server. :func:`serve` can be used as an asynchronous context manager:: @@ -917,64 +851,61 @@ class Serve: async with serve(...): await stop - In this case, the server is shut down when exiting the context. - - :func:`serve` is a wrapper around the event loop's - :meth:`~asyncio.loop.create_server` method. It creates and starts a - :class:`asyncio.Server` with :meth:`~asyncio.loop.create_server`. Then it - wraps the :class:`asyncio.Server` in a :class:`WebSocketServer` and - returns the :class:`WebSocketServer`. - - ``ws_handler`` is the WebSocket handler. It must be a coroutine accepting - two arguments: the WebSocket connection, which is an instance of - :class:`WebSocketServerProtocol`, and the path of the request. - - The ``host`` and ``port`` arguments, as well as unrecognized keyword - arguments, are passed to :meth:`~asyncio.loop.create_server`. - - For example, you can set the ``ssl`` keyword argument to a - :class:`~ssl.SSLContext` to enable TLS. - - ``create_protocol`` defaults to :class:`WebSocketServerProtocol`. It may - be replaced by a wrapper or a subclass to customize the protocol that - manages the connection. - - The behavior of ``ping_interval``, ``ping_timeout``, ``close_timeout``, - ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit`` is - described in :class:`WebSocketServerProtocol`. - - :func:`serve` also accepts the following optional arguments: - - * ``compression`` is a shortcut to configure compression extensions; - by default it enables the "permessage-deflate" extension; set it to - ``None`` to disable compression. - * ``origins`` defines acceptable Origin HTTP headers; include ``None`` in - the list if the lack of an origin is acceptable. - * ``extensions`` is a list of supported extensions in order of - decreasing preference. - * ``subprotocols`` is a list of supported subprotocols in order of - decreasing preference. - * ``extra_headers`` sets additional HTTP response headers when the - handshake succeeds; it can be a :class:`~websockets.http.Headers` - instance, a :class:`~collections.abc.Mapping`, an iterable of ``(name, - value)`` pairs, or a callable taking the request path and headers in - arguments and returning one of the above. - * ``process_request`` allows intercepting the HTTP request; it must be a - coroutine taking the request path and headers in argument; see - :meth:`~WebSocketServerProtocol.process_request` for details. - * ``select_subprotocol`` allows customizing the logic for selecting a - subprotocol; it must be a callable taking the subprotocols offered by - the client and available on the server in argument; see - :meth:`~WebSocketServerProtocol.select_subprotocol` for details. - - Since there's no useful way to propagate exceptions triggered in handlers, - they're sent to the ``"websockets.server"`` logger instead. - Debugging is much easier if you configure logging to print them:: - - import logging - logger = logging.getLogger("websockets.server") - logger.setLevel(logging.ERROR) - logger.addHandler(logging.StreamHandler()) + The server is shut down automatically when exiting the context. + + Args: + ws_handler: connection handler. It must be a coroutine accepting + two arguments: the WebSocket connection, which is a + :class:`WebSocketServerProtocol`, and the path of the request. + host: network interfaces the server is bound to; + see :meth:`~asyncio.loop.create_server` for details. + port: TCP port the server listens on; + see :meth:`~asyncio.loop.create_server` for details. + create_protocol: factory for the :class:`asyncio.Protocol` managing + the connection; defaults to :class:`WebSocketServerProtocol`; may + be set to a wrapper or a subclass to customize connection handling. + logger: logger for this server; + defaults to ``logging.getLogger("websockets.server")``; + see the :doc:`logging guide <../topics/logging>` for details. + compression: shortcut that enables the "permessage-deflate" extension + by default; may be set to :obj:`None` to disable compression; + see the :doc:`compression guide <../topics/compression>` for details. + origins: acceptable values of the ``Origin`` header; include + :obj:`None` in the list if the lack of an origin is acceptable. + This is useful for defending against Cross-Site WebSocket + Hijacking attacks. + extensions: list of supported extensions, in order in which they + should be tried. + subprotocols: list of supported subprotocols, in order of decreasing + preference. + extra_headers (Union[HeadersLike, Callable[[str, Headers], HeadersLike]]): + arbitrary HTTP headers to add to the request; this can be + a :data:`~websockets.datastructures.HeadersLike` or a callable + taking the request path and headers in arguments and returning + a :data:`~websockets.datastructures.HeadersLike`. + process_request (Optional[Callable[[str, Headers], \ + Awaitable[Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]]]]): + intercept HTTP request before the opening handshake; + see :meth:`~WebSocketServerProtocol.process_request` for details. + select_subprotocol: select a subprotocol supported by the client; + see :meth:`~WebSocketServerProtocol.select_subprotocol` for details. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. + + Any other keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_server` method. + + For example: + + * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enable TLS. + + * You can set ``sock`` to a :obj:`~socket.socket` that you created + outside of websockets. + + Returns: + WebSocketServer: WebSocket server. """ @@ -985,13 +916,7 @@ def __init__( port: Optional[int] = None, *, create_protocol: Optional[Callable[[Any], WebSocketServerProtocol]] = None, - ping_interval: Optional[float] = 20, - ping_timeout: Optional[float] = 20, - close_timeout: Optional[float] = None, - max_size: Optional[int] = 2 ** 20, - max_queue: Optional[int] = 2 ** 5, - read_limit: int = 2 ** 16, - write_limit: int = 2 ** 16, + logger: Optional[LoggerLike] = None, compression: Optional[str] = "deflate", origins: Optional[Sequence[Optional[Origin]]] = None, extensions: Optional[Sequence[ServerExtensionFactory]] = None, @@ -1003,7 +928,13 @@ def __init__( select_subprotocol: Optional[ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] ] = None, - logger: Optional[LoggerLike] = None, + ping_interval: Optional[float] = 20, + ping_timeout: Optional[float] = 20, + close_timeout: Optional[float] = None, + max_size: Optional[int] = 2 ** 20, + max_queue: Optional[int] = 2 ** 5, + read_limit: int = 2 ** 16, + write_limit: int = 2 ** 16, **kwargs: Any, ) -> None: # Backwards compatibility: close_timeout used to be called timeout. @@ -1131,14 +1062,15 @@ def unix_serve( """ Similar to :func:`serve`, but for listening on Unix sockets. - This function calls the event loop's - :meth:`~asyncio.loop.create_unix_server` method. + This function builds upon the event + loop's :meth:`~asyncio.loop.create_unix_server` method. It is only available on Unix. It's useful for deploying a server behind a reverse proxy such as nginx. - :param path: file system path to the Unix socket + Args: + path: file system path to the Unix socket. """ return serve(ws_handler, path=path, unix=True, **kwargs) diff --git a/src/websockets/server.py b/src/websockets/server.py index 0ae0ae940..5f7bec30d 100644 --- a/src/websockets/server.py +++ b/src/websockets/server.py @@ -69,14 +69,24 @@ def __init__( def accept(self, request: Request) -> Response: """ - Create a WebSocket handshake response event to send to the client. + Create a WebSocket handshake response event to accept the connection. - If the connection cannot be established, the response rejects the - connection, which may be unexpected. + If the connection cannot be established, create a HTTP response event + to reject the handshake. + + Args: + request: handshake request event received from the client. + + Returns: + Response: handshake response event to send to the client. """ try: - key, extensions_header, protocol_header = self.process_request(request) + ( + accept_header, + extensions_header, + protocol_header, + ) = self.process_request(request) except InvalidOrigin as exc: request.exception = exc if self.debug: @@ -124,7 +134,7 @@ def accept(self, request: Request) -> Response: headers["Upgrade"] = "websocket" headers["Connection"] = "Upgrade" - headers["Sec-WebSocket-Accept"] = accept_key(key) + headers["Sec-WebSocket-Accept"] = accept_header if extensions_header is not None: headers["Sec-WebSocket-Extensions"] = extensions_header @@ -141,17 +151,24 @@ def process_request( self, request: Request ) -> Tuple[str, Optional[str], Optional[str]]: """ - Check a handshake request received from the client. + Check a handshake request and negociate extensions and subprotocol. - This function doesn't verify that the request is an HTTP/1.1 or higher GET - request and doesn't perform ``Host`` and ``Origin`` checks. These controls - are usually performed earlier in the HTTP request handling code. They're + This function doesn't verify that the request is an HTTP/1.1 or higher + GET request and doesn't check the ``Host`` header. These controls are + usually performed earlier in the HTTP request handling code. They're the responsibility of the caller. - :param request: request - :returns: ``key`` which must be passed to :func:`build_response` - :raises ~websockets.exceptions.InvalidHandshake: if the handshake request - is invalid; then the server must return 400 Bad Request error + Args: + request: WebSocket handshake request received from the client. + + Returns: + Tuple[str, Optional[str], Optional[str]]: + ``Sec-WebSocket-Accept``, ``Sec-WebSocket-Extensions``, and + ``Sec-WebSocket-Protocol`` headers for the handshake response. + + Raises: + InvalidHandshake: if the handshake request is invalid; + then the server must return 400 Bad Request error. """ headers = request.headers @@ -204,21 +221,32 @@ def process_request( if version != "13": raise InvalidHeaderValue("Sec-WebSocket-Version", version) + accept_header = accept_key(key) + self.origin = self.process_origin(headers) extensions_header, self.extensions = self.process_extensions(headers) protocol_header = self.subprotocol = self.process_subprotocol(headers) - return key, extensions_header, protocol_header + return ( + accept_header, + extensions_header, + protocol_header, + ) def process_origin(self, headers: Headers) -> Optional[Origin]: """ Handle the Origin HTTP request header. - :param headers: request headers - :raises ~websockets.exceptions.InvalidOrigin: if the origin isn't - acceptable + Args: + headers: WebSocket handshake request headers. + + Returns: + Optional[Origin]: origin, if it is acceptable. + + Raises: + InvalidOrigin: if the origin isn't acceptable. """ # "The user agent MUST NOT include more than one Origin header field" @@ -242,9 +270,6 @@ def process_extensions( Accept or reject each extension proposed in the client request. Negotiate parameters for accepted extensions. - Return the Sec-WebSocket-Extensions HTTP response header and the list - of accepted extensions. - :rfc:`6455` leaves the rules up to the specification of each :extension. @@ -263,9 +288,15 @@ def process_extensions( Other requirements, for example related to mandatory extensions or the order of extensions, may be implemented by overriding this method. - :param headers: request headers - :raises ~websockets.exceptions.InvalidHandshake: to abort the - handshake with an HTTP 400 error code + Args: + headers: WebSocket handshake request headers. + + Returns: + Tuple[Optional[str], List[Extension]]: ``Sec-WebSocket-Extensions`` + HTTP response header and list of accepted extensions. + + Raises: + InvalidHandshake: to abort the handshake with an HTTP 400 error. """ response_header_value: Optional[str] = None @@ -317,12 +348,15 @@ def process_subprotocol(self, headers: Headers) -> Optional[Subprotocol]: """ Handle the Sec-WebSocket-Protocol HTTP request header. - Return Sec-WebSocket-Protocol HTTP response header, which is the same - as the selected subprotocol. + Args: + headers: WebSocket handshake request headers. + + Returns: + Optional[Subprotocol]: Subprotocol, if one was selected; this is + also the value of the ``Sec-WebSocket-Protocol`` response header. - :param headers: request headers - :raises ~websockets.exceptions.InvalidHandshake: to abort the - handshake with an HTTP 400 error code + Raises: + InvalidHandshake: to abort the handshake with an HTTP 400 error. """ subprotocol: Optional[Subprotocol] = None @@ -360,8 +394,13 @@ def select_subprotocol( many servers providing a subprotocol will require that the client uses that subprotocol. - :param client_subprotocols: list of subprotocols offered by the client - :param server_subprotocols: list of subprotocols available on the server + Args: + client_subprotocols: list of subprotocols offered by the client. + server_subprotocols: list of subprotocols available on the server. + + Returns: + Optional[Subprotocol]: Subprotocol, if a common subprotocol was + found. """ subprotocols = set(client_subprotocols) & set(server_subprotocols) @@ -380,11 +419,14 @@ def reject( exception: Optional[Exception] = None, ) -> Response: """ - Create a HTTP response event to send to the client. + Create a HTTP response event to reject the connection. A short plain text response is the best fallback when failing to establish a WebSocket connection. + Returns: + Response: HTTP handshake response to send to the client. + """ body = text.encode() if headers is None: @@ -399,7 +441,10 @@ def reject( def send_response(self, response: Response) -> None: """ - Send a WebSocket handshake response to the client. + Send a handshake response to the client. + + Args: + response: WebSocket handshake response event to send. """ if self.debug: diff --git a/src/websockets/streams.py b/src/websockets/streams.py index d1ce377e7..094cbb53a 100644 --- a/src/websockets/streams.py +++ b/src/websockets/streams.py @@ -7,8 +7,8 @@ class StreamReader: """ Generator-based stream reader. - This class doesn't support concurrent calls to :meth:`read_line()`, - :meth:`read_exact()`, or :meth:`read_to_eof()`. Make sure calls are + This class doesn't support concurrent calls to :meth:`read_line`, + :meth:`read_exact`, or :meth:`read_to_eof`. Make sure calls are serialized. """ @@ -21,11 +21,12 @@ def read_line(self) -> Generator[None, None, bytes]: """ Read a LF-terminated line from the stream. - The return value includes the LF character. - This is a generator-based coroutine. - :raises EOFError: if the stream ends without a LF + The return value includes the LF character. + + Raises: + EOFError: if the stream ends without a LF. """ n = 0 # number of bytes to read @@ -44,11 +45,15 @@ def read_line(self) -> Generator[None, None, bytes]: def read_exact(self, n: int) -> Generator[None, None, bytes]: """ - Read ``n`` bytes from the stream. + Read a given number of bytes from the stream. This is a generator-based coroutine. - :raises EOFError: if the stream ends in less than ``n`` bytes + Args: + n: how many bytes to read. + + Raises: + EOFError: if the stream ends in less than ``n`` bytes. """ assert n >= 0 @@ -92,11 +97,15 @@ def at_eof(self) -> Generator[None, None, bool]: def feed_data(self, data: bytes) -> None: """ - Write ``data`` to the stream. + Write data to the stream. + + :meth:`feed_data` cannot be called after :meth:`feed_eof`. - :meth:`feed_data()` cannot be called after :meth:`feed_eof()`. + Args: + data: data to write. - :raises EOFError: if the stream has ended + Raises: + EOFError: if the stream has ended. """ if self.eof: @@ -107,9 +116,10 @@ def feed_eof(self) -> None: """ End the stream. - :meth:`feed_eof()` must be called at must once. + :meth:`feed_eof` cannot be called more than once. - :raises EOFError: if the stream has ended + Raises: + EOFError: if the stream has ended. """ if self.eof: @@ -118,7 +128,7 @@ def feed_eof(self) -> None: def discard(self) -> None: """ - Discarding all buffered data, but don't end the stream. + Discard all buffered data, but don't end the stream. """ del self.buffer[:] diff --git a/src/websockets/typing.py b/src/websockets/typing.py index 1bd118071..dadee7aba 100644 --- a/src/websockets/typing.py +++ b/src/websockets/typing.py @@ -17,24 +17,25 @@ # Public types used in the signature of public APIs Data = Union[str, bytes] -Data.__doc__ = """ -Types supported in a WebSocket message: +Data.__doc__ = """Types supported in a WebSocket message: +:class:`str` for a Text_ frame, :class:`bytes` for a Binary_. + +.. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 +.. _Binary : https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 -- :class:`str` for text messages; -- :class:`bytes` for binary messages. """ LoggerLike = Union[logging.Logger, logging.LoggerAdapter] -LoggerLike.__doc__ = """Types accepted where :class:`~logging.Logger` is expected.""" +LoggerLike.__doc__ = """Types accepted where a :class:`~logging.Logger` is expected.""" Origin = NewType("Origin", str) -Origin.__doc__ = """Value of a Origin header.""" +Origin.__doc__ = """Value of a ``Origin`` header.""" Subprotocol = NewType("Subprotocol", str) -Subprotocol.__doc__ = """Subprotocol in a Sec-WebSocket-Protocol header.""" +Subprotocol.__doc__ = """Subprotocol in a ``Sec-WebSocket-Protocol`` header.""" ExtensionName = NewType("ExtensionName", str) @@ -48,12 +49,12 @@ # Private types ExtensionHeader = Tuple[ExtensionName, List[ExtensionParameter]] -ExtensionHeader.__doc__ = """Extension in a Sec-WebSocket-Extensions header.""" +ExtensionHeader.__doc__ = """Extension in a ``Sec-WebSocket-Extensions`` header.""" ConnectionOption = NewType("ConnectionOption", str) -ConnectionOption.__doc__ = """Connection option in a Connection header.""" +ConnectionOption.__doc__ = """Connection option in a ``Connection`` header.""" UpgradeProtocol = NewType("UpgradeProtocol", str) -UpgradeProtocol.__doc__ = """Upgrade protocol in an Upgrade header.""" +UpgradeProtocol.__doc__ = """Upgrade protocol in an ``Upgrade`` header.""" diff --git a/src/websockets/uri.py b/src/websockets/uri.py index 397c23116..3d8f7cd95 100644 --- a/src/websockets/uri.py +++ b/src/websockets/uri.py @@ -1,12 +1,3 @@ -""" -:mod:`websockets.uri` parses WebSocket URIs. - -See `section 3 of RFC 6455`_. - -.. _section 3 of RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-3 - -""" - from __future__ import annotations import dataclasses @@ -24,14 +15,16 @@ class WebSocketURI: """ WebSocket URI. - :param bool secure: secure flag - :param str host: lower-case host - :param int port: port, always set even if it's the default - :param str resource_name: path and optional query - :param str user_info: ``(username, password)`` tuple when the URI contains - `User Information`_, else ``None``. + Attributes: + secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI. + host: Host, normalized to lower case. + port: Port, always set even if it's the default. + resource_name: Path and optional query. + user_info: ``(username, password)`` when the URI contains + `User Information`_, else :obj:`None`. .. _User Information: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.1 + """ secure: bool @@ -49,8 +42,11 @@ def parse_uri(uri: str) -> WebSocketURI: """ Parse and validate a WebSocket URI. - :raises ~websockets.exceptions.InvalidURI: if ``uri`` isn't a valid - WebSocket URI. + Args: + uri: WebSocket URI. + + Raises: + InvalidURI: if ``uri`` isn't a valid WebSocket URI. """ parsed = urllib.parse.urlparse(uri) diff --git a/src/websockets/utils.py b/src/websockets/utils.py index c6e4b788c..c40404906 100644 --- a/src/websockets/utils.py +++ b/src/websockets/utils.py @@ -25,7 +25,8 @@ def accept_key(key: str) -> str: """ Compute the value of the Sec-WebSocket-Accept header. - :param key: value of the Sec-WebSocket-Key header + Args: + key: value of the Sec-WebSocket-Key header. """ sha1 = hashlib.sha1((key + GUID).encode()).digest() @@ -36,8 +37,9 @@ def apply_mask(data: bytes, mask: bytes) -> bytes: """ Apply masking to the data of a WebSocket message. - :param data: Data to mask - :param mask: 4-bytes mask + Args: + data: data to mask. + mask: 4-bytes mask. """ if len(mask) != 4: