Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websockets/frames.py: 47%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import dataclasses
4import enum
5import io
6import os
7import re
8import secrets
9import struct
10from collections.abc import Generator, Sequence
11from typing import Callable, Self
13from .exceptions import PayloadTooBig, ProtocolError
14from .typing import BytesLike
17try:
18 from .speedups import apply_mask
19except ImportError:
20 from .utils import apply_mask
23__all__ = [
24 "Opcode",
25 "CloseCode",
26 "Frame",
27 "Close",
28]
30is_space_or_printable_ascii = re.compile(rb"[\x09-\x0D\x20-\x7E]*").fullmatch
33class Opcode(enum.IntEnum):
34 """Opcode values for WebSocket frames."""
36 CONT, TEXT, BINARY = 0x00, 0x01, 0x02
37 CLOSE, PING, PONG = 0x08, 0x09, 0x0A
40CONT = Opcode.CONT
41TEXT = Opcode.TEXT
42BINARY = Opcode.BINARY
43CLOSE = Opcode.CLOSE
44PING = Opcode.PING
45PONG = Opcode.PONG
47DATA_OPCODES = CONT, TEXT, BINARY
48CTRL_OPCODES = CLOSE, PING, PONG
51class CloseCode(enum.IntEnum):
52 """Close code values for WebSocket close frames."""
54 NORMAL_CLOSURE = 1000
55 GOING_AWAY = 1001
56 PROTOCOL_ERROR = 1002
57 UNSUPPORTED_DATA = 1003
58 # 1004 is reserved
59 NO_STATUS_RCVD = 1005
60 ABNORMAL_CLOSURE = 1006
61 INVALID_DATA = 1007
62 POLICY_VIOLATION = 1008
63 MESSAGE_TOO_BIG = 1009
64 MANDATORY_EXTENSION = 1010
65 INTERNAL_ERROR = 1011
66 SERVICE_RESTART = 1012
67 TRY_AGAIN_LATER = 1013
68 BAD_GATEWAY = 1014
69 TLS_HANDSHAKE = 1015
72# See https://www.iana.org/assignments/websocket/websocket.xhtml
73CLOSE_CODE_EXPLANATIONS: dict[int, str] = {
74 CloseCode.NORMAL_CLOSURE: "OK",
75 CloseCode.GOING_AWAY: "going away",
76 CloseCode.PROTOCOL_ERROR: "protocol error",
77 CloseCode.UNSUPPORTED_DATA: "unsupported data",
78 CloseCode.NO_STATUS_RCVD: "no status received [internal]",
79 CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]",
80 CloseCode.INVALID_DATA: "invalid frame payload data",
81 CloseCode.POLICY_VIOLATION: "policy violation",
82 CloseCode.MESSAGE_TOO_BIG: "message too big",
83 CloseCode.MANDATORY_EXTENSION: "mandatory extension",
84 CloseCode.INTERNAL_ERROR: "internal error",
85 CloseCode.SERVICE_RESTART: "service restart",
86 CloseCode.TRY_AGAIN_LATER: "try again later",
87 CloseCode.BAD_GATEWAY: "bad gateway",
88 CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]",
89}
92# Close code that are allowed in a close frame.
93# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`.
94EXTERNAL_CLOSE_CODES = {
95 CloseCode.NORMAL_CLOSURE,
96 CloseCode.GOING_AWAY,
97 CloseCode.PROTOCOL_ERROR,
98 CloseCode.UNSUPPORTED_DATA,
99 CloseCode.INVALID_DATA,
100 CloseCode.POLICY_VIOLATION,
101 CloseCode.MESSAGE_TOO_BIG,
102 CloseCode.MANDATORY_EXTENSION,
103 CloseCode.INTERNAL_ERROR,
104 CloseCode.SERVICE_RESTART,
105 CloseCode.TRY_AGAIN_LATER,
106 CloseCode.BAD_GATEWAY,
107}
110OK_CLOSE_CODES = {
111 CloseCode.NORMAL_CLOSURE,
112 CloseCode.GOING_AWAY,
113 CloseCode.NO_STATUS_RCVD,
114}
117@dataclasses.dataclass
118class Frame:
119 """
120 WebSocket frame.
122 Attributes:
123 opcode: Opcode.
124 data: Payload data.
125 fin: FIN bit.
126 rsv1: RSV1 bit.
127 rsv2: RSV2 bit.
128 rsv3: RSV3 bit.
130 Only these fields are needed. The MASK bit, payload length and masking-key
131 are handled on the fly when parsing and serializing frames.
133 """
135 opcode: Opcode
136 data: BytesLike
137 fin: bool = True
138 rsv1: bool = False
139 rsv2: bool = False
140 rsv3: bool = False
142 # Configure if you want to see more in logs. Should be a multiple of 3.
143 MAX_LOG_SIZE = int(os.environ.get("WEBSOCKETS_MAX_LOG_SIZE", "75"))
145 DEFAULT_IS_TEXT = {TEXT: True, BINARY: False, CLOSE: True}
147 def __str__(self) -> str:
148 """
149 Return a human-readable representation of a frame.
151 This function is intended for logging and debugging. It doesn't aim to
152 support round-tripping because payloads can be too long for displaying
153 conveniently. Instead, it shows the beginning and the end. It's robust
154 to incorrect data.
156 It attempts to decode UTF-8 in data frames, even in binary frames,
157 because those frequently contain UTF-8 data.
159 """
160 expect_text = self.DEFAULT_IS_TEXT.get(self.opcode)
161 data_repr, is_text = self._data_repr()
163 data_type = "" if expect_text == is_text else ("text" if is_text else "binary")
164 length = f"{len(self.data)} byte{'' if len(self.data) == 1 else 's'}"
165 non_final = "" if self.fin else "continued"
166 metadata = ", ".join(filter(None, [data_type, length, non_final]))
168 return f"{self.opcode.name} {data_repr} [{metadata}]"
170 def _data_repr(self) -> tuple[str, bool | None]:
171 """
172 Return a human-readable representation of the payload.
174 Also returns whether the payload is text.
176 The representation is elided to fit ``MAX_LOG_SIZE``.
178 This is a helper for the __str__ method.
180 """
181 data_repr: str = ""
182 is_text: bool | None = None
184 if not self.data:
185 return "''", self.DEFAULT_IS_TEXT.get(self.opcode)
187 # Close frames: parse close code and reason and display them as text.
188 # Fall back to binary when the payload is malformed.
190 if self.opcode is CLOSE:
191 try:
192 data_repr = str(Close.parse(self.data))
193 is_text = True
194 except (ProtocolError, UnicodeDecodeError):
195 data_repr = " ".join(f"{byte:02x}" for byte in self.data)
196 is_text = False
198 # Control frames: display printable ASCII payloads as text, else binary.
199 # We could decode UTF-8 payloads, but this causes confusion when random
200 # 4-bytes binary payloads are accidentally valid UTF-8 sequences.
202 elif self.opcode in CTRL_OPCODES:
203 if is_space_or_printable_ascii(self.data):
204 data_repr = repr(bytes(self.data).decode("ascii"))
205 is_text = True
206 else:
207 data_repr = " ".join(f"{byte:02x}" for byte in self.data)
208 is_text = False
210 # Data frames: check whether the payload is UTF-8, regardless of opcode,
211 # in order to display nicely UTF-8 text in binary frames, and be robust.
212 # Also support frames fragmented within UTF-8 sequences.
214 else:
215 if len(self.data) > 4 * self.MAX_LOG_SIZE:
216 # Process only the start and end, as the middle will be elided.
217 # Cast to bytes because self.data could be a memoryview.
218 data_start = bytes(self.data[: 8 * self.MAX_LOG_SIZE // 3])
219 data_end = bytes(self.data[-4 * self.MAX_LOG_SIZE // 3 :])
220 is_text = is_utf8_fragment(
221 data_start,
222 must_start_clean=self.opcode != CONT,
223 ) and is_utf8_fragment(
224 data_end,
225 must_end_clean=self.fin,
226 )
227 if is_text:
228 data_repr = repr((data_start + data_end).decode(errors="replace"))
230 else:
231 # Cast to bytes because self.data could be a memoryview.
232 data = bytes(self.data)
233 is_text = is_utf8_fragment(
234 data,
235 must_start_clean=self.opcode != CONT,
236 must_end_clean=self.fin,
237 )
238 if is_text:
239 data_repr = repr(data.decode(errors="replace"))
241 # When the payload is text (except perhaps for boundaries), we have
242 # enough in ``data_repr``. Do the same when the payload is binary.
244 if not is_text:
245 binary = self.data
246 if len(binary) > self.MAX_LOG_SIZE // 3:
247 cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8
248 # Encode two dummy bytes to force eliding and adding an ellipsis.
249 binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]])
250 data_repr = " ".join(f"{byte:02x}" for byte in binary)
252 # Elide the middle of the representation to fit the maximum log size.
254 if len(data_repr) > self.MAX_LOG_SIZE:
255 cut = self.MAX_LOG_SIZE // 3 - 1 # by default cut = 24
256 data_repr = data_repr[: 2 * cut] + "..." + data_repr[-cut:]
258 return data_repr, is_text
260 @classmethod
261 def parse(
262 cls,
263 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
264 *,
265 mask: bool,
266 max_size: int | None = None,
267 extensions: Sequence[extensions.Extension] | None = None,
268 ) -> Generator[None, None, Frame]:
269 """
270 Parse a WebSocket frame.
272 This is a generator-based coroutine.
274 Args:
275 read_exact: Generator-based coroutine that reads the requested
276 bytes or raises an exception if there isn't enough data.
277 mask: Whether the frame should be masked i.e. whether the read
278 happens on the server side.
279 max_size: Maximum payload size in bytes.
280 extensions: List of extensions, applied in reverse order.
282 Raises:
283 EOFError: If the connection is closed without a full WebSocket frame.
284 PayloadTooBig: If the frame's payload size exceeds ``max_size``.
285 ProtocolError: If the frame contains incorrect values.
287 """
288 # Read the header.
289 data = yield from read_exact(2)
290 head1, head2 = struct.unpack("!BB", data)
292 # While not Pythonic, this is marginally faster than calling bool().
293 fin = True if head1 & 0b10000000 else False
294 rsv1 = True if head1 & 0b01000000 else False
295 rsv2 = True if head1 & 0b00100000 else False
296 rsv3 = True if head1 & 0b00010000 else False
298 try:
299 opcode = Opcode(head1 & 0b00001111)
300 except ValueError as exc:
301 raise ProtocolError("invalid opcode") from exc
303 if (True if head2 & 0b10000000 else False) != mask:
304 raise ProtocolError("incorrect masking")
306 length = head2 & 0b01111111
307 if length == 126:
308 data = yield from read_exact(2)
309 (length,) = struct.unpack("!H", data)
310 elif length == 127:
311 data = yield from read_exact(8)
312 (length,) = struct.unpack("!Q", data)
313 if max_size is not None and length > max_size:
314 raise PayloadTooBig(length, max_size)
315 if mask:
316 mask_bytes = yield from read_exact(4)
318 # Read the data.
319 data = yield from read_exact(length)
320 if mask:
321 data = apply_mask(data, mask_bytes)
323 frame = cls(opcode, data, fin, rsv1, rsv2, rsv3)
325 if extensions is None:
326 extensions = []
327 for extension in reversed(extensions):
328 frame = extension.decode(frame, max_size=max_size)
330 frame.check()
332 return frame
334 def serialize(
335 self,
336 *,
337 mask: bool,
338 extensions: Sequence[extensions.Extension] | None = None,
339 ) -> bytes:
340 """
341 Serialize a WebSocket frame.
343 Args:
344 mask: Whether the frame should be masked i.e. whether the write
345 happens on the client side.
346 extensions: List of extensions, applied in order.
348 Raises:
349 ProtocolError: If the frame contains incorrect values.
351 """
352 self.check()
354 if extensions is None:
355 extensions = []
356 for extension in extensions:
357 self = extension.encode(self)
359 output = io.BytesIO()
361 # Prepare the header.
362 head1 = (
363 (0b10000000 if self.fin else 0)
364 | (0b01000000 if self.rsv1 else 0)
365 | (0b00100000 if self.rsv2 else 0)
366 | (0b00010000 if self.rsv3 else 0)
367 | self.opcode
368 )
370 head2 = 0b10000000 if mask else 0
372 length = len(self.data)
373 if length < 126:
374 output.write(struct.pack("!BB", head1, head2 | length))
375 elif length < 65536:
376 output.write(struct.pack("!BBH", head1, head2 | 126, length))
377 else:
378 output.write(struct.pack("!BBQ", head1, head2 | 127, length))
380 if mask:
381 mask_bytes = secrets.token_bytes(4)
382 output.write(mask_bytes)
384 # Prepare the data.
385 data: BytesLike
386 if mask:
387 data = apply_mask(self.data, mask_bytes)
388 else:
389 data = self.data
390 output.write(data)
392 return output.getvalue()
394 def check(self) -> None:
395 """
396 Check that reserved bits and opcode have acceptable values.
398 Raises:
399 ProtocolError: If a reserved bit or the opcode is invalid.
401 """
402 if self.rsv1 or self.rsv2 or self.rsv3:
403 raise ProtocolError("reserved bits must be 0")
405 if self.opcode in CTRL_OPCODES:
406 if len(self.data) > 125:
407 raise ProtocolError("control frame too long")
408 if not self.fin:
409 raise ProtocolError("fragmented control frame")
412@dataclasses.dataclass
413class Close:
414 """
415 Code and reason for WebSocket close frames.
417 Attributes:
418 code: Close code.
419 reason: Close reason.
421 """
423 code: CloseCode | int
424 reason: str
426 def __str__(self) -> str:
427 """
428 Return a human-readable representation of a close code and reason.
430 """
431 if 3000 <= self.code < 4000:
432 explanation = "registered"
433 elif 4000 <= self.code < 5000:
434 explanation = "private use"
435 else:
436 explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown")
437 result = f"{self.code} ({explanation})"
439 if self.reason:
440 result = f"{result} {self.reason}"
442 return result
444 @classmethod
445 def parse(cls, data: BytesLike) -> Self:
446 """
447 Parse the payload of a close frame.
449 Args:
450 data: Payload of the close frame.
452 Raises:
453 ProtocolError: If data is ill-formed.
454 UnicodeDecodeError: If the reason isn't valid UTF-8.
456 """
457 if isinstance(data, memoryview):
458 raise AssertionError("only compressed outgoing frames use memoryview")
459 if len(data) >= 2:
460 (code,) = struct.unpack("!H", data[:2])
461 reason = data[2:].decode()
462 close = cls(code, reason)
463 close.check()
464 return close
465 elif len(data) == 0:
466 return cls(CloseCode.NO_STATUS_RCVD, "")
467 else:
468 raise ProtocolError("close frame too short")
470 def serialize(self) -> bytes:
471 """
472 Serialize the payload of a close frame.
474 """
475 self.check()
476 return struct.pack("!H", self.code) + self.reason.encode()
478 def check(self) -> None:
479 """
480 Check that the close code has a valid value for a close frame.
482 Raises:
483 ProtocolError: If the close code is invalid.
485 """
486 if not (self.code in EXTERNAL_CLOSE_CODES or 3000 <= self.code < 5000):
487 raise ProtocolError("invalid status code")
490def is_utf8_fragment(
491 data: bytes,
492 must_start_clean: bool = False,
493 must_end_clean: bool = False,
494) -> bool:
495 """Guess if data is a fragment of UTF-8 text."""
496 # Possible byte sequences for UTF-8 characters are:
497 # 0xxxxxxx
498 # 110xxxxx 10xxxxxx
499 # 1110xxxx 10xxxxxx 10xxxxxx
500 # 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
502 # The algorithm determines ``start`` and ``end`` so that ``data[start:end]``
503 # must be a valid UTF-8 sequence for data to be a valid UTF-8 fragment.
505 start, end = 0, len(data)
507 if not must_start_clean:
508 # Remove continuation bytes from the beginning.
509 max_start = min(3, len(data))
510 while start < max_start:
511 byte = data[start]
513 # Continuation byte
514 if byte & 0b11000000 == 0b10000000:
515 start += 1
516 continue
518 break
520 if not must_end_clean:
521 # Remove a partial multibyte sequence from the end.
522 end -= 1 # index of the last byte
523 min_end = max(len(data) - 4, start)
524 while end >= min_end:
525 byte = data[end]
526 # Continuation byte
527 if byte & 0b11000000 == 0b10000000:
528 end -= 1
529 continue
531 # ASCII byte
532 if byte & 0b10000000 == 0b00000000:
533 seq_len = 1
534 # Leading byte of a 2-byte sequence
535 elif byte & 0b11100000 == 0b11000000:
536 seq_len = 2
537 # Leading byte of a 3-byte sequence
538 elif byte & 0b11110000 == 0b11100000:
539 seq_len = 3
540 # Leading byte of a 4-byte sequence
541 elif byte & 0b11111000 == 0b11110000:
542 seq_len = 4
543 # Invalid byte
544 else:
545 seq_len = 0
547 # Cut only when there's an incomplete sequence at the end.
548 if seq_len <= len(data) - end:
549 end = len(data)
551 break
553 try:
554 text = data[start:end].decode()
555 except UnicodeDecodeError:
556 return False
557 else:
558 # Non-printable characters signal binary data.
559 return "\\x" not in repr(text)
562# At the bottom to break import cycles created by type annotations.
563from . import extensions # noqa: E402