Module: Dommy::Rack::WebSocketFrame
- Defined in:
- lib/dommy/rack/web_socket_frame.rb
Overview
RFC 6455 frame codec — the byte layout only, no I/O lifecycle. Encodes client frames (masked, per §5.3) and decodes server frames from a blocking IO. WebSocketTransport owns the socket, threads, and what each opcode means; this module owns how a frame is laid out on the wire.
Constant Summary collapse
- TEXT =
0x1- BINARY =
0x2- CLOSE =
0x8- PING =
0x9- PONG =
0xA
Class Method Summary collapse
-
.client_frame(opcode, payload) ⇒ Object
A single client frame: FIN + opcode, then the length with the mask bit set (clients MUST mask; RFC 6455 §5.3), the 4-byte key, and the masked payload.
-
.close_payload(code, reason) ⇒ Object
The body of a close frame: 2-byte status code + UTF-8 reason.
-
.parse_close(payload) ⇒ Object
[code, reason] from a close frame's payload; an empty payload means "no status received" (1005).
-
.read(io) ⇒ Object
Read one frame from
io. - .read_exact(io, n) ⇒ Object
-
.xor_mask(payload, key) ⇒ Object
XOR (un)masking — its own inverse, so one helper serves both sides.
Class Method Details
.client_frame(opcode, payload) ⇒ Object
A single client frame: FIN + opcode, then the length with the mask bit set (clients MUST mask; RFC 6455 §5.3), the 4-byte key, and the masked payload.
37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/dommy/rack/web_socket_frame.rb', line 37 def client_frame(opcode, payload) header = [0x80 | opcode].pack("C") length = payload.bytesize header << if length < 126 then [0x80 | length].pack("C") elsif length < 65_536 then [0x80 | 126, length].pack("Cn") else [0x80 | 127, length].pack("CQ>") end key = SecureRandom.bytes(4) header + key + xor_mask(payload, key) end |
.close_payload(code, reason) ⇒ Object
The body of a close frame: 2-byte status code + UTF-8 reason.
50 51 52 |
# File 'lib/dommy/rack/web_socket_frame.rb', line 50 def close_payload(code, reason) [code].pack("n") + reason.to_s.b end |
.parse_close(payload) ⇒ Object
[code, reason] from a close frame's payload; an empty payload means "no status received" (1005).
56 57 58 59 60 |
# File 'lib/dommy/rack/web_socket_frame.rb', line 56 def parse_close(payload) code = payload.bytesize >= 2 ? payload[0, 2].unpack1("n") : 1005 reason = payload.byteslice(2..).to_s.force_encoding(Encoding::UTF_8) [code, reason] end |
.read(io) ⇒ Object
Read one frame from io. Returns [opcode, payload] with the payload
unmasked. Raises EOFError when the stream ends mid-frame.
22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/dommy/rack/web_socket_frame.rb', line 22 def read(io) b1, b2 = read_exact(io, 2).unpack("C2") opcode = b1 & 0x0f length = b2 & 0x7f length = read_exact(io, 2).unpack1("n") if length == 126 length = read_exact(io, 8).unpack1("Q>") if length == 127 mask = (b2 & 0x80).positive? ? read_exact(io, 4) : nil payload = length.zero? ? +"" : read_exact(io, length) payload = xor_mask(payload, mask) if mask [opcode, payload] end |
.read_exact(io, n) ⇒ Object
62 63 64 65 66 67 68 69 70 71 |
# File 'lib/dommy/rack/web_socket_frame.rb', line 62 def read_exact(io, n) data = +"" while data.bytesize < n chunk = io.read(n - data.bytesize) raise EOFError, "connection closed" if chunk.nil? data << chunk end data end |
.xor_mask(payload, key) ⇒ Object
XOR (un)masking — its own inverse, so one helper serves both sides.
74 75 76 77 78 79 |
# File 'lib/dommy/rack/web_socket_frame.rb', line 74 def xor_mask(payload, key) bytes = payload.bytes key_bytes = key.bytes bytes.each_index { |i| bytes[i] ^= key_bytes[i % 4] } bytes.pack("C*") end |