Module: Hyperion::WebSocket::RubyFrame

Defined in:
lib/hyperion/websocket/frame.rb

Overview

Pure-Ruby fallback used when the C ext is missing. Same public surface as ‘CFrame` so the Parser / Builder façades above don’t need to branch on ‘NATIVE_AVAILABLE` per call. Performance is ~5–10× worse on XOR — fine for a safety net, fine for JRuby interop, NOT recommended for the production hot path.

Class Method Summary collapse

Class Method Details

.build(opcode, payload, fin: true, mask: false, mask_key: nil, rsv1: false) ⇒ Object

Raises:

  • (ArgumentError)


305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/hyperion/websocket/frame.rb', line 305

def build(opcode, payload, fin: true, mask: false, mask_key: nil, rsv1: false)
  raise ArgumentError, "unknown opcode 0x#{opcode.to_s(16)}" unless [0x0, 0x1, 0x2, 0x8, 0x9,
                                                                     0xA].include?(opcode)

  payload_len = payload.bytesize

  if opcode >= 0x8
    raise ArgumentError, 'control frame must have fin=true' unless fin
    raise ArgumentError, 'control frame payload exceeds 125 bytes' if payload_len > 125
    raise ArgumentError, 'control frame must not have rsv1=true' if rsv1
  end

  if mask
    raise ArgumentError, 'mask: true requires a 4-byte mask_key' if mask_key.nil?
    raise ArgumentError, 'mask_key must be 4 bytes' if mask_key.bytesize != 4
  end

  out = String.new(encoding: Encoding::BINARY)
  out << ((fin ? 0x80 : 0x00) | (rsv1 ? 0x40 : 0x00) | (opcode & 0x0F)).chr
  mask_bit = mask ? 0x80 : 0x00

  if payload_len < 126
    out << (mask_bit | payload_len).chr
  elsif payload_len <= 0xFFFF
    out << (mask_bit | 126).chr
    out << ((payload_len >> 8) & 0xFF).chr
    out << (payload_len & 0xFF).chr
  else
    out << (mask_bit | 127).chr
    8.times { |i| out << ((payload_len >> ((7 - i) * 8)) & 0xFF).chr }
  end

  if mask
    out << mask_key.b
    out << unmask(payload.b, mask_key) # XOR is symmetric
  else
    out << payload.b
  end

  out
end

.parse(buf, offset = 0) ⇒ Object



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/hyperion/websocket/frame.rb', line 236

def parse(buf, offset = 0)
  return :incomplete if offset > buf.bytesize

  avail = buf.bytesize - offset
  return :incomplete if avail < 2

  b0 = buf.getbyte(offset)
  b1 = buf.getbyte(offset + 1)

  fin    = (b0 & 0x80) != 0
  rsv1   = (b0 & 0x40) != 0
  rsv2   = (b0 & 0x20) != 0
  rsv3   = (b0 & 0x10) != 0
  opcode = b0 & 0x0F
  masked = (b1 & 0x80) != 0
  len7   = b1 & 0x7F

  # RFC 7692 §6: RSV1 is the permessage-deflate marker. Allow it
  # through in the parse tuple; the Connection wrapper rejects
  # RSV1 when no extension was negotiated. RSV2/RSV3 are reserved
  # with no defined semantics → reject.
  return :error if rsv2 || rsv3

  return :error unless [0x0, 0x1, 0x2, 0x8, 0x9, 0xA].include?(opcode)

  if opcode >= 0x8
    return :error unless fin
    return :error if len7 > 125
    # RFC 7692 §6.1 — control frames MUST NOT be compressed.
    return :error if rsv1
  end

  header_len = 2
  payload_len =
    case len7
    when 0..125
      len7
    when 126
      return :incomplete if avail < header_len + 2

      v = (buf.getbyte(offset + 2) << 8) | buf.getbyte(offset + 3)
      header_len += 2
      v
    else
      return :incomplete if avail < header_len + 8

      return :error if (buf.getbyte(offset + 2) & 0x80) != 0

      v = 0
      8.times { |i| v = (v << 8) | buf.getbyte(offset + 2 + i) }
      header_len += 8
      v
    end

  mask_key = nil
  if masked
    return :incomplete if avail < header_len + 4

    mask_key = buf.byteslice(offset + header_len, 4)
    header_len += 4
  end

  payload_offset = offset + header_len
  frame_total_len = header_len + payload_len
  return :incomplete if avail < frame_total_len

  [fin, opcode, payload_len, masked, mask_key, payload_offset, frame_total_len, rsv1]
end

.unmask(payload, key) ⇒ Object

Raises:

  • (ArgumentError)


224
225
226
227
228
229
230
231
232
233
234
# File 'lib/hyperion/websocket/frame.rb', line 224

def unmask(payload, key)
  raise ArgumentError, 'mask_key must be 4 bytes' if key.bytesize != 4

  out = String.new(capacity: payload.bytesize, encoding: Encoding::BINARY)
  bytes  = payload.bytes
  kbytes = key.bytes
  bytes.each_with_index do |b, i|
    out << (b ^ kbytes[i & 0x3]).chr
  end
  out
end