reader_py.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. """Reader for WebSocket protocol versions 13 and 8."""
  2. import asyncio
  3. import builtins
  4. from collections import deque
  5. from typing import Deque, Final, Optional, Set, Tuple, Union
  6. from ..base_protocol import BaseProtocol
  7. from ..compression_utils import ZLibDecompressor
  8. from ..helpers import _EXC_SENTINEL, set_exception
  9. from ..streams import EofStream
  10. from .helpers import UNPACK_CLOSE_CODE, UNPACK_LEN3, websocket_mask
  11. from .models import (
  12. WS_DEFLATE_TRAILING,
  13. WebSocketError,
  14. WSCloseCode,
  15. WSMessage,
  16. WSMsgType,
  17. )
  18. ALLOWED_CLOSE_CODES: Final[Set[int]] = {int(i) for i in WSCloseCode}
  19. # States for the reader, used to parse the WebSocket frame
  20. # integer values are used so they can be cythonized
  21. READ_HEADER = 1
  22. READ_PAYLOAD_LENGTH = 2
  23. READ_PAYLOAD_MASK = 3
  24. READ_PAYLOAD = 4
  25. WS_MSG_TYPE_BINARY = WSMsgType.BINARY
  26. WS_MSG_TYPE_TEXT = WSMsgType.TEXT
  27. # WSMsgType values unpacked so they can by cythonized to ints
  28. OP_CODE_NOT_SET = -1
  29. OP_CODE_CONTINUATION = WSMsgType.CONTINUATION.value
  30. OP_CODE_TEXT = WSMsgType.TEXT.value
  31. OP_CODE_BINARY = WSMsgType.BINARY.value
  32. OP_CODE_CLOSE = WSMsgType.CLOSE.value
  33. OP_CODE_PING = WSMsgType.PING.value
  34. OP_CODE_PONG = WSMsgType.PONG.value
  35. EMPTY_FRAME_ERROR = (True, b"")
  36. EMPTY_FRAME = (False, b"")
  37. COMPRESSED_NOT_SET = -1
  38. COMPRESSED_FALSE = 0
  39. COMPRESSED_TRUE = 1
  40. TUPLE_NEW = tuple.__new__
  41. cython_int = int # Typed to int in Python, but cython with use a signed int in the pxd
  42. class WebSocketDataQueue:
  43. """WebSocketDataQueue resumes and pauses an underlying stream.
  44. It is a destination for WebSocket data.
  45. """
  46. def __init__(
  47. self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop
  48. ) -> None:
  49. self._size = 0
  50. self._protocol = protocol
  51. self._limit = limit * 2
  52. self._loop = loop
  53. self._eof = False
  54. self._waiter: Optional[asyncio.Future[None]] = None
  55. self._exception: Union[BaseException, None] = None
  56. self._buffer: Deque[Tuple[WSMessage, int]] = deque()
  57. self._get_buffer = self._buffer.popleft
  58. self._put_buffer = self._buffer.append
  59. def is_eof(self) -> bool:
  60. return self._eof
  61. def exception(self) -> Optional[BaseException]:
  62. return self._exception
  63. def set_exception(
  64. self,
  65. exc: BaseException,
  66. exc_cause: builtins.BaseException = _EXC_SENTINEL,
  67. ) -> None:
  68. self._eof = True
  69. self._exception = exc
  70. if (waiter := self._waiter) is not None:
  71. self._waiter = None
  72. set_exception(waiter, exc, exc_cause)
  73. def _release_waiter(self) -> None:
  74. if (waiter := self._waiter) is None:
  75. return
  76. self._waiter = None
  77. if not waiter.done():
  78. waiter.set_result(None)
  79. def feed_eof(self) -> None:
  80. self._eof = True
  81. self._release_waiter()
  82. self._exception = None # Break cyclic references
  83. def feed_data(self, data: "WSMessage", size: "cython_int") -> None:
  84. self._size += size
  85. self._put_buffer((data, size))
  86. self._release_waiter()
  87. if self._size > self._limit and not self._protocol._reading_paused:
  88. self._protocol.pause_reading()
  89. async def read(self) -> WSMessage:
  90. if not self._buffer and not self._eof:
  91. assert not self._waiter
  92. self._waiter = self._loop.create_future()
  93. try:
  94. await self._waiter
  95. except (asyncio.CancelledError, asyncio.TimeoutError):
  96. self._waiter = None
  97. raise
  98. return self._read_from_buffer()
  99. def _read_from_buffer(self) -> WSMessage:
  100. if self._buffer:
  101. data, size = self._get_buffer()
  102. self._size -= size
  103. if self._size < self._limit and self._protocol._reading_paused:
  104. self._protocol.resume_reading()
  105. return data
  106. if self._exception is not None:
  107. raise self._exception
  108. raise EofStream
  109. class WebSocketReader:
  110. def __init__(
  111. self, queue: WebSocketDataQueue, max_msg_size: int, compress: bool = True
  112. ) -> None:
  113. self.queue = queue
  114. self._max_msg_size = max_msg_size
  115. self._exc: Optional[Exception] = None
  116. self._partial = bytearray()
  117. self._state = READ_HEADER
  118. self._opcode: int = OP_CODE_NOT_SET
  119. self._frame_fin = False
  120. self._frame_opcode: int = OP_CODE_NOT_SET
  121. self._payload_fragments: list[bytes] = []
  122. self._frame_payload_len = 0
  123. self._tail: bytes = b""
  124. self._has_mask = False
  125. self._frame_mask: Optional[bytes] = None
  126. self._payload_bytes_to_read = 0
  127. self._payload_len_flag = 0
  128. self._compressed: int = COMPRESSED_NOT_SET
  129. self._decompressobj: Optional[ZLibDecompressor] = None
  130. self._compress = compress
  131. def feed_eof(self) -> None:
  132. self.queue.feed_eof()
  133. # data can be bytearray on Windows because proactor event loop uses bytearray
  134. # and asyncio types this to Union[bytes, bytearray, memoryview] so we need
  135. # coerce data to bytes if it is not
  136. def feed_data(
  137. self, data: Union[bytes, bytearray, memoryview]
  138. ) -> Tuple[bool, bytes]:
  139. if type(data) is not bytes:
  140. data = bytes(data)
  141. if self._exc is not None:
  142. return True, data
  143. try:
  144. self._feed_data(data)
  145. except Exception as exc:
  146. self._exc = exc
  147. set_exception(self.queue, exc)
  148. return EMPTY_FRAME_ERROR
  149. return EMPTY_FRAME
  150. def _handle_frame(
  151. self,
  152. fin: bool,
  153. opcode: Union[int, cython_int], # Union intended: Cython pxd uses C int
  154. payload: Union[bytes, bytearray],
  155. compressed: Union[int, cython_int], # Union intended: Cython pxd uses C int
  156. ) -> None:
  157. msg: WSMessage
  158. if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
  159. # Validate continuation frames before processing
  160. if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
  161. raise WebSocketError(
  162. WSCloseCode.PROTOCOL_ERROR,
  163. "Continuation frame for non started message",
  164. )
  165. # load text/binary
  166. if not fin:
  167. # got partial frame payload
  168. if opcode != OP_CODE_CONTINUATION:
  169. self._opcode = opcode
  170. self._partial += payload
  171. if self._max_msg_size and len(self._partial) >= self._max_msg_size:
  172. raise WebSocketError(
  173. WSCloseCode.MESSAGE_TOO_BIG,
  174. f"Message size {len(self._partial)} "
  175. f"exceeds limit {self._max_msg_size}",
  176. )
  177. return
  178. has_partial = bool(self._partial)
  179. if opcode == OP_CODE_CONTINUATION:
  180. opcode = self._opcode
  181. self._opcode = OP_CODE_NOT_SET
  182. # previous frame was non finished
  183. # we should get continuation opcode
  184. elif has_partial:
  185. raise WebSocketError(
  186. WSCloseCode.PROTOCOL_ERROR,
  187. "The opcode in non-fin frame is expected "
  188. f"to be zero, got {opcode!r}",
  189. )
  190. assembled_payload: Union[bytes, bytearray]
  191. if has_partial:
  192. assembled_payload = self._partial + payload
  193. self._partial.clear()
  194. else:
  195. assembled_payload = payload
  196. if self._max_msg_size and len(assembled_payload) >= self._max_msg_size:
  197. raise WebSocketError(
  198. WSCloseCode.MESSAGE_TOO_BIG,
  199. f"Message size {len(assembled_payload)} "
  200. f"exceeds limit {self._max_msg_size}",
  201. )
  202. # Decompress process must to be done after all packets
  203. # received.
  204. if compressed:
  205. if not self._decompressobj:
  206. self._decompressobj = ZLibDecompressor(suppress_deflate_header=True)
  207. # XXX: It's possible that the zlib backend (isal is known to
  208. # do this, maybe others too?) will return max_length bytes,
  209. # but internally buffer more data such that the payload is
  210. # >max_length, so we return one extra byte and if we're able
  211. # to do that, then the message is too big.
  212. payload_merged = self._decompressobj.decompress_sync(
  213. assembled_payload + WS_DEFLATE_TRAILING,
  214. (
  215. self._max_msg_size + 1
  216. if self._max_msg_size
  217. else self._max_msg_size
  218. ),
  219. )
  220. if self._max_msg_size and len(payload_merged) > self._max_msg_size:
  221. raise WebSocketError(
  222. WSCloseCode.MESSAGE_TOO_BIG,
  223. f"Decompressed message exceeds size limit {self._max_msg_size}",
  224. )
  225. elif type(assembled_payload) is bytes:
  226. payload_merged = assembled_payload
  227. else:
  228. payload_merged = bytes(assembled_payload)
  229. if opcode == OP_CODE_TEXT:
  230. try:
  231. text = payload_merged.decode("utf-8")
  232. except UnicodeDecodeError as exc:
  233. raise WebSocketError(
  234. WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
  235. ) from exc
  236. # XXX: The Text and Binary messages here can be a performance
  237. # bottleneck, so we use tuple.__new__ to improve performance.
  238. # This is not type safe, but many tests should fail in
  239. # test_client_ws_functional.py if this is wrong.
  240. self.queue.feed_data(
  241. TUPLE_NEW(WSMessage, (WS_MSG_TYPE_TEXT, text, "")),
  242. len(payload_merged),
  243. )
  244. else:
  245. self.queue.feed_data(
  246. TUPLE_NEW(WSMessage, (WS_MSG_TYPE_BINARY, payload_merged, "")),
  247. len(payload_merged),
  248. )
  249. elif opcode == OP_CODE_CLOSE:
  250. if len(payload) >= 2:
  251. close_code = UNPACK_CLOSE_CODE(payload[:2])[0]
  252. if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES:
  253. raise WebSocketError(
  254. WSCloseCode.PROTOCOL_ERROR,
  255. f"Invalid close code: {close_code}",
  256. )
  257. try:
  258. close_message = payload[2:].decode("utf-8")
  259. except UnicodeDecodeError as exc:
  260. raise WebSocketError(
  261. WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message"
  262. ) from exc
  263. msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, close_code, close_message))
  264. elif payload:
  265. raise WebSocketError(
  266. WSCloseCode.PROTOCOL_ERROR,
  267. f"Invalid close frame: {fin} {opcode} {payload!r}",
  268. )
  269. else:
  270. msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, 0, ""))
  271. self.queue.feed_data(msg, 0)
  272. elif opcode == OP_CODE_PING:
  273. msg = TUPLE_NEW(WSMessage, (WSMsgType.PING, payload, ""))
  274. self.queue.feed_data(msg, len(payload))
  275. elif opcode == OP_CODE_PONG:
  276. msg = TUPLE_NEW(WSMessage, (WSMsgType.PONG, payload, ""))
  277. self.queue.feed_data(msg, len(payload))
  278. else:
  279. raise WebSocketError(
  280. WSCloseCode.PROTOCOL_ERROR, f"Unexpected opcode={opcode!r}"
  281. )
  282. def _feed_data(self, data: bytes) -> None:
  283. """Return the next frame from the socket."""
  284. if self._tail:
  285. data, self._tail = self._tail + data, b""
  286. start_pos: int = 0
  287. data_len = len(data)
  288. data_cstr = data
  289. while True:
  290. # read header
  291. if self._state == READ_HEADER:
  292. if data_len - start_pos < 2:
  293. break
  294. first_byte = data_cstr[start_pos]
  295. second_byte = data_cstr[start_pos + 1]
  296. start_pos += 2
  297. fin = (first_byte >> 7) & 1
  298. rsv1 = (first_byte >> 6) & 1
  299. rsv2 = (first_byte >> 5) & 1
  300. rsv3 = (first_byte >> 4) & 1
  301. opcode = first_byte & 0xF
  302. # frame-fin = %x0 ; more frames of this message follow
  303. # / %x1 ; final frame of this message
  304. # frame-rsv1 = %x0 ;
  305. # 1 bit, MUST be 0 unless negotiated otherwise
  306. # frame-rsv2 = %x0 ;
  307. # 1 bit, MUST be 0 unless negotiated otherwise
  308. # frame-rsv3 = %x0 ;
  309. # 1 bit, MUST be 0 unless negotiated otherwise
  310. #
  311. # Remove rsv1 from this test for deflate development
  312. if rsv2 or rsv3 or (rsv1 and not self._compress):
  313. raise WebSocketError(
  314. WSCloseCode.PROTOCOL_ERROR,
  315. "Received frame with non-zero reserved bits",
  316. )
  317. if opcode > 0x7 and fin == 0:
  318. raise WebSocketError(
  319. WSCloseCode.PROTOCOL_ERROR,
  320. "Received fragmented control frame",
  321. )
  322. has_mask = (second_byte >> 7) & 1
  323. length = second_byte & 0x7F
  324. # Control frames MUST have a payload
  325. # length of 125 bytes or less
  326. if opcode > 0x7 and length > 125:
  327. raise WebSocketError(
  328. WSCloseCode.PROTOCOL_ERROR,
  329. "Control frame payload cannot be larger than 125 bytes",
  330. )
  331. # Set compress status if last package is FIN
  332. # OR set compress status if this is first fragment
  333. # Raise error if not first fragment with rsv1 = 0x1
  334. if self._frame_fin or self._compressed == COMPRESSED_NOT_SET:
  335. self._compressed = COMPRESSED_TRUE if rsv1 else COMPRESSED_FALSE
  336. elif rsv1:
  337. raise WebSocketError(
  338. WSCloseCode.PROTOCOL_ERROR,
  339. "Received frame with non-zero reserved bits",
  340. )
  341. self._frame_fin = bool(fin)
  342. self._frame_opcode = opcode
  343. self._has_mask = bool(has_mask)
  344. self._payload_len_flag = length
  345. self._state = READ_PAYLOAD_LENGTH
  346. # read payload length
  347. if self._state == READ_PAYLOAD_LENGTH:
  348. len_flag = self._payload_len_flag
  349. if len_flag == 126:
  350. if data_len - start_pos < 2:
  351. break
  352. first_byte = data_cstr[start_pos]
  353. second_byte = data_cstr[start_pos + 1]
  354. start_pos += 2
  355. self._payload_bytes_to_read = first_byte << 8 | second_byte
  356. elif len_flag > 126:
  357. if data_len - start_pos < 8:
  358. break
  359. self._payload_bytes_to_read = UNPACK_LEN3(data, start_pos)[0]
  360. start_pos += 8
  361. else:
  362. self._payload_bytes_to_read = len_flag
  363. self._state = READ_PAYLOAD_MASK if self._has_mask else READ_PAYLOAD
  364. # read payload mask
  365. if self._state == READ_PAYLOAD_MASK:
  366. if data_len - start_pos < 4:
  367. break
  368. self._frame_mask = data_cstr[start_pos : start_pos + 4]
  369. start_pos += 4
  370. self._state = READ_PAYLOAD
  371. if self._state == READ_PAYLOAD:
  372. chunk_len = data_len - start_pos
  373. if self._payload_bytes_to_read >= chunk_len:
  374. f_end_pos = data_len
  375. self._payload_bytes_to_read -= chunk_len
  376. else:
  377. f_end_pos = start_pos + self._payload_bytes_to_read
  378. self._payload_bytes_to_read = 0
  379. had_fragments = self._frame_payload_len
  380. self._frame_payload_len += f_end_pos - start_pos
  381. f_start_pos = start_pos
  382. start_pos = f_end_pos
  383. if self._payload_bytes_to_read != 0:
  384. # If we don't have a complete frame, we need to save the
  385. # data for the next call to feed_data.
  386. self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
  387. break
  388. payload: Union[bytes, bytearray]
  389. if had_fragments:
  390. # We have to join the payload fragments get the payload
  391. self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos])
  392. if self._has_mask:
  393. assert self._frame_mask is not None
  394. payload_bytearray = bytearray(b"".join(self._payload_fragments))
  395. websocket_mask(self._frame_mask, payload_bytearray)
  396. payload = payload_bytearray
  397. else:
  398. payload = b"".join(self._payload_fragments)
  399. self._payload_fragments.clear()
  400. elif self._has_mask:
  401. assert self._frame_mask is not None
  402. payload_bytearray = data_cstr[f_start_pos:f_end_pos] # type: ignore[assignment]
  403. if type(payload_bytearray) is not bytearray: # pragma: no branch
  404. # Cython will do the conversion for us
  405. # but we need to do it for Python and we
  406. # will always get here in Python
  407. payload_bytearray = bytearray(payload_bytearray)
  408. websocket_mask(self._frame_mask, payload_bytearray)
  409. payload = payload_bytearray
  410. else:
  411. payload = data_cstr[f_start_pos:f_end_pos]
  412. self._handle_frame(
  413. self._frame_fin, self._frame_opcode, payload, self._compressed
  414. )
  415. self._frame_payload_len = 0
  416. self._state = READ_HEADER
  417. # XXX: Cython needs slices to be bounded, so we can't omit the slice end here.
  418. self._tail = data_cstr[start_pos:data_len] if start_pos < data_len else b""