class FTW::WebSocket::Writer

This class implements a writer for WebSocket messages over a stream.

Protocol diagram copied from RFC6455

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

Constants

VALID_MODES

A list of valid modes. Used to validate input in write_text.

In :server mode, payloads are not masked. In :client mode, payloads are masked. Masking is described in RFC6455.

Public Instance Methods

write_text(connection, text, mode=:server) click to toggle source

Write the given text in a websocket frame to the connection.

Valid 'mode' settings are :server or :client. If :client, the payload will be masked according to RFC6455 section 5.3: tools.ietf.org/html/rfc6455#section-5.3

# File lib/ftw/websocket/writer.rb, line 45
def write_text(connection, text, mode=:server)
  if !VALID_MODES.include?(mode)
    raise InvalidArgument.new("Invalid message mode: #{mode}, expected one of" \
                              "#{VALID_MODES.inspect}")
  end

  data = []
  pack = []

  # For now, assume single-fragment, text frames
  pack_opcode(data, pack, OPCODE_TEXT)
  pack_payload(data, pack, text, mode)
  connection.write(data.pack(pack.join("")))
end

Private Instance Methods

mask(message, key) click to toggle source

Implement masking as described by tools.ietf.org/html/rfc6455#section-5.3 Basically, we take a 4-byte random string and use it, round robin, to XOR every byte. Like so:

message[0] ^ key[0]
message[1] ^ key[1]
message[2] ^ key[2]
message[3] ^ key[3]
message[4] ^ key[0]
...
# File lib/ftw/websocket/writer.rb, line 93
def mask(message, key)
  masked = []
  mask_bytes = key.unpack("C4")
  i = 0
  message.each_byte do |byte|
    masked << (byte ^ mask_bytes[i % 4])
    i += 1
  end
  return masked.pack("C*")
end
pack_extended_length(data, pack, length) click to toggle source

Pack the extended length. 16 bits or 64 bits

# File lib/ftw/websocket/writer.rb, line 122
def pack_extended_length(data, pack, length)
  data << length
  if length >= (1 << 16)
    # For lengths >= 16 bits, pack 8 byte length
    pack << "Q>"
  else
    # For lengths < 16 bits, pack 2 byte length
    pack << "S>"
  end
end
pack_maskbit_and_length(data, pack, length, mode) click to toggle source

Pack the first part of the length (mask and 7-bit length)

# File lib/ftw/websocket/writer.rb, line 105
def pack_maskbit_and_length(data, pack, length, mode)
  # Pack mask + payload length
  maskbit = (mode == :client) ? (1 << 7) : 0
  if length >= 126
    if length < (1 << 16) # if less than 2^16, use 2 bytes
      lengthbits = 126
    else
      lengthbits = 127
    end
  else
    lengthbits = length
  end
  data << (maskbit | lengthbits)
  pack << "C"
end
pack_opcode(data, pack, opcode) click to toggle source

Pack the opcode and flags

Currently assumes 'fin' flag is set.

# File lib/ftw/websocket/writer.rb, line 63
def pack_opcode(data, pack, opcode)
  # Pack the first byte (fin + opcode)
  data << ((1 << 7) | opcode)
  pack << "C"
end
pack_payload(data, pack, text, mode) click to toggle source

Pack the payload.

# File lib/ftw/websocket/writer.rb, line 70
def pack_payload(data, pack, text, mode)
  pack_maskbit_and_length(data, pack, text.bytesize, mode)
  pack_extended_length(data, pack, text.bytesize) if text.bytesize >= 126
  if mode == :client
    mask_key = [rand(1 << 32)].pack("Q")
    pack_mask(data, pack, mask_key)
    data << mask(text, mask_key)
    pack << "A*"
  else
    data << text
    pack << "A*"
  end
end