Websocket
WebSocket Protocol
Section titled “WebSocket Protocol”Status: ✅ Implemented (axon-web#5) Milestone: M14 Protocol: RFC 6455 Dependencies: SHA-1 (via extern), Base64 (via extern)
Overview
Section titled “Overview”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.
Quick Start
Section titled “Quick Start”from websocket import ( compute_accept_key, parse_frame, serialize_frame, mask_payload, validate_handshake, build_handshake_response, Opcode,)
# Validate an upgrade requestok, 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 frameframe_bytes = serialize_frame(Opcode.TEXT, b"Hello, WebSocket!")
# Parse a received frameframe, bytes_consumed = parse_frame(raw_bytes)print(frame.opcode, frame.payload)Frame Types
Section titled “Frame Types”WebSocket defines six frame types via the Opcode enum:
| Opcode | Value | Type | Description |
|---|---|---|---|
CONTINUATION | 0x0 | Data | Continuation of a fragmented message |
TEXT | 0x1 | Data | UTF-8 text payload |
BINARY | 0x2 | Data | Binary payload |
CLOSE | 0x8 | Control | Connection close (with optional status code + reason) |
PING | 0x9 | Control | Heartbeat request |
PONG | 0xA | Control | Heartbeat response |
Control frames (CLOSE, PING, PONG) must not exceed 125 bytes of
payload and must not be fragmented.
Frame Format
Section titled “Frame Format” 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 ... |+---------------------------------------------------------------+Payload Length Encoding
Section titled “Payload Length Encoding”| Encoded value | Actual length | Extra bytes |
|---|---|---|
| 0–125 | Direct value | 0 |
| 126 | Next 2 bytes (big-endian uint16) | 2 |
| 127 | Next 8 bytes (big-endian uint64) | 8 |
API Reference
Section titled “API Reference”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_frame(data: bytes) -> (Frame, int)
Section titled “parse_frame(data: bytes) -> (Frame, int)”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.
Frame (dataclass)
Section titled “Frame (dataclass)”| Field | Type | Description |
|---|---|---|
fin | bool | Final fragment flag |
opcode | Opcode | Frame type |
masked | bool | Whether payload was masked |
payload_len | int | Payload length |
mask_key | bytes | None | 4-byte mask key |
payload | bytes | Unmasked payload |
Connection State Machine
Section titled “Connection State Machine”┌────────────┐ handshake ┌──────┐│ Connecting ├──────────────────► Open │└────────────┘ └──┬───┘ │ close frame ▼ ┌───────┐ │Closing│ └──┬────┘ │ close acknowledged ▼ ┌──────┐ │Closed│ └──────┘Handshake
Section titled “Handshake”The WebSocket handshake upgrades an HTTP/1.1 connection:
- Client sends an HTTP GET with upgrade headers
- Server validates the headers (
validate_handshake) - Server responds with HTTP 101 (
build_handshake_response) - Both sides switch to the WebSocket frame protocol
Required Client Headers
Section titled “Required Client Headers”| Header | Value |
|---|---|
Upgrade | websocket |
Connection | Upgrade |
Sec-WebSocket-Key | Base64-encoded 16 random bytes |
Sec-WebSocket-Version | 13 |
Security Considerations
Section titled “Security Considerations”- Masking: Client-to-server frames MUST be masked (§5.1). Server-to-client frames MUST NOT be masked.
- Origin checking: Applications should validate the
Originheader to prevent cross-site WebSocket hijacking. - Message size limits: Applications should enforce maximum message sizes to prevent memory exhaustion.
Testing
Section titled “Testing”# Unit tests (Python prototype)python3 -m pytest tools/test_websocket.py -v
# Property-based testspython3 -m pytest tools/test_websocket_properties.py -v
# Cross-validation testspython3 -m pytest tests/test_websocket.py -vThe 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)
Design Decisions
Section titled “Design Decisions”- 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 parse —
parse_framealways returns unmasked payloads, simplifying downstream consumers. - Involutory masking — a single
mask_payloadfunction handles both masking and unmasking (XOR is its own inverse).
See Also
Section titled “See Also”- architecture.md — overall framework architecture
- middleware.md — middleware pipeline (provides Request/Response)
- router.md — HTTP request router
- static-server.md — static file serving
- json-codec.md — JSON request/response codec