You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Enso-Bot/venv/Lib/site-packages/websockets/server.py

547 lines
20 KiB
Python

4 years ago
"""
The :mod:`websockets.server` module defines a simple WebSocket server API.
4 years ago
"""
import asyncio
import collections.abc
import logging
from .compatibility import (
BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE,
SWITCHING_PROTOCOLS, asyncio_ensure_future
)
4 years ago
from .exceptions import (
AbortHandshake, InvalidHandshake, InvalidMessage, InvalidOrigin
4 years ago
)
from .handshake import build_response, check_request
from .http import USER_AGENT, build_headers, read_request
from .protocol import CONNECTING, OPEN, WebSocketCommonProtocol
4 years ago
__all__ = ['serve', 'WebSocketServerProtocol']
4 years ago
logger = logging.getLogger(__name__)
class WebSocketServerProtocol(WebSocketCommonProtocol):
"""
Complete WebSocket server implementation as an :class:`asyncio.Protocol`.
4 years ago
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.
"""
state = CONNECTING
4 years ago
def __init__(self, ws_handler, ws_server, *,
origins=None, subprotocols=None, extra_headers=None, **kwds):
4 years ago
self.ws_handler = ws_handler
self.ws_server = ws_server
self.origins = origins
self.subprotocols = subprotocols
4 years ago
self.extra_headers = extra_headers
super().__init__(**kwds)
4 years ago
def connection_made(self, transport):
4 years ago
super().connection_made(transport)
# Register the connection with the server when creating the handler
# task. (Registering at the beginning of the handler coroutine would
4 years ago
# create a race condition between the creation of the task, which
# schedules its execution, and the moment the handler starts running.)
4 years ago
self.ws_server.register(self)
self.handler_task = asyncio_ensure_future(
self.handler(), loop=self.loop)
4 years ago
@asyncio.coroutine
def handler(self):
# Since this method doesn't have a caller able to handle exceptions,
# it attemps to log relevant ones and close the connection properly.
4 years ago
try:
try:
path = yield from self.handshake(
origins=self.origins, subprotocols=self.subprotocols,
extra_headers=self.extra_headers)
except ConnectionError as exc:
logger.debug(
"Connection error in opening handshake", exc_info=True)
4 years ago
raise
except Exception as exc:
if self._is_server_shutting_down(exc):
early_response = (
SERVICE_UNAVAILABLE,
[],
b"Server is shutting down.",
4 years ago
)
elif isinstance(exc, AbortHandshake):
early_response = (
exc.status,
exc.headers,
exc.body,
)
elif isinstance(exc, InvalidOrigin):
logger.warning("Invalid origin", exc_info=True)
early_response = (
FORBIDDEN,
[],
str(exc).encode(),
4 years ago
)
elif isinstance(exc, InvalidHandshake):
logger.warning("Invalid handshake", exc_info=True)
early_response = (
BAD_REQUEST,
[],
str(exc).encode(),
4 years ago
)
else:
logger.warning("Error in opening handshake", exc_info=True)
early_response = (
INTERNAL_SERVER_ERROR,
[],
b"See server log for more information.",
4 years ago
)
yield from self.write_http_response(*early_response)
self.opening_handshake.set_result(False)
yield from self.close_connection(force=True)
4 years ago
return
try:
yield from self.ws_handler(self, path)
except Exception as exc:
if self._is_server_shutting_down(exc):
yield from self.fail_connection(1001)
else:
logger.error("Error in connection handler", exc_info=True)
yield from self.fail_connection(1011)
4 years ago
raise
try:
yield from self.close()
except ConnectionError as exc:
logger.debug(
"Connection error in closing handshake", exc_info=True)
4 years ago
raise
except Exception as exc:
if not self._is_server_shutting_down(exc):
logger.warning("Error in closing handshake", exc_info=True)
4 years ago
raise
except Exception:
# Last-ditch attempt to avoid leaking connections on errors.
try:
self.writer.close()
except Exception: # pragma: no cover
4 years ago
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):
4 years ago
"""
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.
4 years ago
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.
4 years ago
"""
try:
path, headers = yield from read_request(self.reader)
except ValueError as exc:
raise InvalidMessage("Malformed HTTP message") from exc
4 years ago
self.path = path
self.request_headers = build_headers(headers)
self.raw_request_headers = headers
4 years ago
return path, self.request_headers
4 years ago
@asyncio.coroutine
def write_http_response(self, status, headers, body=None):
4 years ago
"""
Write status line and headers to the HTTP response.
This coroutine is also able to write a response body.
"""
self.response_headers = build_headers(headers)
self.raw_response_headers = headers
4 years ago
# Since the status line and headers only contain ASCII characters,
# we can keep this simple.
response = [
'HTTP/1.1 {value} {phrase}'.format(
value=status.value, phrase=status.phrase)]
response.extend('{}: {}'.format(k, v) for k, v in headers)
response.append('\r\n')
response = '\r\n'.join(response).encode()
4 years ago
self.writer.write(response)
4 years ago
if body is not None:
self.writer.write(body)
4 years ago
@asyncio.coroutine
def process_request(self, path, request_headers):
4 years ago
"""
Intercept the HTTP request and return an HTTP response if needed.
4 years ago
``request_headers`` are a :class:`~http.client.HTTPMessage`.
4 years ago
If this coroutine returns ``None``, the WebSocket handshake continues.
If it returns a status code, headers and a optionally a response body,
that HTTP response is sent and the connection is closed.
4 years ago
The HTTP status must be a :class:`~http.HTTPStatus`. HTTP headers must
be an iterable of ``(name, value)`` pairs. If provided, the HTTP
response body must be :class:`bytes`.
4 years ago
(: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.)
4 years ago
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``.
4 years ago
It is declared as a coroutine because such authentication checks are
likely to require network requests.
4 years ago
"""
def process_origin(self, get_header, origins=None):
4 years ago
"""
Handle the Origin HTTP header.
4 years ago
Raise :exc:`~websockets.exceptions.InvalidOrigin` if the origin isn't
acceptable.
4 years ago
"""
origin = get_header('Origin')
4 years ago
if origins is not None:
if origin not in origins:
raise InvalidOrigin("Origin not allowed: {}".format(origin))
4 years ago
return origin
def process_subprotocol(self, get_header, subprotocols=None):
4 years ago
"""
Handle the Sec-WebSocket-Protocol HTTP header.
4 years ago
"""
if subprotocols is not None:
subprotocol = get_header('Sec-WebSocket-Protocol')
if subprotocol:
return self.select_subprotocol(
[p.strip() for p in subprotocol.split(',')],
subprotocols,
)
4 years ago
@staticmethod
def select_subprotocol(client_protos, server_protos):
4 years ago
"""
Pick a subprotocol among those offered by the client.
4 years ago
"""
common_protos = set(client_protos) & set(server_protos)
if not common_protos:
return None
priority = lambda p: client_protos.index(p) + server_protos.index(p)
return sorted(common_protos, key=priority)[0]
4 years ago
@asyncio.coroutine
def handshake(self, origins=None, subprotocols=None, extra_headers=None):
4 years ago
"""
Perform the server side of the opening handshake.
4 years ago
If provided, ``origins`` is a list of acceptable HTTP Origin values.
Include ``''`` if the lack of an origin is acceptable.
4 years ago
If provided, ``subprotocols`` is a list of supported subprotocols in
order of decreasing preference.
4 years ago
If provided, ``extra_headers`` sets additional HTTP response headers.
It can be a mapping or an iterable of (name, value) pairs. It can also
be a callable taking the request path and headers in arguments.
4 years ago
Raise :exc:`~websockets.exceptions.InvalidHandshake` or a subclass if
the handshake fails.
4 years ago
Return the URI of the request.
4 years ago
"""
path, request_headers = yield from self.read_http_request()
4 years ago
# 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)
4 years ago
if early_response is not None:
raise AbortHandshake(*early_response)
get_header = lambda k: request_headers.get(k, '')
4 years ago
key = check_request(get_header)
4 years ago
self.origin = self.process_origin(get_header, origins)
self.subprotocol = self.process_subprotocol(get_header, subprotocols)
4 years ago
response_headers = []
set_header = lambda k, v: response_headers.append((k, v))
4 years ago
set_header('Server', USER_AGENT)
4 years ago
if self.subprotocol:
set_header('Sec-WebSocket-Protocol', self.subprotocol)
4 years ago
if extra_headers is not None:
if callable(extra_headers):
extra_headers = extra_headers(path, self.raw_request_headers)
if isinstance(extra_headers, collections.abc.Mapping):
4 years ago
extra_headers = extra_headers.items()
for name, value in extra_headers:
set_header(name, value)
build_response(set_header, key)
4 years ago
yield from self.write_http_response(
SWITCHING_PROTOCOLS, response_headers)
4 years ago
assert self.state == CONNECTING
self.state = OPEN
self.opening_handshake.set_result(True)
4 years ago
return path
class WebSocketServer:
"""
Wraps an underlying :class:`~asyncio.Server` object.
4 years ago
This class provides the return type of :func:`~websockets.server.serve`.
This class shouldn't be instantiated directly.
4 years ago
Objects of this class store a reference to an underlying
:class:`~asyncio.Server` object returned by
:meth:`~asyncio.AbstractEventLoop.create_server`. The class stores a
reference rather than inheriting from :class:`~asyncio.Server` in part
because :meth:`~asyncio.AbstractEventLoop.create_server` doesn't support
passing a custom :class:`~asyncio.Server` class.
4 years ago
:class:`WebSocketServer` supports cleaning up the underlying
:class:`~asyncio.Server` object and other resources by implementing the
interface of ``asyncio.events.AbstractServer``, namely its ``close()``
and ``wait_closed()`` methods.
4 years ago
"""
def __init__(self, loop):
4 years ago
# Store a reference to loop to avoid relying on self.server._loop.
self.loop = loop
self.closing = False
self.websockets = set()
4 years ago
def wrap(self, server):
4 years ago
"""
Attach to a given :class:`~asyncio.Server`.
Since :meth:`~asyncio.AbstractEventLoop.create_server` doesn't support
injecting a custom ``Server`` class, a simple solution that doesn't
rely on private APIs is to:
4 years ago
- instantiate a :class:`WebSocketServer`
- give the protocol factory a reference to that instance
- call :meth:`~asyncio.AbstractEventLoop.create_server` with the
factory
4 years ago
- attach the resulting :class:`~asyncio.Server` with this method
"""
self.server = server
def register(self, protocol):
4 years ago
self.websockets.add(protocol)
def unregister(self, protocol):
4 years ago
self.websockets.remove(protocol)
def close(self):
4 years ago
"""
Close the underlying server, and clean up connections.
4 years ago
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.
4 years ago
"""
# 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
4 years ago
# Stop accepting new connections.
self.server.close()
# Close open connections. For each connection, two tasks are running:
# 1. self.worker_task shuffles messages between the network and queues
# 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 worker task to terminate.
for websocket in self.websockets:
websocket.handler_task.cancel()
4 years ago
@asyncio.coroutine
def wait_closed(self):
4 years ago
"""
Wait until the underlying server and all connections are closed.
4 years ago
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.
4 years ago
This method must be called after :meth:`close()`.
4 years ago
"""
# asyncio.wait doesn't accept an empty first argument.
if self.websockets:
# The handler or the worker task 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.worker_task for websocket in self.websockets],
loop=self.loop)
yield from self.server.wait_closed()
@asyncio.coroutine
def serve(ws_handler, host=None, port=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, subprotocols=None, extra_headers=None,
**kwds):
4 years ago
"""
Create, start, and return a :class:`WebSocketServer` object.
4 years ago
:func:`serve` is a wrapper around the event loop's
:meth:`~asyncio.AbstractEventLoop.create_server` method.
Internally, the function creates and starts a :class:`~asyncio.Server`
object by calling :meth:`~asyncio.AbstractEventLoop.create_server`. The
:class:`WebSocketServer` keeps a reference to this object.
4 years ago
The returned :class:`WebSocketServer` and its resources can be cleaned
up by calling its :meth:`~websockets.server.WebSocketServer.close` and
:meth:`~websockets.server.WebSocketServer.wait_closed` methods.
4 years ago
On Python 3.5 and greater, :func:`serve` can also be used as an
asynchronous context manager. In this case, the server is shut down
when exiting the context.
4 years ago
The ``ws_handler`` argument is the WebSocket handler. It must be a
coroutine accepting two arguments: a :class:`WebSocketServerProtocol`
and the request URI.
4 years ago
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
4 years ago
: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`.
4 years ago
:func:`serve` also accepts the following optional arguments:
* ``origins`` defines acceptable Origin HTTP headers include
``''`` if the lack of an origin is acceptable
4 years ago
* ``subprotocols`` is a list of supported subprotocols in order of
decreasing preference
* ``extra_headers`` sets additional HTTP response headers it can be a
mapping, an iterable of (name, value) pairs, or a callable taking the
request path and headers in arguments.
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.
4 years ago
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())
"""
if loop is None:
loop = asyncio.get_event_loop()
# 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
ws_server = WebSocketServer(loop)
secure = kwds.get('ssl') is not None
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, subprotocols=subprotocols,
extra_headers=extra_headers,
)
server = yield from loop.create_server(factory, host, port, **kwds)
ws_server.wrap(server)
return ws_server
try:
from .py35.server import Serve
except (SyntaxError, ImportError): # pragma: no cover
pass
else:
Serve.__wrapped__ = serve
# Copy over docstring to support building documentation on Python 3.5.
Serve.__doc__ = serve.__doc__
serve = Serve