class FTW::WebSocket

WebSockets, RFC6455.

TODO(sissel): Find a comfortable way to make this websocket stuff both use HTTP::Connection for the HTTP handshake and also be usable from HTTP::Client TODO(sissel): Also consider SPDY and the kittens.

Constants

TEXTFRAME

The frame identifier for a 'text' frame

WEBSOCKET_ACCEPT_UUID

Search RFC6455 for this string and you will find its definitions. It is used in servers accepting websocket upgrades.

Public Class Methods

new(request) click to toggle source

Creates a new websocket and fills in the given http request with any necessary settings.

# File lib/ftw/websocket.rb, line 36
def initialize(request)
  @key_nonce = generate_key_nonce
  @request = request
  prepare(@request)
  @parser = FTW::WebSocket::Parser.new
  @messages = []
end

Public Instance Methods

connection=(connection) click to toggle source

Set the connection for this websocket. This is usually invoked by FTW::Agent after the websocket upgrade and handshake have been successful.

You probably don't call this yourself.

# File lib/ftw/websocket.rb, line 48
def connection=(connection)
  @connection = connection
end
each(&block) click to toggle source

Iterate over each WebSocket message. This method will run forever unless you break from it.

The text payload of each message will be yielded to the block.

# File lib/ftw/websocket.rb, line 121
def each(&block)
  while true
    block.call(receive)
  end
end
handshake_ok?(response) click to toggle source

Is this Response acceptable for our WebSocket Upgrade request?

# File lib/ftw/websocket.rb, line 102
def handshake_ok?(response)
  # See RFC6455 section 4.2.2
  return false unless response.status == 101 # "Switching Protocols"
  return false unless response.headers.get("upgrade").downcase == "websocket"
  return false unless response.headers.get("connection").downcase == "upgrade"

  # Now verify Sec-WebSocket-Accept. It should be the SHA-1 of the
  # Sec-WebSocket-Key (in base64) + WEBSOCKET_ACCEPT_UUID
  expected = @key_nonce + WEBSOCKET_ACCEPT_UUID
  expected_hash = Digest::SHA1.base64digest(expected)
  return false unless response.headers.get("Sec-WebSocket-Accept") == expected_hash

  return true
end
publish(message) click to toggle source

Publish a message text.

This will send a websocket text frame over the connection.

# File lib/ftw/websocket.rb, line 145
def publish(message)
  writer = FTW::WebSocket::Writer.singleton
  writer.write_text(@connection, message)
end
receive() click to toggle source

Receive a single payload

# File lib/ftw/websocket.rb, line 128
def receive
  @messages += network_consume if @messages.empty?
  @messages.shift
end

Private Instance Methods

generate_key_nonce() click to toggle source

Generate a websocket key nonce.

# File lib/ftw/websocket.rb, line 81
def generate_key_nonce
  # RFC6455 section 4.1 says:
  # ---
  # 7.   The request MUST include a header field with the name
  #      |Sec-WebSocket-Key|.  The value of this header field MUST be a
  #      nonce consisting of a randomly selected 16-byte value that has
  #      been base64-encoded (see Section 4 of [RFC4648]).  The nonce
  #      MUST be selected randomly for each connection.
  # ---
  #
  # It's not totally clear to me how cryptographically strong this random
  # nonce needs to be, and if it does not need to be strong and it would
  # benefit users who do not have ruby with openssl enabled, maybe just use
  # rand() to generate this string.
  #
  # Thus, generate a random 16 byte string and encode i with base64.
  # Array#pack("m") packs with base64 encoding.
  return Base64.strict_encode64(OpenSSL::Random.random_bytes(16))
end
network_consume() click to toggle source

Consume payloads from the network.

# File lib/ftw/websocket.rb, line 134
def network_consume
  payloads = []
  @parser.feed(@connection.read(16384)) do |payload|
    payloads << payload
  end
  return payloads
end
prepare(request) click to toggle source

Prepare the request. This sets any required headers and attributes as specified by RFC6455

# File lib/ftw/websocket.rb, line 54
def prepare(request)
  # RFC6455 section 4.1:
  #  "2.   The method of the request MUST be GET, and the HTTP version MUST
  #        be at least 1.1."
  request.method = "GET"
  request.version = 1.1

  # RFC6455 section 4.2.1 bullet 3
  request.headers.set("Upgrade", "websocket") 
  # RFC6455 section 4.2.1 bullet 4
  request.headers.set("Connection", "Upgrade") 
  # RFC6455 section 4.2.1 bullet 5
  request.headers.set("Sec-WebSocket-Key", @key_nonce)
  # RFC6455 section 4.2.1 bullet 6
  request.headers.set("Sec-WebSocket-Version", 13)
  # RFC6455 section 4.2.1 bullet 7 (optional)
  # The Origin header is optional for non-browser clients.
  #request.headers.set("Origin", ...)
  # RFC6455 section 4.2.1 bullet 8 (optional)
  #request.headers.set("Sec-Websocket-Protocol", ...)
  # RFC6455 section 4.2.1 bullet 9 (optional)
  #request.headers.set("Sec-Websocket-Extensions", ...)
  # RFC6455 section 4.2.1 bullet 10 (optional)
  # TODO(sissel): Any other headers like cookies, auth headers, are allowed.
end