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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
require "http/parser"
require "uri"
module EventMachine
module WebSocket
# Resposible for creating the server handshake response
class Handshake
include EM::Deferrable
attr_reader :parser, :protocol_version
# Unfortunately drafts 75 & 76 require knowledge of whether the
# connection is being terminated as ws/wss in order to generate the
# correct handshake response
def initialize(secure)
@parser = Http::Parser.new
@secure = secure
@parser.on_headers_complete = proc { |headers|
@headers = Hash[headers.map { |k,v| [k.downcase, v] }]
}
end
def receive_data(data)
@parser << data
if defined? @headers
process(@headers, @parser.upgrade_data)
end
rescue HTTP::Parser::Error => e
fail(HandshakeError.new("Invalid HTTP header: #{e.message}"))
end
# Returns the WebSocket upgrade headers as a hash.
#
# Keys are strings, unmodified from the request.
#
def headers
@parser.headers
end
# The same as headers, except that the hash keys are downcased
#
def headers_downcased
@headers
end
# Returns the request path (excluding any query params)
#
def path
@path
end
# Returns the query params as a string foo=bar&baz=...
def query_string
@query_string
end
def query
Hash[query_string.split('&').map { |c| c.split('=', 2) }]
end
# Returns the WebSocket origin header if provided
#
def origin
@headers["origin"] || @headers["sec-websocket-origin"] || nil
end
def secure?
@secure
end
private
def process(headers, remains)
unless @parser.http_method == "GET"
raise HandshakeError, "Must be GET request"
end
# Validate request path
#
# According to http://tools.ietf.org/search/rfc2616#section-5.1.2, an
# invalid Request-URI should result in a 400 status code, but
# HandshakeError's currently result in a WebSocket abort. It's not
# clear which should take precedence, but an abort will do just fine.
begin
uri = URI.parse(@parser.request_url)
@path = uri.path
@query_string = uri.query || ""
rescue URI::InvalidURIError
raise HandshakeError, "Invalid request URI: #{@parser.request_url}"
end
# Validate Upgrade
unless @parser.upgrade?
raise HandshakeError, "Not an upgrade request"
end
upgrade = @headers['upgrade']
unless upgrade.kind_of?(String) && upgrade.downcase == 'websocket'
raise HandshakeError, "Invalid upgrade header: #{upgrade.inspect}"
end
# Determine version heuristically
version = if @headers['sec-websocket-version']
# Used from drafts 04 onwards
@headers['sec-websocket-version'].to_i
elsif @headers['sec-websocket-draft']
# Used in drafts 01 - 03
@headers['sec-websocket-draft'].to_i
elsif @headers['sec-websocket-key1']
76
else
75
end
# Additional handling of bytes after the header if required
case version
when 75
if !remains.empty?
raise HandshakeError, "Extra bytes after header"
end
when 76, 1..3
if remains.length < 8
# The whole third-key has not been received yet.
return nil
elsif remains.length > 8
raise HandshakeError, "Extra bytes after third key"
end
@headers['third-key'] = remains
end
handshake_klass = case version
when 75
Handshake75
when 76, 1..3
Handshake76
when 5, 6, 7, 8, 13
Handshake04
else
# According to spec should abort the connection
raise HandshakeError, "Protocol version #{version} not supported"
end
upgrade_response = handshake_klass.handshake(@headers, @parser.request_url, @secure)
handler_klass = Handler.klass_factory(version)
@protocol_version = version
succeed(upgrade_response, handler_klass)
rescue HandshakeError => e
fail(e)
end
end
end
end
|