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
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
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
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
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 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 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 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
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 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