tuxbot-bot/venv/lib/python3.7/site-packages/websockets/server.py
2019-12-16 18:12:10 +01:00

779 lines
29 KiB
Python

"""
The :mod:`websockets.server` module defines a simple WebSocket server API.
"""
import asyncio
import collections.abc
import email.utils
import logging
import sys
from .compatibility import (
BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE,
SWITCHING_PROTOCOLS, UPGRADE_REQUIRED, asyncio_ensure_future
)
from .exceptions import (
AbortHandshake, InvalidHandshake, InvalidMessage, InvalidOrigin,
InvalidUpgrade, NegotiationError
)
from .extensions.permessage_deflate import ServerPerMessageDeflateFactory
from .handshake import build_response, check_request
from .headers import (
build_extension_list, parse_extension_list, parse_subprotocol_list
)
from .http import USER_AGENT, Headers, read_request
from .protocol import WebSocketCommonProtocol
__all__ = ['serve', 'unix_serve', 'WebSocketServerProtocol']
logger = logging.getLogger(__name__)
class WebSocketServerProtocol(WebSocketCommonProtocol):
"""
Complete WebSocket server implementation as an :class:`asyncio.Protocol`.
This class inherits most of its methods from
:class:`~websockets.protocol.WebSocketCommonProtocol`.
For the sake of simplicity, it doesn't rely on a full HTTP implementation.
Its support for HTTP responses is very limited.
"""
is_client = False
side = 'server'
def __init__(self, ws_handler, ws_server, *,
origins=None, extensions=None, subprotocols=None,
extra_headers=None, **kwds):
self.ws_handler = ws_handler
self.ws_server = ws_server
self.origins = origins
self.available_extensions = extensions
self.available_subprotocols = subprotocols
self.extra_headers = extra_headers
super().__init__(**kwds)
def connection_made(self, transport):
"""
Register connection and initialize a task to handle it.
"""
super().connection_made(transport)
# Register the connection with the server before creating the handler
# task. Registering at the beginning of the handler coroutine would
# create a race condition between the creation of the task, which
# schedules its execution, and the moment the handler starts running.
self.ws_server.register(self)
self.handler_task = asyncio_ensure_future(
self.handler(), loop=self.loop)
@asyncio.coroutine
def handler(self):
"""
Handle the lifecycle of a WebSocket connection.
Since this method doesn't have a caller able to handle exceptions, it
attemps to log relevant ones and close the connection properly.
"""
try:
try:
path = yield from self.handshake(
origins=self.origins,
available_extensions=self.available_extensions,
available_subprotocols=self.available_subprotocols,
extra_headers=self.extra_headers,
)
except ConnectionError as exc:
logger.debug(
"Connection error in opening handshake", exc_info=True)
raise
except Exception as exc:
if self._is_server_shutting_down(exc):
status, headers, body = (
SERVICE_UNAVAILABLE,
[],
b"Server is shutting down.\n",
)
elif isinstance(exc, AbortHandshake):
status, headers, body = (
exc.status,
exc.headers,
exc.body,
)
elif isinstance(exc, InvalidOrigin):
logger.debug("Invalid origin", exc_info=True)
status, headers, body = (
FORBIDDEN,
[],
(str(exc) + "\n").encode(),
)
elif isinstance(exc, InvalidUpgrade):
logger.debug("Invalid upgrade", exc_info=True)
status, headers, body = (
UPGRADE_REQUIRED,
[('Upgrade', 'websocket')],
(str(exc) + "\n").encode(),
)
elif isinstance(exc, InvalidHandshake):
logger.debug("Invalid handshake", exc_info=True)
status, headers, body = (
BAD_REQUEST,
[],
(str(exc) + "\n").encode(),
)
else:
logger.warning("Error in opening handshake", exc_info=True)
status, headers, body = (
INTERNAL_SERVER_ERROR,
[],
b"See server log for more information.\n",
)
if not isinstance(headers, Headers):
headers = Headers(headers)
headers.setdefault('Date', email.utils.formatdate(usegmt=True))
headers.setdefault('Server', USER_AGENT)
headers.setdefault('Content-Length', str(len(body)))
headers.setdefault('Content-Type', 'text/plain')
headers.setdefault('Connection', 'close')
yield from self.write_http_response(status, headers, body)
yield from self.fail_connection()
return
try:
yield from self.ws_handler(self, path)
except Exception as exc:
if self._is_server_shutting_down(exc):
if not self.closed:
self.fail_connection(1001)
else:
logger.error("Error in connection handler", exc_info=True)
if not self.closed:
self.fail_connection(1011)
raise
try:
yield from self.close()
except ConnectionError as exc:
logger.debug(
"Connection error in closing handshake", exc_info=True)
raise
except Exception as exc:
if not self._is_server_shutting_down(exc):
logger.warning("Error in closing handshake", exc_info=True)
raise
except Exception:
# Last-ditch attempt to avoid leaking connections on errors.
try:
self.writer.close()
except Exception: # pragma: no cover
pass
finally:
# Unregister the connection with the server when the handler task
# terminates. Registration is tied to the lifecycle of the handler
# task because the server waits for tasks attached to registered
# connections before terminating.
self.ws_server.unregister(self)
def _is_server_shutting_down(self, exc):
"""
Decide whether an exception means that the server is shutting down.
"""
return (
isinstance(exc, asyncio.CancelledError) and
self.ws_server.closing
)
@asyncio.coroutine
def read_http_request(self):
"""
Read request line and headers from the HTTP request.
Raise :exc:`~websockets.exceptions.InvalidMessage` if the HTTP message
is malformed or isn't an HTTP/1.1 GET request.
Don'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 ``self.reader`` after this coroutine returns.
"""
try:
path, headers = yield from read_request(self.reader)
except ValueError as exc:
raise InvalidMessage("Malformed HTTP message") from exc
self.path = path
self.request_headers = headers
return path, self.request_headers
@asyncio.coroutine
def write_http_response(self, status, headers, body=None):
"""
Write status line and headers to the HTTP response.
This coroutine is also able to write a response body.
"""
self.response_headers = headers
# Since the status line and headers only contain ASCII characters,
# we can keep this simple.
response = 'HTTP/1.1 {status.value} {status.phrase}\r\n'.format(
status=status)
response += str(headers)
self.writer.write(response.encode())
if body is not None:
self.writer.write(body)
@asyncio.coroutine
def process_request(self, path, request_headers):
"""
Intercept the HTTP request and return an HTTP response if needed.
``request_headers`` is a :class:`~websockets.http.Headers` instance.
If this coroutine returns ``None``, the WebSocket handshake continues.
If it returns a status code, headers and a response body, that HTTP
response is sent and the connection is closed.
The HTTP status must be a :class:`~http.HTTPStatus`.
(:class:`~http.HTTPStatus` was added in Python 3.5. Use a compatible
object on earlier versions. Look at ``SWITCHING_PROTOCOLS`` in
``websockets.compatibility`` for an example.)
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 method may be overridden to check the request headers and set a
different status, for example to authenticate the request and return
``HTTPStatus.UNAUTHORIZED`` or ``HTTPStatus.FORBIDDEN``.
It is declared as a coroutine because such authentication checks are
likely to require network requests.
"""
@staticmethod
def process_origin(headers, origins=None):
"""
Handle the Origin HTTP request header.
Raise :exc:`~websockets.exceptions.InvalidOrigin` if the origin isn't
acceptable.
"""
origin = headers.get('Origin', '')
if origins is not None:
if origin not in origins:
raise InvalidOrigin(origin)
return origin
@staticmethod
def process_extensions(headers, available_extensions):
"""
Handle the Sec-WebSocket-Extensions HTTP request header.
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.
Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the
handshake with an HTTP 400 error code. (The default implementation
never does this.)
:rfc:`6455` leaves the rules up to the specification of each
:extension.
To provide this level of flexibility, for each extension proposed by
the client, we check for a match with each extension available in the
server configuration. If no match is found, the extension is ignored.
If several variants of the same extension are proposed by the client,
it may be accepted severel times, which won't make sense in general.
Extensions must implement their own requirements. For this purpose,
the list of previously accepted extensions is provided.
This process doesn't allow the server to reorder extensions. It can
only select a subset of the extensions proposed by the client.
Other requirements, for example related to mandatory extensions or the
order of extensions, may be implemented by overriding this method.
"""
response_header = []
accepted_extensions = []
header_values = headers.get_all('Sec-WebSocket-Extensions')
if header_values and available_extensions:
parsed_header_values = sum([
parse_extension_list(header_value)
for header_value in header_values
], [])
for name, request_params in parsed_header_values:
for extension_factory in available_extensions:
# Skip non-matching extensions based on their name.
if extension_factory.name != name:
continue
# Skip non-matching extensions based on their params.
try:
response_params, extension = (
extension_factory.process_request_params(
request_params, accepted_extensions))
except NegotiationError:
continue
# Add matching extension to the final list.
response_header.append((name, response_params))
accepted_extensions.append(extension)
# Break out of the loop once we have a match.
break
# If we didn't break from the loop, no extension in our list
# matched what the client sent. The extension is declined.
# Serialize extension header.
if response_header:
response_header = build_extension_list(response_header)
else:
response_header = None
return response_header, accepted_extensions
# Not @staticmethod because it calls self.select_subprotocol()
def process_subprotocol(self, headers, available_subprotocols):
"""
Handle the Sec-WebSocket-Protocol HTTP request header.
Return Sec-WebSocket-Protocol HTTP response header, which is the same
as the selected subprotocol.
"""
subprotocol = None
header_values = headers.get_all('Sec-WebSocket-Protocol')
if header_values and available_subprotocols:
parsed_header_values = sum([
parse_subprotocol_list(header_value)
for header_value in header_values
], [])
subprotocol = self.select_subprotocol(
parsed_header_values,
available_subprotocols,
)
return subprotocol
@staticmethod
def select_subprotocol(client_subprotocols, server_subprotocols):
"""
Pick a subprotocol among those offered by the client.
If several subprotocols are supported by the client and the server,
the default implementation selects the preferred subprotocols by
giving equal value to the priorities of the client and the server.
If no subprotocols are 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.
"""
subprotocols = set(client_subprotocols) & set(server_subprotocols)
if not subprotocols:
return None
priority = lambda p: (
client_subprotocols.index(p) + server_subprotocols.index(p))
return sorted(subprotocols, key=priority)[0]
@asyncio.coroutine
def handshake(self, origins=None, available_extensions=None,
available_subprotocols=None, extra_headers=None):
"""
Perform the server side of the opening handshake.
If provided, ``origins`` is a list of acceptable HTTP Origin values.
Include ``''`` if the lack of an origin is acceptable.
If provided, ``available_extensions`` is a list of supported
extensions in the order in which they should be used.
If provided, ``available_subprotocols`` is a list of supported
subprotocols in order of decreasing preference.
If provided, ``extra_headers`` sets additional HTTP response headers.
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.
Raise :exc:`~websockets.exceptions.InvalidHandshake` if the handshake
fails.
Return the path of the URI of the request.
"""
path, request_headers = yield from self.read_http_request()
# Hook for customizing request handling, for example checking
# authentication or treating some paths as plain HTTP endpoints.
early_response = yield from self.process_request(path, request_headers)
if early_response is not None:
raise AbortHandshake(*early_response)
key = check_request(request_headers)
self.origin = self.process_origin(request_headers, origins)
extensions_header, self.extensions = self.process_extensions(
request_headers, available_extensions)
protocol_header = self.subprotocol = self.process_subprotocol(
request_headers, available_subprotocols)
response_headers = Headers()
response_headers['Date'] = email.utils.formatdate(usegmt=True)
build_response(response_headers, key)
if extensions_header is not None:
response_headers['Sec-WebSocket-Extensions'] = extensions_header
if self.subprotocol is not None:
response_headers['Sec-WebSocket-Protocol'] = protocol_header
if extra_headers is not None:
if callable(extra_headers):
extra_headers = extra_headers(path, self.request_headers)
if isinstance(extra_headers, Headers):
extra_headers = extra_headers.raw_items()
elif isinstance(extra_headers, collections.abc.Mapping):
extra_headers = extra_headers.items()
for name, value in extra_headers:
response_headers[name] = value
response_headers.setdefault('Server', USER_AGENT)
yield from self.write_http_response(
SWITCHING_PROTOCOLS, response_headers)
self.connection_open()
return path
class WebSocketServer:
"""
Wrapper for :class:`~asyncio.Server` that closes connections on exit.
This class provides the return type of :func:`~websockets.server.serve`.
It mimics the interface of :class:`~asyncio.AbstractServer`, namely its
:meth:`~asyncio.AbstractServer.close()` and
:meth:`~asyncio.AbstractServer.wait_closed()` methods, to close WebSocket
connections properly on exit, in addition to closing the underlying
:class:`~asyncio.Server`.
Instances of this class store a reference to the :class:`~asyncio.Server`
object returned by :meth:`~asyncio.AbstractEventLoop.create_server` rather
than inherit from :class:`~asyncio.Server` in part because
:meth:`~asyncio.AbstractEventLoop.create_server` doesn't support passing a
custom :class:`~asyncio.Server` class.
"""
def __init__(self, loop):
# Store a reference to loop to avoid relying on self.server._loop.
self.loop = loop
self.closing = False
self.websockets = set()
def wrap(self, server):
"""
Attach to a given :class:`~asyncio.Server`.
Since :meth:`~asyncio.AbstractEventLoop.create_server` doesn't support
injecting a custom ``Server`` class, the easiest solution that doesn't
rely on private :mod:`asyncio` APIs is to:
- instantiate a :class:`WebSocketServer`
- give the protocol factory a reference to that instance
- call :meth:`~asyncio.AbstractEventLoop.create_server` with the
factory
- attach the resulting :class:`~asyncio.Server` with this method
"""
self.server = server
def register(self, protocol):
"""
Register a connection with this server.
"""
self.websockets.add(protocol)
def unregister(self, protocol):
"""
Unregister a connection with this server.
"""
self.websockets.remove(protocol)
def close(self):
"""
Close the underlying server, and clean up connections.
This calls :meth:`~asyncio.Server.close` on the underlying
:class:`~asyncio.Server` object, closes open connections with
status code 1001, and stops accepting new connections.
"""
# Make a note that the server is shutting down. Websocket connections
# check this attribute to decide to send a "going away" close code.
self.closing = True
# Stop accepting new connections.
self.server.close()
# Close open connections. For each connection, two tasks are running:
# 1. self.transfer_data_task receives incoming WebSocket messages
# 2. self.handler_task runs the opening handshake, the handler provided
# by the user and the closing handshake
# In the general case, cancelling the handler task will cause the
# handler provided by the user to exit with a CancelledError, which
# will then cause the transfer data task to terminate.
for websocket in self.websockets:
websocket.handler_task.cancel()
@asyncio.coroutine
def wait_closed(self):
"""
Wait until the underlying server and all connections are closed.
This calls :meth:`~asyncio.Server.wait_closed` on the underlying
:class:`~asyncio.Server` object and waits until closing handshakes
are complete and all connections are closed.
This method must be called after :meth:`close()`.
"""
# asyncio.wait doesn't accept an empty first argument.
if self.websockets:
# Either the handler or the connection can terminate first,
# depending on how the client behaves and the server is
# implemented.
yield from asyncio.wait(
[websocket.handler_task for websocket in self.websockets] +
[websocket.close_connection_task
for websocket in self.websockets],
loop=self.loop)
yield from self.server.wait_closed()
@property
def sockets(self):
"""
List of :class:`~socket.socket` objects the server is listening to.
``None`` if the server is closed.
"""
return self.server.sockets
class Serve:
"""
Create, start, and return a :class:`WebSocketServer`.
:func:`serve` returns an awaitable. Awaiting it yields an instance of
:class:`WebSocketServer` which provides
:meth:`~websockets.server.WebSocketServer.close` and
:meth:`~websockets.server.WebSocketServer.wait_closed` methods for
terminating the server and cleaning up its resources.
On Python ≥ 3.5, :func:`serve` can also be used as an asynchronous context
manager. In this case, the server is shut down when exiting the context.
:func:`serve` is a wrapper around the event loop's
:meth:`~asyncio.AbstractEventLoop.create_server` method. Internally, it
creates and starts a :class:`~asyncio.Server` object by calling
:meth:`~asyncio.AbstractEventLoop.create_server`. The
:class:`WebSocketServer` it returns keeps a reference to this object.
The ``ws_handler`` argument is the WebSocket handler. It must be a
coroutine accepting two arguments: a :class:`WebSocketServerProtocol` and
the request URI.
The ``host`` and ``port`` arguments, as well as unrecognized keyword
arguments, are passed along to
:meth:`~asyncio.AbstractEventLoop.create_server`. For example, you can set
the ``ssl`` keyword argument to a :class:`~ssl.SSLContext` to enable TLS.
The ``create_protocol`` parameter allows customizing the asyncio protocol
that manages the connection. It should be a callable or class accepting
the same arguments as :class:`WebSocketServerProtocol` and returning a
:class:`WebSocketServerProtocol` instance. It defaults to
:class:`WebSocketServerProtocol`.
The behavior of the ``timeout``, ``max_size``, and ``max_queue``,
``read_limit``, and ``write_limit`` optional arguments is described in the
documentation of :class:`~websockets.protocol.WebSocketCommonProtocol`.
:func:`serve` also accepts the following optional arguments:
* ``origins`` defines acceptable Origin HTTP headers — include ``''`` 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 — 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.
* ``compression`` is a shortcut to configure compression extensions;
by default it enables the "permessage-deflate" extension; set it to
``None`` to disable compression
Whenever a client connects, the server accepts the connection, creates a
:class:`WebSocketServerProtocol`, performs the opening handshake, and
delegates to the WebSocket handler. Once the handler completes, the server
performs the closing handshake and closes the connection.
When a server is closed with
:meth:`~websockets.server.WebSocketServer.close`, all running WebSocket
handlers are cancelled. They may intercept :exc:`~asyncio.CancelledError`
and perform cleanup actions before re-raising that exception. If a handler
started new tasks, it should cancel them as well in that case.
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())
"""
def __init__(self, ws_handler, host=None, port=None, *,
path=None, create_protocol=None,
timeout=10, max_size=2 ** 20, max_queue=2 ** 5,
read_limit=2 ** 16, write_limit=2 ** 16,
loop=None, legacy_recv=False, klass=None,
origins=None, extensions=None, subprotocols=None,
extra_headers=None, compression='deflate', **kwds):
# Backwards-compatibility: create_protocol used to be called klass.
# In the unlikely event that both are specified, klass is ignored.
if create_protocol is None:
create_protocol = klass
if create_protocol is None:
create_protocol = WebSocketServerProtocol
if loop is None:
loop = asyncio.get_event_loop()
ws_server = WebSocketServer(loop)
secure = kwds.get('ssl') is not None
if compression == 'deflate':
if extensions is None:
extensions = []
if not any(
extension_factory.name == ServerPerMessageDeflateFactory.name
for extension_factory in extensions
):
extensions.append(ServerPerMessageDeflateFactory())
elif compression is not None:
raise ValueError("Unsupported compression: {}".format(compression))
factory = lambda: create_protocol(
ws_handler, ws_server,
host=host, port=port, secure=secure,
timeout=timeout, max_size=max_size, max_queue=max_queue,
read_limit=read_limit, write_limit=write_limit,
loop=loop, legacy_recv=legacy_recv,
origins=origins, extensions=extensions, subprotocols=subprotocols,
extra_headers=extra_headers,
)
if path is None:
creating_server = loop.create_server(factory, host, port, **kwds)
else:
creating_server = loop.create_unix_server(factory, path, **kwds)
# This is a coroutine object.
self._creating_server = creating_server
self.ws_server = ws_server
@asyncio.coroutine
def __iter__(self): # pragma: no cover
server = yield from self._creating_server
self.ws_server.wrap(server)
return self.ws_server
def unix_serve(ws_handler, path, **kwargs):
"""
Similar to :func:`serve()`, but for listening on Unix sockets.
This function calls the event loop's
:meth:`~asyncio.AbstractEventLoop.create_unix_server` method.
It is only available on Unix.
It's useful for deploying a server behind a reverse proxy such as nginx.
"""
return serve(ws_handler, path=path, **kwargs)
# We can't define __await__ on Python < 3.5.1 because asyncio.ensure_future
# didn't accept arbitrary awaitables until Python 3.5.1. We don't define
# __aenter__ and __aexit__ either on Python < 3.5.1 to keep things simple.
if sys.version_info[:3] <= (3, 5, 0): # pragma: no cover
@asyncio.coroutine
def serve(*args, **kwds):
return Serve(*args, **kwds).__iter__()
serve.__doc__ = Serve.__doc__
else:
from .py35.server import __aenter__, __aexit__, __await__
Serve.__aenter__ = __aenter__
Serve.__aexit__ = __aexit__
Serve.__await__ = __await__
serve = Serve