router.py 7.6 KB

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