Skip to content

Websocket

Status: ✅ Implemented (axon-web#5) Milestone: M14 Protocol: RFC 6455 Dependencies: SHA-1 (via extern), Base64 (via extern)

The tools/websocket.py module implements the WebSocket wire protocol per RFC 6455. It handles frame parsing, serialization, XOR masking, handshake validation, and the Sec-WebSocket-Accept key computation — everything needed to upgrade an HTTP connection to a persistent, full-duplex WebSocket channel.

from websocket import (
compute_accept_key,
parse_frame,
serialize_frame,
mask_payload,
validate_handshake,
build_handshake_response,
Opcode,
)
# Validate an upgrade request
ok, reason = validate_handshake(request_headers)
if ok:
response = build_handshake_response(request_headers["Sec-WebSocket-Key"])
# Send response to client, connection is now upgraded
# Send a text frame
frame_bytes = serialize_frame(Opcode.TEXT, b"Hello, WebSocket!")
# Parse a received frame
frame, bytes_consumed = parse_frame(raw_bytes)
print(frame.opcode, frame.payload)

WebSocket defines six frame types via the Opcode enum:

OpcodeValueTypeDescription
CONTINUATION0x0DataContinuation of a fragmented message
TEXT0x1DataUTF-8 text payload
BINARY0x2DataBinary payload
CLOSE0x8ControlConnection close (with optional status code + reason)
PING0x9ControlHeartbeat request
PONG0xAControlHeartbeat response

Control frames (CLOSE, PING, PONG) must not exceed 125 bytes of payload and must not be fragmented.

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------+-------------------------------+
| Payload Data continued ... |
+---------------------------------------------------------------+
Encoded valueActual lengthExtra bytes
0–125Direct value0
126Next 2 bytes (big-endian uint16)2
127Next 8 bytes (big-endian uint64)8

compute_accept_key(client_key: str) -> str

Section titled “compute_accept_key(client_key: str) -> str”

Compute the Sec-WebSocket-Accept header value per RFC 6455 §4.2.2. Concatenates the client key with the magic GUID, takes SHA-1, and base64-encodes the result.

mask_payload(payload: bytes, mask_key: bytes) -> bytes

Section titled “mask_payload(payload: bytes, mask_key: bytes) -> bytes”

Apply XOR masking per §5.3. Masking is involutory: mask(mask(data, key), key) == data.

Parse a WebSocket frame from raw bytes. Returns the frame (with unmasked payload) and the number of bytes consumed. Raises WebSocketError on protocol violations.

serialize_frame(opcode, payload, *, fin=True, mask=False, mask_key=None) -> bytes

Section titled “serialize_frame(opcode, payload, *, fin=True, mask=False, mask_key=None) -> bytes”

Serialize a frame to bytes. If mask=True and no mask_key is provided, a random 4-byte key is generated.

validate_handshake(headers: dict) -> (bool, str)

Section titled “validate_handshake(headers: dict) -> (bool, str)”

Validate a WebSocket upgrade request. Checks Upgrade, Connection, Sec-WebSocket-Key, and Sec-WebSocket-Version headers.

build_handshake_response(client_key: str) -> str

Section titled “build_handshake_response(client_key: str) -> str”

Build the HTTP 101 Switching Protocols response string.

FieldTypeDescription
finboolFinal fragment flag
opcodeOpcodeFrame type
maskedboolWhether payload was masked
payload_lenintPayload length
mask_keybytes | None4-byte mask key
payloadbytesUnmasked payload
┌────────────┐ handshake ┌──────┐
│ Connecting ├──────────────────► Open │
└────────────┘ └──┬───┘
│ close frame
┌───────┐
│Closing│
└──┬────┘
│ close acknowledged
┌──────┐
│Closed│
└──────┘

The WebSocket handshake upgrades an HTTP/1.1 connection:

  1. Client sends an HTTP GET with upgrade headers
  2. Server validates the headers (validate_handshake)
  3. Server responds with HTTP 101 (build_handshake_response)
  4. Both sides switch to the WebSocket frame protocol
HeaderValue
Upgradewebsocket
ConnectionUpgrade
Sec-WebSocket-KeyBase64-encoded 16 random bytes
Sec-WebSocket-Version13
  • Masking: Client-to-server frames MUST be masked (§5.1). Server-to-client frames MUST NOT be masked.
  • Origin checking: Applications should validate the Origin header to prevent cross-site WebSocket hijacking.
  • Message size limits: Applications should enforce maximum message sizes to prevent memory exhaustion.
Terminal window
# Unit tests (Python prototype)
python3 -m pytest tools/test_websocket.py -v
# Property-based tests
python3 -m pytest tools/test_websocket_properties.py -v
# Cross-validation tests
python3 -m pytest tests/test_websocket.py -v

The test suite covers:

  • RFC 6455 §4.2.2 accept key test vector
  • Frame parsing for all opcodes (text, binary, ping, pong, close)
  • Masked and unmasked frame roundtrips
  • Payload size boundaries (0, 125, 126, 65535, 65536)
  • Handshake validation (valid + 8 invalid cases)
  • Masking involution (property: mask(mask(x, k), k) == x)
  • Serialize/parse roundtrip (property: parse(serialize(f)) == f)
  • Payload length encoding correctness (7-bit, 16-bit, 64-bit)
  • Zero dependencies — uses only the Python standard library.
  • Separate parse/serialize — no connection I/O in the codec layer; the caller manages the socket. This makes the codec testable without network access.
  • Unmask on parseparse_frame always returns unmasked payloads, simplifying downstream consumers.
  • Involutory masking — a single mask_payload function handles both masking and unmasking (XOR is its own inverse).