Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websockets/frames.py: 48%
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 secrets
8import struct
9from collections.abc import Generator, Sequence
10from typing import Callable, Self
12from .exceptions import PayloadTooBig, ProtocolError
13from .typing import BytesLike
16try:
17 from .speedups import apply_mask
18except ImportError:
19 from .utils import apply_mask
22__all__ = [
23 "Opcode",
24 "CloseCode",
25 "Frame",
26 "Close",
27]
30class Opcode(enum.IntEnum):
31 """Opcode values for WebSocket frames."""
33 CONT, TEXT, BINARY = 0x00, 0x01, 0x02
34 CLOSE, PING, PONG = 0x08, 0x09, 0x0A
37CONT = Opcode.CONT
38TEXT = Opcode.TEXT
39BINARY = Opcode.BINARY
40CLOSE = Opcode.CLOSE
41PING = Opcode.PING
42PONG = Opcode.PONG
44DATA_OPCODES = CONT, TEXT, BINARY
45CTRL_OPCODES = CLOSE, PING, PONG
48class CloseCode(enum.IntEnum):
49 """Close code values for WebSocket close frames."""
51 NORMAL_CLOSURE = 1000
52 GOING_AWAY = 1001
53 PROTOCOL_ERROR = 1002
54 UNSUPPORTED_DATA = 1003
55 # 1004 is reserved
56 NO_STATUS_RCVD = 1005
57 ABNORMAL_CLOSURE = 1006
58 INVALID_DATA = 1007
59 POLICY_VIOLATION = 1008
60 MESSAGE_TOO_BIG = 1009
61 MANDATORY_EXTENSION = 1010
62 INTERNAL_ERROR = 1011
63 SERVICE_RESTART = 1012
64 TRY_AGAIN_LATER = 1013
65 BAD_GATEWAY = 1014
66 TLS_HANDSHAKE = 1015
69# See https://www.iana.org/assignments/websocket/websocket.xhtml
70CLOSE_CODE_EXPLANATIONS: dict[int, str] = {
71 CloseCode.NORMAL_CLOSURE: "OK",
72 CloseCode.GOING_AWAY: "going away",
73 CloseCode.PROTOCOL_ERROR: "protocol error",
74 CloseCode.UNSUPPORTED_DATA: "unsupported data",
75 CloseCode.NO_STATUS_RCVD: "no status received [internal]",
76 CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]",
77 CloseCode.INVALID_DATA: "invalid frame payload data",
78 CloseCode.POLICY_VIOLATION: "policy violation",
79 CloseCode.MESSAGE_TOO_BIG: "message too big",
80 CloseCode.MANDATORY_EXTENSION: "mandatory extension",
81 CloseCode.INTERNAL_ERROR: "internal error",
82 CloseCode.SERVICE_RESTART: "service restart",
83 CloseCode.TRY_AGAIN_LATER: "try again later",
84 CloseCode.BAD_GATEWAY: "bad gateway",
85 CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]",
86}
89# Close code that are allowed in a close frame.
90# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`.
91EXTERNAL_CLOSE_CODES = {
92 CloseCode.NORMAL_CLOSURE,
93 CloseCode.GOING_AWAY,
94 CloseCode.PROTOCOL_ERROR,
95 CloseCode.UNSUPPORTED_DATA,
96 CloseCode.INVALID_DATA,
97 CloseCode.POLICY_VIOLATION,
98 CloseCode.MESSAGE_TOO_BIG,
99 CloseCode.MANDATORY_EXTENSION,
100 CloseCode.INTERNAL_ERROR,
101 CloseCode.SERVICE_RESTART,
102 CloseCode.TRY_AGAIN_LATER,
103 CloseCode.BAD_GATEWAY,
104}
107OK_CLOSE_CODES = {
108 CloseCode.NORMAL_CLOSURE,
109 CloseCode.GOING_AWAY,
110 CloseCode.NO_STATUS_RCVD,
111}
114@dataclasses.dataclass
115class Frame:
116 """
117 WebSocket frame.
119 Attributes:
120 opcode: Opcode.
121 data: Payload data.
122 fin: FIN bit.
123 rsv1: RSV1 bit.
124 rsv2: RSV2 bit.
125 rsv3: RSV3 bit.
127 Only these fields are needed. The MASK bit, payload length and masking-key
128 are handled on the fly when parsing and serializing frames.
130 """
132 opcode: Opcode
133 data: BytesLike
134 fin: bool = True
135 rsv1: bool = False
136 rsv2: bool = False
137 rsv3: bool = False
139 # Configure if you want to see more in logs. Should be a multiple of 3.
140 MAX_LOG_SIZE = int(os.environ.get("WEBSOCKETS_MAX_LOG_SIZE", "75"))
142 DEFAULT_IS_TEXT = {TEXT: True, BINARY: False, CLOSE: True}
144 def __str__(self) -> str:
145 """
146 Return a human-readable representation of a frame.
148 This function is intended for logging and debugging. It doesn't aim to
149 support round-tripping because payloads can be too long for displaying
150 conveniently. Instead, it shows the beginning and the end. It's robust
151 to incorrect data.
153 It attempts to decode UTF-8 payloads whenever possible, even for binary
154 frames and control frames, because those frequently contain UTF-8 data.
155 It applies the same logic to continuation frames, because we don't know
156 if they continue a text frame or a binary frame.
158 """
159 expect_text = self.DEFAULT_IS_TEXT.get(self.opcode)
160 data_repr, is_text = self._data_repr()
162 data_type = "" if expect_text == is_text else ("text" if is_text else "binary")
163 length = f"{len(self.data)} byte{'' if len(self.data) == 1 else 's'}"
164 non_final = "" if self.fin else "continued"
165 metadata = ", ".join(filter(None, [data_type, length, non_final]))
167 return f"{self.opcode.name} {data_repr} [{metadata}]"
169 def _data_repr(self) -> tuple[str, bool | None]:
170 """
171 Return a human-readable representation of the payload.
173 Also returns whether the payload is text.
175 The representation is elided to fit ``MAX_LOG_SIZE``.
177 This is a helper for the __str__ method.
179 """
180 if not self.data:
181 return "''", self.DEFAULT_IS_TEXT.get(self.opcode)
183 # Special case for close frames: parse close code and reason.
184 # Fall back to the standard case if the payload is malformed.
186 if self.opcode is CLOSE:
187 try:
188 return str(Close.parse(self.data)), True
189 except (ProtocolError, UnicodeDecodeError):
190 pass
192 # Guess whether the payload is UTF-8 or binary, regardless of opcode, to
193 # display UTF-8 text in binary frames nicely and generally to be helpful
194 # and robust. Also support frames fragmented within UTF-8 sequences.
196 if len(self.data) > 4 * self.MAX_LOG_SIZE:
197 # Process only the start and the end, as the middle will be elided.
198 # Cast to bytes because self.data could be a memoryview.
199 data_start = bytes(self.data[: 8 * self.MAX_LOG_SIZE // 3])
200 data_end = bytes(self.data[-4 * self.MAX_LOG_SIZE // 3 :])
201 is_text = is_utf8_fragment(
202 data_start,
203 must_start_clean=self.opcode != CONT,
204 ) and is_utf8_fragment(
205 data_end,
206 must_end_clean=self.fin,
207 )
208 if is_text:
209 data_repr = repr((data_start + data_end).decode(errors="replace"))
211 else:
212 # Cast to bytes because self.data could be a memoryview.
213 data = bytes(self.data)
214 is_text = is_utf8_fragment(
215 data,
216 must_start_clean=self.opcode != CONT,
217 must_end_clean=self.fin,
218 )
219 if is_text:
220 data_repr = repr(data.decode(errors="replace"))
222 # When the payload is text (except perhaps for boundaries), we decoded
223 # enough in ``data_repr``. Now, do the same when the payload is binary.
225 if not is_text:
226 binary = self.data
227 if len(binary) > self.MAX_LOG_SIZE // 3:
228 cut = (self.MAX_LOG_SIZE // 3 - 1) // 3 # by default cut = 8
229 # Encode two dummy bytes to force eliding and adding an ellipsis.
230 binary = b"".join([binary[: 2 * cut], b"\x00\x00", binary[-cut:]])
231 data_repr = " ".join(f"{byte:02x}" for byte in binary)
233 # Elide the middle of the representation to fit the maximum log size.
235 if len(data_repr) > self.MAX_LOG_SIZE:
236 cut = self.MAX_LOG_SIZE // 3 - 1 # by default cut = 24
237 data_repr = data_repr[: 2 * cut] + "..." + data_repr[-cut:]
239 return data_repr, is_text
241 @classmethod
242 def parse(
243 cls,
244 read_exact: Callable[[int], Generator[None, None, bytes | bytearray]],
245 *,
246 mask: bool,
247 max_size: int | None = None,
248 extensions: Sequence[extensions.Extension] | None = None,
249 ) -> Generator[None, None, Frame]:
250 """
251 Parse a WebSocket frame.
253 This is a generator-based coroutine.
255 Args:
256 read_exact: Generator-based coroutine that reads the requested
257 bytes or raises an exception if there isn't enough data.
258 mask: Whether the frame should be masked i.e. whether the read
259 happens on the server side.
260 max_size: Maximum payload size in bytes.
261 extensions: List of extensions, applied in reverse order.
263 Raises:
264 EOFError: If the connection is closed without a full WebSocket frame.
265 PayloadTooBig: If the frame's payload size exceeds ``max_size``.
266 ProtocolError: If the frame contains incorrect values.
268 """
269 # Read the header.
270 data = yield from read_exact(2)
271 head1, head2 = struct.unpack("!BB", data)
273 # While not Pythonic, this is marginally faster than calling bool().
274 fin = True if head1 & 0b10000000 else False
275 rsv1 = True if head1 & 0b01000000 else False
276 rsv2 = True if head1 & 0b00100000 else False
277 rsv3 = True if head1 & 0b00010000 else False
279 try:
280 opcode = Opcode(head1 & 0b00001111)
281 except ValueError as exc:
282 raise ProtocolError("invalid opcode") from exc
284 if (True if head2 & 0b10000000 else False) != mask:
285 raise ProtocolError("incorrect masking")
287 length = head2 & 0b01111111
288 if length == 126:
289 data = yield from read_exact(2)
290 (length,) = struct.unpack("!H", data)
291 elif length == 127:
292 data = yield from read_exact(8)
293 (length,) = struct.unpack("!Q", data)
294 if max_size is not None and length > max_size:
295 raise PayloadTooBig(length, max_size)
296 if mask:
297 mask_bytes = yield from read_exact(4)
299 # Read the data.
300 data = yield from read_exact(length)
301 if mask:
302 data = apply_mask(data, mask_bytes)
304 frame = cls(opcode, data, fin, rsv1, rsv2, rsv3)
306 if extensions is None:
307 extensions = []
308 for extension in reversed(extensions):
309 frame = extension.decode(frame, max_size=max_size)
311 frame.check()
313 return frame
315 def serialize(
316 self,
317 *,
318 mask: bool,
319 extensions: Sequence[extensions.Extension] | None = None,
320 ) -> bytes:
321 """
322 Serialize a WebSocket frame.
324 Args:
325 mask: Whether the frame should be masked i.e. whether the write
326 happens on the client side.
327 extensions: List of extensions, applied in order.
329 Raises:
330 ProtocolError: If the frame contains incorrect values.
332 """
333 self.check()
335 if extensions is None:
336 extensions = []
337 for extension in extensions:
338 self = extension.encode(self)
340 output = io.BytesIO()
342 # Prepare the header.
343 head1 = (
344 (0b10000000 if self.fin else 0)
345 | (0b01000000 if self.rsv1 else 0)
346 | (0b00100000 if self.rsv2 else 0)
347 | (0b00010000 if self.rsv3 else 0)
348 | self.opcode
349 )
351 head2 = 0b10000000 if mask else 0
353 length = len(self.data)
354 if length < 126:
355 output.write(struct.pack("!BB", head1, head2 | length))
356 elif length < 65536:
357 output.write(struct.pack("!BBH", head1, head2 | 126, length))
358 else:
359 output.write(struct.pack("!BBQ", head1, head2 | 127, length))
361 if mask:
362 mask_bytes = secrets.token_bytes(4)
363 output.write(mask_bytes)
365 # Prepare the data.
366 data: BytesLike
367 if mask:
368 data = apply_mask(self.data, mask_bytes)
369 else:
370 data = self.data
371 output.write(data)
373 return output.getvalue()
375 def check(self) -> None:
376 """
377 Check that reserved bits and opcode have acceptable values.
379 Raises:
380 ProtocolError: If a reserved bit or the opcode is invalid.
382 """
383 if self.rsv1 or self.rsv2 or self.rsv3:
384 raise ProtocolError("reserved bits must be 0")
386 if self.opcode in CTRL_OPCODES:
387 if len(self.data) > 125:
388 raise ProtocolError("control frame too long")
389 if not self.fin:
390 raise ProtocolError("fragmented control frame")
393@dataclasses.dataclass
394class Close:
395 """
396 Code and reason for WebSocket close frames.
398 Attributes:
399 code: Close code.
400 reason: Close reason.
402 """
404 code: CloseCode | int
405 reason: str
407 def __str__(self) -> str:
408 """
409 Return a human-readable representation of a close code and reason.
411 """
412 if 3000 <= self.code < 4000:
413 explanation = "registered"
414 elif 4000 <= self.code < 5000:
415 explanation = "private use"
416 else:
417 explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown")
418 result = f"{self.code} ({explanation})"
420 if self.reason:
421 result = f"{result} {self.reason}"
423 return result
425 @classmethod
426 def parse(cls, data: BytesLike) -> Self:
427 """
428 Parse the payload of a close frame.
430 Args:
431 data: Payload of the close frame.
433 Raises:
434 ProtocolError: If data is ill-formed.
435 UnicodeDecodeError: If the reason isn't valid UTF-8.
437 """
438 if isinstance(data, memoryview):
439 raise AssertionError("only compressed outgoing frames use memoryview")
440 if len(data) >= 2:
441 (code,) = struct.unpack("!H", data[:2])
442 reason = data[2:].decode()
443 close = cls(code, reason)
444 close.check()
445 return close
446 elif len(data) == 0:
447 return cls(CloseCode.NO_STATUS_RCVD, "")
448 else:
449 raise ProtocolError("close frame too short")
451 def serialize(self) -> bytes:
452 """
453 Serialize the payload of a close frame.
455 """
456 self.check()
457 return struct.pack("!H", self.code) + self.reason.encode()
459 def check(self) -> None:
460 """
461 Check that the close code has a valid value for a close frame.
463 Raises:
464 ProtocolError: If the close code is invalid.
466 """
467 if not (self.code in EXTERNAL_CLOSE_CODES or 3000 <= self.code < 5000):
468 raise ProtocolError("invalid status code")
471def is_utf8_fragment(
472 data: bytes,
473 must_start_clean: bool = False,
474 must_end_clean: bool = False,
475) -> bool:
476 """Guess if data is a fragment of UTF-8 text."""
477 # Possible byte sequences for UTF-8 characters are:
478 # 0xxxxxxx
479 # 110xxxxx 10xxxxxx
480 # 1110xxxx 10xxxxxx 10xxxxxx
481 # 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
483 # The algorithm determines ``start`` and ``end`` so that ``data[start:end]``
484 # must be a valid UTF-8 sequence for data to be a valid UTF-8 fragment.
486 start, end = 0, len(data)
488 if not must_start_clean:
489 # Remove continuation bytes from the beginning.
490 max_start = min(3, len(data))
491 while start < max_start:
492 byte = data[start]
494 # Continuation byte
495 if byte & 0b11000000 == 0b10000000:
496 start += 1
497 continue
499 break
501 if not must_end_clean:
502 # Remove a partial multibyte sequence from the end.
503 end -= 1 # index of the last byte
504 min_end = max(len(data) - 4, start)
505 while end >= min_end:
506 byte = data[end]
507 # Continuation byte
508 if byte & 0b11000000 == 0b10000000:
509 end -= 1
510 continue
512 # ASCII byte
513 if byte & 0b10000000 == 0b00000000:
514 seq_len = 1
515 # Leading byte of a 2-byte sequence
516 elif byte & 0b11100000 == 0b11000000:
517 seq_len = 2
518 # Leading byte of a 3-byte sequence
519 elif byte & 0b11110000 == 0b11100000:
520 seq_len = 3
521 # Leading byte of a 4-byte sequence
522 elif byte & 0b11111000 == 0b11110000:
523 seq_len = 4
524 # Invalid byte
525 else:
526 seq_len = 0
528 # Cut only when there's an incomplete sequence at the end.
529 if seq_len <= len(data) - end:
530 end = len(data)
532 break
534 try:
535 text = data[start:end].decode()
536 except UnicodeDecodeError:
537 return False
538 else:
539 # Non-printable characters signal binary data.
540 return "\\x" not in repr(text)
543# At the bottom to break import cycles created by type annotations.
544from . import extensions # noqa: E402