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
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
@type frame() :: %{fin: 0 | 1, opcode: non_neg_integer(), payload: binary()}
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.
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.
@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 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.
@spec random_key() :: String.t()
A fresh random base64 Sec-WebSocket-Key, per RFC 6455 §4.1 (16 random bytes).