You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

197 regels
8.1 KiB

  1. from __future__ import annotations
  2. import enum
  3. import json
  4. from collections.abc import AsyncIterator, Iterable
  5. from typing import Any, cast
  6. from starlette.requests import HTTPConnection
  7. from starlette.responses import Response
  8. from starlette.types import Message, Receive, Scope, Send
  9. class WebSocketState(enum.Enum):
  10. CONNECTING = 0
  11. CONNECTED = 1
  12. DISCONNECTED = 2
  13. RESPONSE = 3
  14. class WebSocketDisconnect(Exception):
  15. def __init__(self, code: int = 1000, reason: str | None = None) -> None:
  16. self.code = code
  17. self.reason = reason or ""
  18. class WebSocket(HTTPConnection):
  19. def __init__(self, scope: Scope, receive: Receive, send: Send) -> None:
  20. super().__init__(scope)
  21. assert scope["type"] == "websocket"
  22. self._receive = receive
  23. self._send = send
  24. self.client_state = WebSocketState.CONNECTING
  25. self.application_state = WebSocketState.CONNECTING
  26. async def receive(self) -> Message:
  27. """
  28. Receive ASGI websocket messages, ensuring valid state transitions.
  29. """
  30. if self.client_state == WebSocketState.CONNECTING:
  31. message = await self._receive()
  32. message_type = message["type"]
  33. if message_type != "websocket.connect":
  34. raise RuntimeError(f'Expected ASGI message "websocket.connect", but got {message_type!r}')
  35. self.client_state = WebSocketState.CONNECTED
  36. return message
  37. elif self.client_state == WebSocketState.CONNECTED:
  38. message = await self._receive()
  39. message_type = message["type"]
  40. if message_type not in {"websocket.receive", "websocket.disconnect"}:
  41. raise RuntimeError(
  42. f'Expected ASGI message "websocket.receive" or "websocket.disconnect", but got {message_type!r}'
  43. )
  44. if message_type == "websocket.disconnect":
  45. self.client_state = WebSocketState.DISCONNECTED
  46. return message
  47. else:
  48. raise RuntimeError('Cannot call "receive" once a disconnect message has been received.')
  49. async def send(self, message: Message) -> None:
  50. """
  51. Send ASGI websocket messages, ensuring valid state transitions.
  52. """
  53. if self.application_state == WebSocketState.CONNECTING:
  54. message_type = message["type"]
  55. if message_type not in {"websocket.accept", "websocket.close", "websocket.http.response.start"}:
  56. raise RuntimeError(
  57. 'Expected ASGI message "websocket.accept", "websocket.close" or "websocket.http.response.start", '
  58. f"but got {message_type!r}"
  59. )
  60. if message_type == "websocket.close":
  61. self.application_state = WebSocketState.DISCONNECTED
  62. elif message_type == "websocket.http.response.start":
  63. self.application_state = WebSocketState.RESPONSE
  64. else:
  65. self.application_state = WebSocketState.CONNECTED
  66. await self._send(message)
  67. elif self.application_state == WebSocketState.CONNECTED:
  68. message_type = message["type"]
  69. if message_type not in {"websocket.send", "websocket.close"}:
  70. raise RuntimeError(
  71. f'Expected ASGI message "websocket.send" or "websocket.close", but got {message_type!r}'
  72. )
  73. if message_type == "websocket.close":
  74. self.application_state = WebSocketState.DISCONNECTED
  75. try:
  76. await self._send(message)
  77. except OSError:
  78. self.application_state = WebSocketState.DISCONNECTED
  79. raise WebSocketDisconnect(code=1006)
  80. elif self.application_state == WebSocketState.RESPONSE:
  81. message_type = message["type"]
  82. if message_type != "websocket.http.response.body":
  83. raise RuntimeError(f'Expected ASGI message "websocket.http.response.body", but got {message_type!r}')
  84. if not message.get("more_body", False):
  85. self.application_state = WebSocketState.DISCONNECTED
  86. await self._send(message)
  87. else:
  88. raise RuntimeError('Cannot call "send" once a close message has been sent.')
  89. async def accept(
  90. self,
  91. subprotocol: str | None = None,
  92. headers: Iterable[tuple[bytes, bytes]] | None = None,
  93. ) -> None:
  94. headers = headers or []
  95. if self.client_state == WebSocketState.CONNECTING: # pragma: no branch
  96. # If we haven't yet seen the 'connect' message, then wait for it first.
  97. await self.receive()
  98. await self.send({"type": "websocket.accept", "subprotocol": subprotocol, "headers": headers})
  99. def _raise_on_disconnect(self, message: Message) -> None:
  100. if message["type"] == "websocket.disconnect":
  101. raise WebSocketDisconnect(message["code"], message.get("reason"))
  102. async def receive_text(self) -> str:
  103. if self.application_state != WebSocketState.CONNECTED:
  104. raise RuntimeError('WebSocket is not connected. Need to call "accept" first.')
  105. message = await self.receive()
  106. self._raise_on_disconnect(message)
  107. return cast(str, message["text"])
  108. async def receive_bytes(self) -> bytes:
  109. if self.application_state != WebSocketState.CONNECTED:
  110. raise RuntimeError('WebSocket is not connected. Need to call "accept" first.')
  111. message = await self.receive()
  112. self._raise_on_disconnect(message)
  113. return cast(bytes, message["bytes"])
  114. async def receive_json(self, mode: str = "text") -> Any:
  115. if mode not in {"text", "binary"}:
  116. raise RuntimeError('The "mode" argument should be "text" or "binary".')
  117. if self.application_state != WebSocketState.CONNECTED:
  118. raise RuntimeError('WebSocket is not connected. Need to call "accept" first.')
  119. message = await self.receive()
  120. self._raise_on_disconnect(message)
  121. if mode == "text":
  122. text = message["text"]
  123. else:
  124. text = message["bytes"].decode("utf-8")
  125. return json.loads(text)
  126. async def iter_text(self) -> AsyncIterator[str]:
  127. try:
  128. while True:
  129. yield await self.receive_text()
  130. except WebSocketDisconnect:
  131. pass
  132. async def iter_bytes(self) -> AsyncIterator[bytes]:
  133. try:
  134. while True:
  135. yield await self.receive_bytes()
  136. except WebSocketDisconnect:
  137. pass
  138. async def iter_json(self) -> AsyncIterator[Any]:
  139. try:
  140. while True:
  141. yield await self.receive_json()
  142. except WebSocketDisconnect:
  143. pass
  144. async def send_text(self, data: str) -> None:
  145. await self.send({"type": "websocket.send", "text": data})
  146. async def send_bytes(self, data: bytes) -> None:
  147. await self.send({"type": "websocket.send", "bytes": data})
  148. async def send_json(self, data: Any, mode: str = "text") -> None:
  149. if mode not in {"text", "binary"}:
  150. raise RuntimeError('The "mode" argument should be "text" or "binary".')
  151. text = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
  152. if mode == "text":
  153. await self.send({"type": "websocket.send", "text": text})
  154. else:
  155. await self.send({"type": "websocket.send", "bytes": text.encode("utf-8")})
  156. async def close(self, code: int = 1000, reason: str | None = None) -> None:
  157. await self.send({"type": "websocket.close", "code": code, "reason": reason or ""})
  158. async def send_denial_response(self, response: Response) -> None:
  159. if "websocket.http.response" in self.scope.get("extensions", {}):
  160. await response(self.scope, self.receive, self.send)
  161. else:
  162. raise RuntimeError("The server doesn't support the Websocket Denial Response extension.")
  163. class WebSocketClose:
  164. def __init__(self, code: int = 1000, reason: str | None = None) -> None:
  165. self.code = code
  166. self.reason = reason or ""
  167. async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
  168. await send({"type": "websocket.close", "code": self.code, "reason": self.reason})