class FTW::WebSocket::Parser

This class implements a parser 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 ...                |
+---------------------------------------------------------------+

Example use:

socket = FTW::Connection.new("example.com:80")
parser = FTW::WebSocket::Parser.new
# ... do HTTP Upgrade request to websockets
loop do
  data = socket.sysread(4096)
  payload = parser.feed(data)
  if payload
    # We got a full websocket frame, print the payload.
    p :payload => payload
  end
end

Constants

STATES

States are based on the minimal unit of 'byte'

Public Class Methods

new() click to toggle source

A new WebSocket protocol parser.

# File lib/ftw/websocket/parser.rb, line 49
def initialize
  @logger = Cabin::Channel.get
  @opcode = 0
  @masking_key = ""
  @flag_final_payload = 0
  @flag_mask = 0

  transition(:flags_and_opcode, 1)
  @buffer = ""
  @buffer.force_encoding("BINARY")
end

Public Instance Methods

feed(data) { |value| ... } click to toggle source

Feed data to this parser.

Currently, it will return the raw payload of websocket messages. Otherwise, it returns nil if no complete message has yet been consumed.

@param [String] the string data to feed into the parser. @return [String, nil] the websocket message payload, if any, nil otherwise.

# File lib/ftw/websocket/parser.rb, line 77
def feed(data)
  @buffer << data
  while have?(@need)
    value = send(@state)
    # Return if our state yields a value.
    yield value if !value.nil? and block_given?
  end
  return nil
end

Private Instance Methods

extended_payload_length() click to toggle source

State: payload_length This is the 'extended payload length' with support for both 16 and 64 bit lengths. See: tools.ietf.org/html/rfc6455#section-5.2

# File lib/ftw/websocket/parser.rb, line 159
def extended_payload_length
  #     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  |
  #    +-------------------------------+-------------------------------+
  data = get
  case @need
    when 2
      @payload_length = data.unpack("S>").first
    when 8
      @payload_length = data.unpack("Q>").first
    else
      raise "Unknown payload_length byte length '#{@need}'"
  end

  if @masked
    # Read the mask next if there is one.
    transition(:mask, 4)
  else
    # Otherwise, next is the payload
    transition(:payload, @payload_length)
  end

  # This state yields no output.
  return nil
end
flags_and_opcode() click to toggle source

State: Flags (fin, etc) and Opcode. See: tools.ietf.org/html/rfc6455#section-5.3

# File lib/ftw/websocket/parser.rb, line 107
def flags_and_opcode
  #     0
  #     0 1 2 3 4 5 6 7
  #    +-+-+-+-+-------
  #    |F|R|R|R| opcode
  #    |I|S|S|S|  (4)
  #    |N|V|V|V|
  #    | |1|2|3|
  byte = get(@need).bytes.first
  @opcode = byte & 0xF # last 4 bites
  @fin = (byte & 0x80 == 0x80)# first bit

  #p :byte => byte, :bits => byte.to_s(2), :opcode => @opcode, :fin => @fin
  # mask_and_payload_length has a minimum length
  # of 1 byte, so start there.
  transition(:mask_and_payload_init, 1)

  # This state yields no output.
  return nil
end
get(length=nil) click to toggle source

Get 'length' string from the buffer.

# File lib/ftw/websocket/parser.rb, line 93
def get(length=nil)
  length = @need if length.nil?
  data = @buffer[0 ... length]
  @buffer = @buffer[length .. -1]
  return data
end
have?(length) click to toggle source

Do we have at least 'length' bytes in the buffer?

# File lib/ftw/websocket/parser.rb, line 88
def have?(length)
  return length <= @buffer.bytesize 
end
mask() click to toggle source

State: mask Read the mask key

# File lib/ftw/websocket/parser.rb, line 195
def mask
  #    + - - - - - - - - - - - - - - - +-------------------------------+
  #    |                               |Masking-key, if MASK set to 1  |
  #    +-------------------------------+-------------------------------+
  #    | Masking-key (continued)       |          Payload Data         |
  #    +-------------------------------- - - - - - - - - - - - - - - - +
  @mask = get(@need)
  transition(:payload, @payload_length)
  return nil
end
mask_and_payload_init() click to toggle source

State: mask_and_payload_init See: tools.ietf.org/html/rfc6455#section-5.2

# File lib/ftw/websocket/parser.rb, line 130
def mask_and_payload_init
  byte = get(@need).bytes.first
  @masked = (byte & 0x80) == 0x80 # first bit (msb)
  @payload_length = byte & 0x7F # remaining bits are the length
  case @payload_length
    when 126 # 2 byte, unsigned value is the payload length
      transition(:extended_payload_length, 2)
    when 127 # 8 byte, unsigned value is the payload length
      transition(:extended_payload_length, 8)
    else
      # If there is a mask, read that next
      if @masked
        transition(:mask, 4)
      else
        # Otherwise, the payload is next.
        # Keep the current payload length, a 7 bit value.
        # Go to read the payload
        transition(:payload, @payload_length)
      end
  end # case @payload_length

  # This state yields no output.
  return nil
end
need(length) click to toggle source

Set the minimum number of bytes we need in the buffer for the next read.

# File lib/ftw/websocket/parser.rb, line 101
def need(length)
  @need = length
end
payload() click to toggle source

State: payload Read the full payload and return it. See: tools.ietf.org/html/rfc6455#section-5.3

# File lib/ftw/websocket/parser.rb, line 209
def payload
  # TODO(sissel): Handle massive payload lengths without exceeding memory.
  # Perhaps if the payload is large (say, larger than 500KB by default),
  # instead of returning the whole thing, simply return an Enumerable that
  # yields chunks of the payload. There's no reason to buffer the entire
  # thing. Have the consumer of this library make that decision.
  data = get(@need)
  transition(:flags_and_opcode, 1)
  if @masked
    return unmask(data, @mask)
  else
    return data
  end
end
transition(state, next_length) click to toggle source

Transition to a specified state and set the next required read length.

# File lib/ftw/websocket/parser.rb, line 62
def transition(state, next_length)
  @logger.debug("Transitioning", :transition => state, :nextlen => next_length)
  @state = state
  # TODO(sissel): Assert this self.respond_to?(state)
  # TODO(sissel): Assert next_length is a number
  need(next_length)
end
unmask(message, key) click to toggle source

Unmask the message using the key.

For implementation specification, see tools.ietf.org/html/rfc6455#section-5.3

# File lib/ftw/websocket/parser.rb, line 228
def unmask(message, key)
  masked = []
  mask_bytes = key.unpack("C4")
  i = 0
  message.each_byte do |byte|
    masked << (byte ^ mask_bytes[i % 4])
    i += 1
  end
  #p :unmasked => masked.pack("C*"), :original => message
  return masked.pack("C*")
end