router.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. from __future__ import annotations
  2. import http
  3. import ssl as ssl_module
  4. import urllib.parse
  5. from typing import Any, Awaitable, Callable, Literal
  6. from ..http11 import Request, Response
  7. from ..typing import PathLike
  8. from .server import Server, ServerConnection, serve
  9. __all__ = ["route", "unix_route", "Router"]
  10. try:
  11. from werkzeug.exceptions import NotFound
  12. from werkzeug.routing import Map, RequestRedirect
  13. except ImportError:
  14. def route(
  15. url_map: Map,
  16. *args: Any,
  17. server_name: str | None = None,
  18. ssl: ssl_module.SSLContext | Literal[True] | None = None,
  19. create_router: type[Router] | None = None,
  20. **kwargs: Any,
  21. ) -> Server:
  22. raise ImportError("route() requires werkzeug")
  23. def unix_route(
  24. url_map: Map,
  25. path: PathLike | None = None,
  26. **kwargs: Any,
  27. ) -> Server:
  28. raise ImportError("unix_route() requires werkzeug")
  29. else:
  30. def route(
  31. url_map: Map,
  32. *args: Any,
  33. server_name: str | None = None,
  34. ssl: ssl_module.SSLContext | Literal[True] | None = None,
  35. create_router: type[Router] | None = None,
  36. **kwargs: Any,
  37. ) -> Server:
  38. """
  39. Create a WebSocket server dispatching connections to different handlers.
  40. This feature requires the third-party library `werkzeug`_:
  41. .. code-block:: console
  42. $ pip install werkzeug
  43. .. _werkzeug: https://werkzeug.palletsprojects.com/
  44. :func:`route` accepts the same arguments as
  45. :func:`~websockets.sync.server.serve`, except as described below.
  46. The first argument is a :class:`werkzeug.routing.Map` that maps URL patterns
  47. to connection handlers. In addition to the connection, handlers receive
  48. parameters captured in the URL as keyword arguments.
  49. Here's an example::
  50. from websockets.asyncio.router import route
  51. from werkzeug.routing import Map, Rule
  52. async def channel_handler(websocket, channel_id):
  53. ...
  54. url_map = Map([
  55. Rule("/channel/<uuid:channel_id>", endpoint=channel_handler),
  56. ...
  57. ])
  58. # set this event to exit the server
  59. stop = asyncio.Event()
  60. async with route(url_map, ...) as server:
  61. await stop.wait()
  62. Refer to the documentation of :mod:`werkzeug.routing` for details.
  63. If you define redirects with ``Rule(..., redirect_to=...)`` in the URL map,
  64. when the server runs behind a reverse proxy that modifies the ``Host``
  65. header or terminates TLS, you need additional configuration:
  66. * Set ``server_name`` to the name of the server as seen by clients. When
  67. not provided, websockets uses the value of the ``Host`` header.
  68. * Set ``ssl=True`` to generate ``wss://`` URIs without enabling TLS.
  69. Under the hood, this bind the URL map with a ``url_scheme`` of
  70. ``wss://`` instead of ``ws://``.
  71. There is no need to specify ``websocket=True`` in each rule. It is added
  72. automatically.
  73. Like :func:`~websockets.sync.server.serve`, :func:`route` returns a
  74. :class:`~websockets.sync.server.Server` that you can also run with
  75. :meth:`~websockets.sync.server.Server.serve_forever`.
  76. Args:
  77. url_map: Mapping of URL patterns to connection handlers.
  78. server_name: Name of the server as seen by clients. If :obj:`None`,
  79. websockets uses the value of the ``Host`` header.
  80. ssl: Configuration for enabling TLS on the connection. Set it to
  81. :obj:`True` if a reverse proxy terminates TLS connections.
  82. create_router: Factory for the :class:`Router` dispatching requests to
  83. handlers. Set it to a wrapper or a subclass to customize routing.
  84. """
  85. url_scheme = "ws" if ssl is None else "wss"
  86. if ssl is not True and ssl is not None:
  87. kwargs["ssl"] = ssl
  88. if create_router is None:
  89. create_router = Router
  90. router = create_router(url_map, server_name, url_scheme)
  91. _process_request: (
  92. Callable[
  93. [ServerConnection, Request],
  94. Awaitable[Response | None] | Response | None,
  95. ]
  96. | None
  97. ) = kwargs.pop("process_request", None)
  98. if _process_request is None:
  99. process_request: Callable[
  100. [ServerConnection, Request],
  101. Awaitable[Response | None] | Response | None,
  102. ] = router.route_request
  103. else:
  104. async def process_request(
  105. connection: ServerConnection,
  106. request: Request,
  107. ) -> Response | None:
  108. response = _process_request(connection, request)
  109. if isinstance(response, Awaitable):
  110. response = await response
  111. if response is not None:
  112. return response
  113. return router.route_request(connection, request)
  114. return serve(
  115. router.handler,
  116. *args,
  117. process_request=process_request,
  118. **kwargs,
  119. )
  120. def unix_route(
  121. url_map: Map,
  122. path: PathLike | None = None,
  123. **kwargs: Any,
  124. ) -> Server:
  125. """
  126. Create a WebSocket Unix server dispatching connections to different handlers.
  127. :func:`unix_route` combines the behaviors of :func:`route` and
  128. :func:`~websockets.asyncio.server.unix_serve`.
  129. Args:
  130. url_map: Mapping of URL patterns to connection handlers.
  131. path: File system path to the Unix socket.
  132. """
  133. return route(url_map, unix=True, path=path, **kwargs)
  134. class Router:
  135. """WebSocket router supporting :func:`route`."""
  136. def __init__(
  137. self,
  138. url_map: Map,
  139. server_name: str | None = None,
  140. url_scheme: str = "ws",
  141. ) -> None:
  142. self.url_map = url_map
  143. self.server_name = server_name
  144. self.url_scheme = url_scheme
  145. for rule in self.url_map.iter_rules():
  146. rule.websocket = True
  147. def get_server_name(self, connection: ServerConnection, request: Request) -> str:
  148. if self.server_name is None:
  149. return request.headers["Host"]
  150. else:
  151. return self.server_name
  152. def redirect(self, connection: ServerConnection, url: str) -> Response:
  153. response = connection.respond(http.HTTPStatus.FOUND, f"Found at {url}")
  154. response.headers["Location"] = url
  155. return response
  156. def not_found(self, connection: ServerConnection) -> Response:
  157. return connection.respond(http.HTTPStatus.NOT_FOUND, "Not Found")
  158. def route_request(
  159. self, connection: ServerConnection, request: Request
  160. ) -> Response | None:
  161. """Route incoming request."""
  162. url_map_adapter = self.url_map.bind(
  163. server_name=self.get_server_name(connection, request),
  164. url_scheme=self.url_scheme,
  165. )
  166. try:
  167. parsed = urllib.parse.urlparse(request.path)
  168. handler, kwargs = url_map_adapter.match(
  169. path_info=parsed.path,
  170. query_args=parsed.query,
  171. )
  172. except RequestRedirect as redirect:
  173. return self.redirect(connection, redirect.new_url)
  174. except NotFound:
  175. return self.not_found(connection)
  176. connection.handler, connection.handler_kwargs = handler, kwargs
  177. return None
  178. async def handler(self, connection: ServerConnection) -> None:
  179. """Handle a connection."""
  180. return await connection.handler(connection, **connection.handler_kwargs)