Class: FTW::WebSocket::Rack

Inherits:
Object
  • Object
show all
Includes:
CRLF, Constants
Defined in:
lib/ftw/websocket/rack.rb

Overview

A websocket helper for Rack

An example with Sinatra:

get "/websocket/echo" do
  ws = FTW::WebSocket::Rack.new(env)
  stream(:keep_open) do |out|
    ws.each do |payload|
      # 'payload' is the text payload of a single websocket message
      # publish it back to the client
      ws.publish(payload)
    end
  end
  ws.rack_response
end

Constant Summary

Constants included from CRLF

CRLF::CRLF

Constants included from Constants

Constants::OPCODE_BINARY, Constants::OPCODE_CLOSE, Constants::OPCODE_CONTINUATION, Constants::OPCODE_PING, Constants::OPCODE_PONG, Constants::OPCODE_TEXT, Constants::WEBSOCKET_ACCEPT_UUID

Instance Method Summary collapse

Constructor Details

#initialize(rack_env) ⇒ Rack

Create a new websocket rack helper… thing.

Parameters:

  • rack_env

    the ‘env’ bit given to your Rack application



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/ftw/websocket/rack.rb', line 31

def initialize(rack_env)
  @env = rack_env
  @handshake_errors = []

  # RFC6455 section 4.2.1 bullet 3
  expect_equal("websocket", @env["HTTP_UPGRADE"],
               "The 'Upgrade' header must be set to 'websocket'")
  # RFC6455 section 4.2.1 bullet 4
  # Firefox uses a multivalued 'Connection' header, that appears like this:
  #   Connection: keep-alive, Upgrade
  # So we have to split this multivalue field. 
  expect_equal(true,
               @env["HTTP_CONNECTION"].split(/, +/).include?("Upgrade"),
               "The 'Connection' header must be set to 'Upgrade'")
  # RFC6455 section 4.2.1 bullet 6
  expect_equal("13", @env["HTTP_SEC_WEBSOCKET_VERSION"],
               "Sec-WebSocket-Version must be set to 13")

  # RFC6455 section 4.2.1 bullet 5
  @key = @env["HTTP_SEC_WEBSOCKET_KEY"] 

  @parser = FTW::WebSocket::Parser.new
end

Instance Method Details

#eachObject

Enumerate each websocket payload (message).

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

Example:

ws.each do |payload|
  puts "Received: #{payload}"
end


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/ftw/websocket/rack.rb', line 106

def each
  connection = @env["ftw.connection"]
  # There seems to be a bug in http_parser.rb where websocket responses
  # lead with a newline for some reason.  It's like the header terminator
  # CRLF still has the LF character left in the buffer. Work around it.
  data = connection.read
  if data[0] == "\n"
    connection.pushback(data[1..-1])
  else
    connection.pushback(data)
  end

  while true
    begin
      data = connection.read(16384)
    rescue EOFError
      # connection shutdown, close up.
      break
    end

    @parser.feed(data) do |payload|
      yield payload if !payload.nil?
    end
  end
end

#publish(message) ⇒ Object

Publish a message over this websocket.

Parameters:

  • message

    Publish a string message to the websocket.



135
136
137
138
# File 'lib/ftw/websocket/rack.rb', line 135

def publish(message)
  writer = FTW::WebSocket::Writer.singleton
  writer.write_text(@env["ftw.connection"], message)
end

#rack_responsenumber, ...

Get the response Rack is expecting.

If this was a valid websocket request, it will return a response that completes the HTTP portion of the websocket handshake.

If this was an invalid websocket request, it will return a 400 status code and descriptions of what failed in the body of the response.

Returns:

  • (number, hash, body)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ftw/websocket/rack.rb', line 77

def rack_response
  if valid?
    # Return the status, headers, body that is expected.
    sec_accept = @key + WEBSOCKET_ACCEPT_UUID
    sec_accept_hash = Digest::SHA1.base64digest(sec_accept)

    headers = {
      "Upgrade" => "websocket",
      "Connection" => "Upgrade",
      "Sec-WebSocket-Accept" => sec_accept_hash
    }
    # See RFC6455 section 4.2.2
    return 101, headers, nil
  else
    # Invalid request, tell the client why.
    return 400, { "Content-Type" => "text/plain" },
      @handshake_errors.map { |m| "#{m}#{CRLF}" }
  end
end

#valid?Boolean

Is this a valid handshake?

Returns:

  • (Boolean)


63
64
65
# File 'lib/ftw/websocket/rack.rb', line 63

def valid?
  return @handshake_errors.empty?
end