Bsdkrun.WsFrame (bsdkrun_ex v0.3.1)

Copy Markdown View Source

Hand-rolled RFC 6455 WebSocket framing for Bsdkrun.GraphQLSocket: the Sec-WebSocket-Accept handshake computation, encoding a masked client frame, and decoding a frame off the front of a byte buffer.

Every function here is pure (no socket, no process), so the protocol logic can be exercised directly in tests via plain binary pattern matching.

Shortcut taken: fragmented/continuation frames (fin: 0, or opcode: 0x0) are parsed structurally — decode/1 returns them like any other frame — but never reassembled into one logical message. graphql-transport-ws messages are short JSON control frames and modest binary chunks that fit in a single frame in practice, so Bsdkrun.GraphQLSocket only acts on complete single-frame text messages (opcode: 0x1, fin: 1) and silently drops everything else (binary, ping, pong, close, continuation) at the WS-frame layer. The daemon's own connection teardown is still detected — that happens at the TCP/TLS level (:tcp_closed / :ssl_closed), not by parsing an RFC 6455 close frame, so this shortcut does not lose that signal.

Summary

Types

A decoded WebSocket frame.

Functions

The Sec-WebSocket-Accept value a compliant server must return for the given client Sec-WebSocket-Key (RFC 6455 §1.3): SHA-1 of the key concatenated with the RFC 6455 magic GUID, base64-encoded.

Decode one frame off the front of buffer.

Encode a masked client frame (RFC 6455 §5.2) with the given opcode and binary payload. Client -> server frames are always masked, with a fresh random 4-byte key per frame.

Encode a masked client text frame (opcode 0x1) carrying text.

XOR data with the 4-byte mask_key, cycling every 4 bytes (RFC 6455 §5.3). The same operation masks (client -> server) and unmasks (applying it twice is the identity), so decode/1 reuses this to unmask an incoming masked frame.

A fresh random base64 Sec-WebSocket-Key, per RFC 6455 §4.1 (16 random bytes).

Types

frame()

@type frame() :: %{fin: 0 | 1, opcode: non_neg_integer(), payload: binary()}

A decoded WebSocket frame.

Functions

accept_key(client_key)

@spec accept_key(String.t()) :: String.t()

The Sec-WebSocket-Accept value a compliant server must return for the given client Sec-WebSocket-Key (RFC 6455 §1.3): SHA-1 of the key concatenated with the RFC 6455 magic GUID, base64-encoded.

decode(arg1)

@spec decode(binary()) :: {:ok, frame(), binary()} | :incomplete

Decode one frame off the front of buffer.

Returns {:ok, frame, rest} when a complete frame is present at the front of buffer (rest is whatever bytes follow it, possibly empty, possibly the start of the next frame), or :incomplete when buffer does not yet hold a full frame and the caller should wait for more bytes.

encode(opcode, payload)

@spec encode(non_neg_integer(), binary()) :: binary()

Encode a masked client frame (RFC 6455 §5.2) with the given opcode and binary payload. Client -> server frames are always masked, with a fresh random 4-byte key per frame.

encode_text(text)

@spec encode_text(String.t()) :: binary()

Encode a masked client text frame (opcode 0x1) carrying text.

mask(data, mask_key)

@spec mask(binary(), <<_::32>>) :: binary()

XOR data with the 4-byte mask_key, cycling every 4 bytes (RFC 6455 §5.3). The same operation masks (client -> server) and unmasks (applying it twice is the identity), so decode/1 reuses this to unmask an incoming masked frame.

random_key()

@spec random_key() :: String.t()

A fresh random base64 Sec-WebSocket-Key, per RFC 6455 §4.1 (16 random bytes).