from __future__ import annotations import http import ssl as ssl_module import urllib.parse from typing import Any, Awaitable, Callable, Literal from werkzeug.exceptions import NotFound from werkzeug.routing import Map, RequestRedirect from ..http11 import Request, Response from .server import Server, ServerConnection, serve __all__ = ["route", "unix_route", "Router"] class Router: """WebSocket router supporting :func:`route`.""" def __init__( self, url_map: Map, server_name: str | None = None, url_scheme: str = "ws", ) -> None: self.url_map = url_map self.server_name = server_name self.url_scheme = url_scheme for rule in self.url_map.iter_rules(): rule.websocket = True def get_server_name(self, connection: ServerConnection, request: Request) -> str: if self.server_name is None: return request.headers["Host"] else: return self.server_name def redirect(self, connection: ServerConnection, url: str) -> Response: response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}") response.headers["Location"] = url return response def not_found(self, connection: ServerConnection) -> Response: return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found") def route_request( self, connection: ServerConnection, request: Request ) -> Response | None: """Route incoming request.""" url_map_adapter = self.url_map.bind( server_name=self.get_server_name(connection, request), url_scheme=self.url_scheme, ) try: parsed = urllib.parse.urlparse(request.path) handler, kwargs = url_map_adapter.match( path_info=parsed.path, query_args=parsed.query, ) except RequestRedirect as redirect: return self.redirect(connection, redirect.new_url) except NotFound: return self.not_found(connection) connection.handler, connection.handler_kwargs = handler, kwargs return None async def handler(self, connection: ServerConnection) -> None: """Handle a connection.""" return await connection.handler(connection, **connection.handler_kwargs) def route( url_map: Map, *args: Any, server_name: str | None = None, ssl: ssl_module.SSLContext | Literal[True] | None = None, create_router: type[Router] | None = None, **kwargs: Any, ) -> Awaitable[Server]: """ Create a WebSocket server dispatching connections to different handlers. This feature requires the third-party library `werkzeug`_: .. code-block:: console $ pip install werkzeug .. _werkzeug: https://werkzeug.palletsprojects.com/ :func:`route` accepts the same arguments as :func:`~websockets.sync.server.serve`, except as described below. The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns to connection handlers. In addition to the connection, handlers receive parameters captured in the URL as keyword arguments. Here's an example:: from websockets.asyncio.router import route from werkzeug.routing import Map, Rule async def channel_handler(websocket, channel_id): ... url_map = Map([ Rule("/channel/", endpoint=channel_handler), ... ]) # set this future to exit the server stop = asyncio.get_running_loop().create_future() async with route(url_map, ...) as server: await stop Refer to the documentation of :mod:`werkzeug.routing` for details. If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map, when the server runs behind a reverse proxy that modifies the ``Host`` header or terminates TLS, you need additional configuration: * Set ``server_name`` to the name of the server as seen by clients. When not provided, websockets uses the value of the ``Host`` header. * Set ``ssl=True`` to generate ``wss://`` URIs without actually enabling TLS. Under the hood, this bind the URL map with a ``url_scheme`` of ``wss://`` instead of ``ws://``. There is no need to specify ``websocket=True`` in each rule. It is added automatically. Args: url_map: Mapping of URL patterns to connection handlers. server_name: Name of the server as seen by clients. If :obj:`None`, websockets uses the value of the ``Host`` header. ssl: Configuration for enabling TLS on the connection. Set it to :obj:`True` if a reverse proxy terminates TLS connections. create_router: Factory for the :class:`Router` dispatching requests to handlers. Set it to a wrapper or a subclass to customize routing. """ url_scheme = "ws" if ssl is None else "wss" if ssl is not True and ssl is not None: kwargs["ssl"] = ssl if create_router is None: create_router = Router router = create_router(url_map, server_name, url_scheme) _process_request: ( Callable[ [ServerConnection, Request], Awaitable[Response | None] | Response | None, ] | None ) = kwargs.pop("process_request", None) if _process_request is None: process_request: Callable[ [ServerConnection, Request], Awaitable[Response | None] | Response | None, ] = router.route_request else: async def process_request( connection: ServerConnection, request: Request ) -> Response | None: response = _process_request(connection, request) if isinstance(response, Awaitable): response = await response if response is not None: return response return router.route_request(connection, request) return serve(router.handler, *args, process_request=process_request, **kwargs) def unix_route( url_map: Map, path: str | None = None, **kwargs: Any, ) -> Awaitable[Server]: """ Create a WebSocket Unix server dispatching connections to different handlers. :func:`unix_route` combines the behaviors of :func:`route` and :func:`~websockets.asyncio.server.unix_serve`. Args: url_map: Mapping of URL patterns to connection handlers. path: File system path to the Unix socket. """ return route(url_map, unix=True, path=path, **kwargs)