Module: Bsdkrun::WebSocketFrame
- Defined in:
- lib/bsdkrun/websocket_frame.rb
Overview
RFC 6455 §5.2 frame encode/decode, hand-rolled because the Ruby standard library has no WebSocket client.
Deliberately narrow: only what WsClient needs to speak
graphql-transport-ws, which exchanges small JSON text frames. Fragmented
messages (a logical message split across CONTINUATION frames) are
unsupported — WebSocketFrame.read returns whatever single frame arrives, and a caller
that gets a non-FIN frame is expected to treat it as an error. In
practice every graphql-ws message this SDK deals with is small enough
that no server has ever been observed to fragment one.
Constant Summary collapse
- OPCODES =
{ continuation: 0x0, text: 0x1, binary: 0x2, close: 0x8, ping: 0x9, pong: 0xA }.freeze
Class Method Summary collapse
-
.apply_mask(payload, key) ⇒ String
XOR-mask (or, symmetrically, unmask)
payloadagainst a 4-bytekey. -
.build(opcode:, payload: "".b, mask: true) ⇒ String
Build a complete frame.
-
.read(io) ⇒ Array(Boolean, Integer, String)
Read exactly one frame from
io(anything responding to#read(n)— aTCPSocket, anOpenSSL::SSL::SSLSocket, or aStringIOin tests).
Class Method Details
.apply_mask(payload, key) ⇒ String
XOR-mask (or, symmetrically, unmask) payload against a 4-byte key.
80 81 82 83 84 85 86 |
# File 'lib/bsdkrun/websocket_frame.rb', line 80 def apply_mask(payload, key) out = payload.b key_bytes = key.bytes bytes = out.bytes bytes.each_index { |i| bytes[i] ^= key_bytes[i % 4] } bytes.pack("C*") end |
.build(opcode:, payload: "".b, mask: true) ⇒ String
Build a complete frame.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/bsdkrun/websocket_frame.rb', line 34 def build(opcode:, payload: "".b, mask: true) payload = payload.to_s.dup.force_encoding(Encoding::BINARY) code = OPCODES.fetch(opcode) { raise ArgumentError, "unknown opcode #{opcode.inspect}" } frame = +"".b frame << [0x80 | code].pack("C") # FIN=1, RSV1-3=0 frame << length_bytes(payload.bytesize, mask) if mask key = Random.bytes(4) frame << key frame << apply_mask(payload, key) else frame << payload end frame end |
.read(io) ⇒ Array(Boolean, Integer, String)
Read exactly one frame from io (anything responding to #read(n) —
a TCPSocket, an OpenSSL::SSL::SSLSocket, or a StringIO in tests).
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/bsdkrun/websocket_frame.rb', line 59 def read(io) b0, b1 = read_exact(io, 2).unpack("C2") fin = (b0 & 0x80) != 0 opcode = b0 & 0x0F masked = (b1 & 0x80) != 0 len = b1 & 0x7F len = read_exact(io, 2).unpack1("n") if len == 126 len = read_exact(io, 8).unpack1("Q>") if len == 127 mask_key = masked ? read_exact(io, 4) : nil payload = len.positive? ? read_exact(io, len) : "".b payload = apply_mask(payload, mask_key) if masked [fin, opcode, payload] end |