Module: Cybertrain::WebSocket::Frame

Defined in:
cybertrain/websocket/frame.rb

Overview

Frame encoding and decoding (RFC 6455 section 5).

Defined Under Namespace

Classes: Assembler, Header

Class Method Summary collapse

Class Method Details

.accept_key(key) ⇒ String

The Sec-WebSocket-Accept value for a Sec-WebSocket-Key.

Parameters:

  • key (String)

Returns:

  • (String)


188
189
190
# File 'cybertrain/websocket/frame.rb', line 188

def self.accept_key(key)
  Base64.strict_encode64(Digest::SHA1.digest(key + GUID))
end

.check_header(header, assembling, from_server) ⇒ nil

Raises ProtocolError for a header a conforming peer would not send.

Parameters:

  • header (Header)
  • assembling (Boolean) —

    a fragmented message is in progress

  • from_server (Boolean) —

    the frame arrived at a client

Returns:

  • (nil)

Raises:



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'cybertrain/websocket/frame.rb', line 302

def self.check_header(header, assembling, from_server)
  op = header.frame_opcode
  if from_server && header.frame_masked
    raise ProtocolError, "masked frame from the server"
  end
  if !from_server && !header.frame_masked
    raise ProtocolError, "unmasked frame from the client"
  end
  raise ProtocolError, "reserved bits set" if header.frame_rsv != 0
  known = op == OP_CONTINUATION || op == OP_TEXT || op == OP_BINARY ||
          op == OP_CLOSE || op == OP_PING || op == OP_PONG
  raise ProtocolError, "unknown opcode #{op}" unless known
  if op >= 8
    raise ProtocolError, "control frame too long" if header.frame_length > 125
    raise ProtocolError, "fragmented control frame" unless header.frame_fin
  elsif op == OP_CONTINUATION
    raise ProtocolError, "continuation without a message" unless assembling
  elsif assembling
    raise ProtocolError, "new data frame inside a fragmented message"
  end
  nil
end

.close_code(payload) ⇒ Integer

Returns the code; 1005 when the payload is empty.

Parameters:

  • payload (String)

Returns:

  • (Integer) —

    the code; 1005 when the payload is empty

Raises:

  • (ProtocolError) —

    for a 1-byte payload, or a code a peer may not send (RFC 6455 7.4: below 1000, the reserved 1004, 1005, 1006 and 1015, the unassigned 1015..2999, or above 4999)



348
349
350
351
352
353
354
355
356
357
358
359
# File 'cybertrain/websocket/frame.rb', line 348

def self.close_code(payload)
  n = payload.bytesize
  return 1005 if n == 0
  raise ProtocolError, "close payload of 1 byte" if n == 1

  code = (payload.getbyte(0) << 8) | payload.getbyte(1)
  valid = (code >= 1000 && code <= 1003) || (code >= 1007 && code <= 1014) ||
          (code >= 3000 && code <= 4999)
  raise ProtocolError, "invalid close code #{code}" unless valid

  code
end

.close_payload(code, reason) ⇒ String

Returns 2-byte big-endian code followed by the reason.

Parameters:

  • code (Integer)
  • reason (String)

Returns:

  • (String) —

    2-byte big-endian code followed by the reason



329
330
331
332
333
334
335
336
337
338
339
340
# File 'cybertrain/websocket/frame.rb', line 329

def self.close_payload(code, reason)
  r = reason.b
  out = ("\0" * (2 + r.bytesize)).b
  out.setbyte(0, (code >> 8) & 0xff)
  out.setbyte(1, code & 0xff)
  i = 0
  while i < r.bytesize
    out.setbyte(2 + i, r.getbyte(i))
    i += 1
  end
  out
end

.close_reason(payload) ⇒ String

The reason: bytes after the code, at most 123, without control characters (bytes 0..31 and 127), since it comes from the peer.

Parameters:

  • payload (String)

Returns:

  • (String)


366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'cybertrain/websocket/frame.rb', line 366

def self.close_reason(payload)
  n = payload.bytesize
  return "" if n <= 2
  n = 125 if n > 125
  out = ("\0" * (n - 2)).b
  used = 0
  i = 2
  while i < n
    b = payload.getbyte(i)
    if b > 31 && b != 127
      out.setbyte(used, b)
      used += 1
    end
    i += 1
  end
  out.byteslice(0, used)
end

.encode(opcode, payload, mask_key) ⇒ String

One FIN frame. A 4-byte mask_key masks the payload; “” does not.

Parameters:

  • opcode (Integer)
  • payload (String)
  • mask_key (String) —

    4 bytes, or “”

Returns:

  • (String) —

    binary



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'cybertrain/websocket/frame.rb', line 198

def self.encode(opcode, payload, mask_key)
  data = payload.b
  n = data.bytesize
  masked = mask_key.bytesize == 4
  ext = n <= 125 ? 0 : (n <= 65535 ? 2 : 8)
  head = 2 + ext + (masked ? 4 : 0)
  out = ("\0" * (head + n)).b
  out.setbyte(0, 0x80 | opcode)
  mask_bit = masked ? 0x80 : 0
  if ext == 0
    out.setbyte(1, mask_bit | n)
  elsif ext == 2
    out.setbyte(1, mask_bit | 126)
    out.setbyte(2, (n >> 8) & 0xff)
    out.setbyte(3, n & 0xff)
  else
    out.setbyte(1, mask_bit | 127)
    i = 0
    while i < 8
      out.setbyte(2 + i, (n >> (8 * (7 - i))) & 0xff)
      i += 1
    end
  end
  pos = 2 + ext
  if masked
    i = 0
    while i < 4
      out.setbyte(pos + i, mask_key.getbyte(i))
      i += 1
    end
    pos += 4
    i = 0
    while i < n
      out.setbyte(pos + i, data.getbyte(i) ^ mask_key.getbyte(i % 4))
      i += 1
    end
  else
    i = 0
    while i < n
      out.setbyte(pos + i, data.getbyte(i))
      i += 1
    end
  end
  out
end

.parse_header(bytes) ⇒ Header

Reads a header from the start of bytes. When there are too few bytes the Header has frame_header_size 0 (never nil).

Parameters:

  • bytes (String)

Returns:

Raises:



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
# File 'cybertrain/websocket/frame.rb', line 250

def self.parse_header(bytes)
  have = bytes.bytesize
  return Header.new(false, 0, 0, false, 0, 0, "") if have < 2
  b0 = bytes.getbyte(0)
  b1 = bytes.getbyte(1)
  masked = (b1 & 0x80) != 0
  len7 = b1 & 0x7f
  ext = len7 == 126 ? 2 : (len7 == 127 ? 8 : 0)
  if ext == 8 && have > 2 && (bytes.getbyte(2) & 0x80) != 0
    raise ProtocolError, "64-bit frame length with the top bit set"
  end
  size = 2 + ext + (masked ? 4 : 0)
  return Header.new(false, 0, 0, false, 0, 0, "") if have < size
  length = len7
  if ext > 0
    length = 0
    i = 0
    while i < ext
      length = (length << 8) | bytes.getbyte(2 + i)
      i += 1
    end
  end
  key = masked ? bytes.byteslice(2 + ext, 4) : ""
  Header.new((b0 & 0x80) != 0, (b0 >> 4) & 0x7, b0 & 0x0f, masked, length, size, key)
end

.unmask(payload, mask_key) ⇒ String

XOR with the 4-byte key; “” returns the payload unchanged.

Parameters:

  • payload (String)
  • mask_key (String)

Returns:

  • (String) —

    binary



281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'cybertrain/websocket/frame.rb', line 281

def self.unmask(payload, mask_key)
  n = payload.bytesize
  out = ("\0" * n).b
  klen = mask_key.bytesize
  i = 0
  while i < n
    b = payload.getbyte(i)
    b ^= mask_key.getbyte(i % klen) if klen == 4
    out.setbyte(i, b)
    i += 1
  end
  out
end