File: rack.rb

package info (click to toggle)
ruby-ftw 0.0.49-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 548 kB
  • sloc: ruby: 1,922; makefile: 5
file content (141 lines) | stat: -rw-r--r-- 4,187 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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
131
132
133
134
135
136
137
138
139
140
141
require "ftw/namespace"
require "ftw/websocket/parser"
require "ftw/crlf"
require "base64" # stdlib 
require "digest/sha1" # stdlib

# 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
class FTW::WebSocket::Rack
  include FTW::WebSocket::Constants
  include FTW::CRLF

  private

  # Create a new websocket rack helper... thing.
  #
  # @param rack_env the 'env' bit given to your Rack application
  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 # def initialize

  # Test values for equality. This is used in handshake tests.
  def expect_equal(expected, actual, message)
    if expected != actual
      @handshake_errors << message
    end
  end # def expected

  # Is this a valid handshake?
  def valid?
    return @handshake_errors.empty?
  end # def valid?

  # 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.
  #
  # @return [number, hash, body]
  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 # def rack_response

  # 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
  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 # def each

  # Publish a message over this websocket.
  #
  # @param message Publish a string message to the websocket.
  def publish(message)
    writer = FTW::WebSocket::Writer.singleton
    writer.write_text(@env["ftw.connection"], message)
  end # def publish

  public(:initialize, :valid?, :rack_response, :each, :publish)
end # class FTW::WebSocket::Rack