libwebsocket 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +88 -0
- data/Rakefile +26 -0
- data/examples/eventmachine_server.rb +36 -0
- data/examples/plain_client.rb +59 -0
- data/examples/thin_server.rb +69 -0
- data/lib/libwebsocket.rb +17 -0
- data/lib/libwebsocket/cookie.rb +60 -0
- data/lib/libwebsocket/cookie/request.rb +48 -0
- data/lib/libwebsocket/cookie/response.rb +44 -0
- data/lib/libwebsocket/frame.rb +67 -0
- data/lib/libwebsocket/handshake.rb +28 -0
- data/lib/libwebsocket/handshake/client.rb +129 -0
- data/lib/libwebsocket/handshake/server.rb +114 -0
- data/lib/libwebsocket/message.rb +167 -0
- data/lib/libwebsocket/request.rb +288 -0
- data/lib/libwebsocket/response.rb +215 -0
- data/lib/libwebsocket/stateful.rb +24 -0
- data/lib/libwebsocket/url.rb +67 -0
- data/test/libwebsocket/cookie/request.rb +37 -0
- data/test/libwebsocket/cookie/response.rb +32 -0
- data/test/libwebsocket/handshake/test_client.rb +64 -0
- data/test/libwebsocket/handshake/test_server.rb +39 -0
- data/test/libwebsocket/test_cookie.rb +21 -0
- data/test/libwebsocket/test_frame.rb +65 -0
- data/test/libwebsocket/test_message.rb +16 -0
- data/test/libwebsocket/test_request_75.rb +145 -0
- data/test/libwebsocket/test_request_76.rb +122 -0
- data/test/libwebsocket/test_request_common.rb +26 -0
- data/test/libwebsocket/test_response_75.rb +80 -0
- data/test/libwebsocket/test_response_76.rb +115 -0
- data/test/libwebsocket/test_response_common.rb +17 -0
- data/test/libwebsocket/test_url.rb +49 -0
- data/test/test_helper.rb +4 -0
- metadata +116 -0
@@ -0,0 +1,288 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
# Construct or parse a WebSocket request.
|
3
|
+
class Request < Message
|
4
|
+
|
5
|
+
attr_accessor :cookies, :resource_name
|
6
|
+
|
7
|
+
# Parse a WebSocket request.
|
8
|
+
# @param [String] opts parse string
|
9
|
+
# @param [Hash] opts parse rack env hash
|
10
|
+
# @see Request#parse_rack_env
|
11
|
+
# @example Parser
|
12
|
+
# req = LibWebSocket::Request.new
|
13
|
+
# req.parse("GET /demo HTTP/1.1\x0d\x0a")
|
14
|
+
# req.parse("Upgrade: WebSocket\x0d\x0a")
|
15
|
+
# req.parse("Connection: Upgrade\x0d\x0a")
|
16
|
+
# req.parse("Host: example.com\x0d\x0a")
|
17
|
+
# req.parse("Origin: http://example.com\x0d\x0a")
|
18
|
+
# req.parse("Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8\x0d\x0a")
|
19
|
+
# req.parse("Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0\x0d\x0a")
|
20
|
+
# req.parse("\x0d\x0aTm[K T2u")
|
21
|
+
def parse(opts)
|
22
|
+
case opts
|
23
|
+
when String then super
|
24
|
+
when Hash then parse_rack_env(opts)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse a WebSocket request.
|
29
|
+
# @param [Hash] env parse rack env hash
|
30
|
+
# @example Parser
|
31
|
+
# req = LibWebSocket::Request.new
|
32
|
+
# req.parse(env)
|
33
|
+
def parse_rack_env(env)
|
34
|
+
method = env['REQUEST_METHOD']
|
35
|
+
unless method == 'GET'
|
36
|
+
self.error = 'Wrong request method'
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
|
40
|
+
self.resource_name = env['REQUEST_URI']
|
41
|
+
|
42
|
+
env.keys.select { |v| v =~ /\AHTTP\_/ }.each do |key|
|
43
|
+
self.field(key.gsub(/\AHTTP\_/, "").gsub('_','-'), env[key])
|
44
|
+
end
|
45
|
+
|
46
|
+
body = env['rack.input']
|
47
|
+
# Is compatible with rack.input spec?
|
48
|
+
unless body.respond_to?(:rewind) and body.respond_to?(:read)
|
49
|
+
self.error = "Invalid rack input"
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
body.rewind
|
54
|
+
@buffer = body.read
|
55
|
+
|
56
|
+
rv = self.parse_body
|
57
|
+
return unless rv
|
58
|
+
|
59
|
+
# Need more data
|
60
|
+
return rv unless rv != true
|
61
|
+
|
62
|
+
return self.done
|
63
|
+
end
|
64
|
+
|
65
|
+
# A shortcut for self.field('Upgrade')
|
66
|
+
def upgrade
|
67
|
+
self.field('Upgrade')
|
68
|
+
end
|
69
|
+
# A shortcut for self.field('Connection')
|
70
|
+
def connection
|
71
|
+
self.field('Connection')
|
72
|
+
end
|
73
|
+
|
74
|
+
# Draft 76 number 1 reader
|
75
|
+
def number1
|
76
|
+
self.number('number1','key1')
|
77
|
+
end
|
78
|
+
# Draft 76 number 1 writter
|
79
|
+
def number1=(val)
|
80
|
+
self.number('number1','key1',val)
|
81
|
+
end
|
82
|
+
# Draft 76 number 2 reader
|
83
|
+
def number2
|
84
|
+
self.number('number2','key2')
|
85
|
+
end
|
86
|
+
# Draft 76 number 2 writter
|
87
|
+
def number2=(val)
|
88
|
+
self.number('number2','key2',val)
|
89
|
+
end
|
90
|
+
# Draft 76 Sec-WebSocket-Key1 reader
|
91
|
+
def key1
|
92
|
+
self.key('key1')
|
93
|
+
end
|
94
|
+
# Draft 76 Sec-WebSocket-Key1 writter
|
95
|
+
def key1=(val)
|
96
|
+
self.key('key1',val)
|
97
|
+
end
|
98
|
+
# Draft 76 Sec-WebSocket-Key2 reader
|
99
|
+
def key2
|
100
|
+
self.key('key2')
|
101
|
+
end
|
102
|
+
# Draft 76 Sec-WebSocket-Key2 writter
|
103
|
+
def key2=(val)
|
104
|
+
self.key('key2',val)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Construct a WebSocket request.
|
108
|
+
# # Constructor
|
109
|
+
# my $req = Protocol::WebSocket::Request.new(
|
110
|
+
# host => 'example.com',
|
111
|
+
# resource_name => '/demo'
|
112
|
+
# );
|
113
|
+
# $req.to_s # GET /demo HTTP/1.1
|
114
|
+
# # Upgrade: WebSocket
|
115
|
+
# # Connection: Upgrade
|
116
|
+
# # Host: example.com
|
117
|
+
# # Origin: http://example.com
|
118
|
+
# # Sec-WebSocket-Key1: 32 0 3lD& 24+< i u4 8! -6/4
|
119
|
+
# # Sec-WebSocket-Key2: 2q 4 2 54 09064
|
120
|
+
# #
|
121
|
+
# # x#####
|
122
|
+
def to_s
|
123
|
+
string = ''
|
124
|
+
|
125
|
+
raise 'resource_name is required' unless self.resource_name
|
126
|
+
string += "GET " + self.resource_name + " HTTP/1.1\x0d\x0a"
|
127
|
+
|
128
|
+
string += "Upgrade: WebSocket\x0d\x0a"
|
129
|
+
string += "Connection: Upgrade\x0d\x0a"
|
130
|
+
|
131
|
+
raise 'Host is required' unless self.host
|
132
|
+
string += "Host: " + self.host + "\x0d\x0a"
|
133
|
+
|
134
|
+
origin = self.origin || 'http://' + self.host
|
135
|
+
string += "Origin: " + origin + "\x0d\x0a"
|
136
|
+
|
137
|
+
if self.version > 75
|
138
|
+
self.generate_keys
|
139
|
+
|
140
|
+
string += 'Sec-WebSocket-Protocol: ' + self.subprotocol + "\x0d\x0a" if self.subprotocol
|
141
|
+
|
142
|
+
string += 'Sec-WebSocket-Key1: ' + self.key1 + "\x0d\x0a"
|
143
|
+
string += 'Sec-WebSocket-Key2: ' + self.key2 + "\x0d\x0a"
|
144
|
+
|
145
|
+
string += 'Content-Length: ' + self.challenge.length.to_s + "\x0d\x0a"
|
146
|
+
else
|
147
|
+
string += 'WebSocket-Protocol: ' + self.subprotocol + "\x0d\x0a" if self.subprotocol
|
148
|
+
end
|
149
|
+
|
150
|
+
# TODO cookies
|
151
|
+
|
152
|
+
string += "\x0d\x0a"
|
153
|
+
|
154
|
+
string += self.challenge if self.version > 75
|
155
|
+
|
156
|
+
return string
|
157
|
+
end
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
def parse_first_line(line)
|
162
|
+
req, resource_name, http = line.split(' ')
|
163
|
+
|
164
|
+
unless req && resource_name && http
|
165
|
+
self.error = 'Wrong request line'
|
166
|
+
return
|
167
|
+
end
|
168
|
+
|
169
|
+
unless req == 'GET' && http == 'HTTP/1.1'
|
170
|
+
self.error = 'Wrong method or http version'
|
171
|
+
return
|
172
|
+
end
|
173
|
+
|
174
|
+
self.resource_name = resource_name
|
175
|
+
|
176
|
+
return self
|
177
|
+
end
|
178
|
+
|
179
|
+
def parse_body
|
180
|
+
if self.key1 && self.key2
|
181
|
+
return true if @buffer.length < 8
|
182
|
+
|
183
|
+
challenge = @buffer.slice!(0..7)
|
184
|
+
self.challenge = challenge
|
185
|
+
else
|
186
|
+
self.version = 75
|
187
|
+
end
|
188
|
+
|
189
|
+
if @buffer.length > 0
|
190
|
+
self.error = 'Leftovers'
|
191
|
+
return
|
192
|
+
end
|
193
|
+
|
194
|
+
return self if self.finalize
|
195
|
+
|
196
|
+
self.error = 'Not a valid request'
|
197
|
+
return
|
198
|
+
end
|
199
|
+
|
200
|
+
def key(name, value = nil)
|
201
|
+
unless value
|
202
|
+
if value = self.instance_variable_get("@#{name}")
|
203
|
+
self.field("Sec-WebSocket-" + name.capitalize, value)
|
204
|
+
end
|
205
|
+
|
206
|
+
return self.field("Sec-WebSocket-" + name.capitalize)
|
207
|
+
end
|
208
|
+
|
209
|
+
return self.field("Sec-WebSocket-" + name.capitalize, value)
|
210
|
+
|
211
|
+
return self
|
212
|
+
end
|
213
|
+
|
214
|
+
def generate_keys
|
215
|
+
unless self.key1
|
216
|
+
number, key = self.generate_key
|
217
|
+
self.number1 = number
|
218
|
+
self.key1 = key
|
219
|
+
end
|
220
|
+
|
221
|
+
unless self.key2
|
222
|
+
number, key = self.generate_key
|
223
|
+
self.number2 = number
|
224
|
+
self.key2 = key
|
225
|
+
end
|
226
|
+
|
227
|
+
self.challenge ||= self.generate_challenge
|
228
|
+
|
229
|
+
return self
|
230
|
+
end
|
231
|
+
|
232
|
+
NOISE_CHARS = ("\x21".."\x2f").to_a + ("\x3a".."\x7e").to_a # From spec
|
233
|
+
|
234
|
+
def generate_key
|
235
|
+
spaces = 1 + rand(12)
|
236
|
+
max = 4_294_967_295 / spaces
|
237
|
+
number = rand(max + 1)
|
238
|
+
key = (number * spaces).to_s
|
239
|
+
(1 + rand(12)).times do
|
240
|
+
char = NOISE_CHARS[rand(NOISE_CHARS.size)]
|
241
|
+
pos = rand(key.size + 1)
|
242
|
+
key[pos...pos] = char
|
243
|
+
end
|
244
|
+
spaces.times do
|
245
|
+
pos = 1 + rand(key.size - 1)
|
246
|
+
key[pos...pos] = " "
|
247
|
+
end
|
248
|
+
return [number, key]
|
249
|
+
end
|
250
|
+
|
251
|
+
def generate_challenge
|
252
|
+
challenge = ''
|
253
|
+
8.times do
|
254
|
+
challenge += rand(256).chr
|
255
|
+
end
|
256
|
+
|
257
|
+
return challenge
|
258
|
+
end
|
259
|
+
|
260
|
+
def finalize
|
261
|
+
return unless self.upgrade && self.upgrade == 'WebSocket'
|
262
|
+
return unless self.connection && self.connection == 'Upgrade'
|
263
|
+
|
264
|
+
origin = self.field('Origin')
|
265
|
+
return unless origin
|
266
|
+
self.origin = origin
|
267
|
+
|
268
|
+
host = self.field('Host')
|
269
|
+
return unless host
|
270
|
+
self.host = host
|
271
|
+
|
272
|
+
subprotocol = self.field('Sec-WebSocket-Protocol') || self.field('WebSocket-Protocol')
|
273
|
+
self.subprotocol = subprotocol if subprotocol
|
274
|
+
|
275
|
+
cookie = self.build_cookie
|
276
|
+
if cookies = cookie.parse(self.fields['cookie'])
|
277
|
+
self.cookies = cookies
|
278
|
+
end
|
279
|
+
|
280
|
+
return self
|
281
|
+
end
|
282
|
+
|
283
|
+
def build_cookie
|
284
|
+
Cookie::Request.new
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
module LibWebSocket
|
2
|
+
#Construct or parse a WebSocket response.
|
3
|
+
class Response < Message
|
4
|
+
|
5
|
+
attr_accessor :location, :secure, :resource_name, :cookies, :key1, :key2
|
6
|
+
|
7
|
+
# Parse a WebSocket response.
|
8
|
+
# @see Message#parse
|
9
|
+
# @example Parser
|
10
|
+
# res = LibWebSocket::Response.new;
|
11
|
+
# res.parse("HTTP/1.1 101 WebSocket Protocol Handshake\x0d\x0a")
|
12
|
+
# res.parse("Upgrade: WebSocket\x0d\x0a")
|
13
|
+
# res.parse("Connection: Upgrade\x0d\x0a")
|
14
|
+
# res.parse("Sec-WebSocket-Origin: file://\x0d\x0a")
|
15
|
+
# res.parse("Sec-WebSocket-Location: ws://example.com/demo\x0d\x0a")
|
16
|
+
# res.parse("\x0d\x0a")
|
17
|
+
# res.parse("0st3Rl&q-2ZU^weu")
|
18
|
+
def parse(string)
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
# Construct a WebSocket response in string format.
|
23
|
+
# @example Construct
|
24
|
+
# res = LibWebSocket::Response.new(
|
25
|
+
# :host => 'example.com',
|
26
|
+
# :resource_name => '/demo',
|
27
|
+
# :origin => 'file://',
|
28
|
+
# :number1 => 777_007_543,
|
29
|
+
# :number2 => 114_997_259,
|
30
|
+
# :challenge => "\x47\x30\x22\x2D\x5A\x3F\x47\x58"
|
31
|
+
# )
|
32
|
+
# res.to_s # HTTP/1.1 101 WebSocket Protocol Handshake
|
33
|
+
# # Upgrade: WebSocket
|
34
|
+
# # Connection: Upgrade
|
35
|
+
# # Sec-WebSocket-Origin: file://
|
36
|
+
# # Sec-WebSocket-Location: ws://example.com/demo
|
37
|
+
# #
|
38
|
+
# # 0st3Rl&q-2ZU^weu
|
39
|
+
def to_s
|
40
|
+
string = ''
|
41
|
+
|
42
|
+
string += "HTTP/1.1 101 WebSocket Protocol Handshake\x0d\x0a"
|
43
|
+
|
44
|
+
string += "Upgrade: WebSocket\x0d\x0a"
|
45
|
+
string += "Connection: Upgrade\x0d\x0a"
|
46
|
+
|
47
|
+
raise 'host is required' unless self.host
|
48
|
+
|
49
|
+
location = self.build_url(
|
50
|
+
:host => self.host,
|
51
|
+
:secure => self.secure,
|
52
|
+
:resource_name => self.resource_name
|
53
|
+
)
|
54
|
+
origin = self.origin || 'http://' + location.host
|
55
|
+
|
56
|
+
if self.version <= 75
|
57
|
+
string += 'WebSocket-Protocol: ' + self.subprotocol + "\x0d\x0a" if self.subprotocol
|
58
|
+
string += 'WebSocket-Origin: ' + origin + "\x0d\x0a"
|
59
|
+
string += 'WebSocket-Location: ' + location.to_s + "\x0d\x0a"
|
60
|
+
else
|
61
|
+
string += 'Sec-WebSocket-Protocol: ' + self.subprotocol + "\x0d\x0a" if self.subprotocol
|
62
|
+
string += 'Sec-WebSocket-Origin: ' + origin + "\x0d\x0a"
|
63
|
+
string += 'Sec-WebSocket-Location: ' + location.to_s + "\x0d\x0a"
|
64
|
+
end
|
65
|
+
|
66
|
+
unless self.cookies.empty?
|
67
|
+
string += 'Set-Cookie: '
|
68
|
+
string += self.cookies.collect(&:to_s).join(',')
|
69
|
+
string += "\x0d\x0a"
|
70
|
+
end
|
71
|
+
|
72
|
+
string += "\x0d\x0a"
|
73
|
+
|
74
|
+
string += self.checksum if self.version > 75
|
75
|
+
|
76
|
+
return string
|
77
|
+
end
|
78
|
+
|
79
|
+
# Construct a WebSocket response in rack format.
|
80
|
+
# @example Construct
|
81
|
+
# res = LibWebSocket::Response.new(
|
82
|
+
# :host => 'example.com',
|
83
|
+
# :resource_name => '/demo',
|
84
|
+
# :origin => 'file://',
|
85
|
+
# :number1 => 777_007_543,
|
86
|
+
# :number2 => 114_997_259,
|
87
|
+
# :challenge => "\x47\x30\x22\x2D\x5A\x3F\x47\x58"
|
88
|
+
# )
|
89
|
+
# res.to_rack # [ 101,
|
90
|
+
# # {
|
91
|
+
# # 'Upgrade' => 'WebSocket'
|
92
|
+
# # 'Connection' => 'Upgrade'
|
93
|
+
# # 'Sec-WebSocket-Origin' => 'file://'
|
94
|
+
# # 'Sec-WebSocket-Location' => 'ws://example.com/demo'
|
95
|
+
# # 'Content-Length' => 16
|
96
|
+
# # },
|
97
|
+
# # [ 0st3Rl&q-2ZU^weu ] ]
|
98
|
+
def to_rack
|
99
|
+
status = 101
|
100
|
+
hash = {}
|
101
|
+
body = ''
|
102
|
+
|
103
|
+
hash = {'Upgrade' => 'WebSocket', 'Connection' => 'Upgrade'}
|
104
|
+
|
105
|
+
raise 'host is required' unless self.host
|
106
|
+
|
107
|
+
location = self.build_url(
|
108
|
+
:host => self.host,
|
109
|
+
:secure => self.secure,
|
110
|
+
:resource_name => self.resource_name
|
111
|
+
)
|
112
|
+
origin = self.origin || 'http://' + location.host
|
113
|
+
|
114
|
+
if self.version <= 75
|
115
|
+
hash.merge!('WebSocket-Protocol' => self.subprotocol) if self.subprotocol
|
116
|
+
hash.merge!('WebSocket-Origin' => origin)
|
117
|
+
hash.merge!('WebSocket-Location' => location.to_s)
|
118
|
+
else
|
119
|
+
hash.merge!('Sec-WebSocket-Protocol' => self.subprotocol) if self.subprotocol
|
120
|
+
hash.merge!('Sec-WebSocket-Origin' => origin)
|
121
|
+
hash.merge!('Sec-WebSocket-Location' => location.to_s)
|
122
|
+
end
|
123
|
+
|
124
|
+
unless self.cookies.empty?
|
125
|
+
hash.merge!('Set-Cookie' => self.cookies.collect(&:to_s).join(','))
|
126
|
+
end
|
127
|
+
|
128
|
+
body = self.checksum if self.version > 75
|
129
|
+
|
130
|
+
hash.merge!('Content-Length' => body.length.to_s)
|
131
|
+
|
132
|
+
return [ status, hash, [ body ]]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Build cookies from hash
|
136
|
+
# @see LibWebSocket::Cookie
|
137
|
+
def cookie=(hash)
|
138
|
+
self.cookies.push self.build_cookie(hash)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Draft 76 number 1 reader
|
142
|
+
def number1
|
143
|
+
self.number('number1','key1')
|
144
|
+
end
|
145
|
+
# Draft 76 number 1 writter
|
146
|
+
def number1=(val)
|
147
|
+
self.number('number1','key1',val)
|
148
|
+
end
|
149
|
+
# Draft 76 number 2 reader
|
150
|
+
def number2
|
151
|
+
self.number('number2','key2')
|
152
|
+
end
|
153
|
+
# Draft 76 number 2 writter
|
154
|
+
def number2=(val)
|
155
|
+
self.number('number2','key2',val)
|
156
|
+
end
|
157
|
+
|
158
|
+
protected
|
159
|
+
|
160
|
+
def parse_first_line(line)
|
161
|
+
unless line == 'HTTP/1.1 101 WebSocket Protocol Handshake'
|
162
|
+
self.error = 'Wrong response line'
|
163
|
+
return
|
164
|
+
end
|
165
|
+
|
166
|
+
return self
|
167
|
+
end
|
168
|
+
|
169
|
+
def parse_body
|
170
|
+
if self.field('Sec-WebSocket-Origin')
|
171
|
+
return true if @buffer.length < 16
|
172
|
+
|
173
|
+
self.version = 76
|
174
|
+
|
175
|
+
checksum = @buffer.slice!(0..15)
|
176
|
+
self.checksum = checksum
|
177
|
+
else
|
178
|
+
self.version = 75
|
179
|
+
end
|
180
|
+
|
181
|
+
return self if self.finalize
|
182
|
+
|
183
|
+
self.error = 'Not a valid response'
|
184
|
+
return
|
185
|
+
end
|
186
|
+
|
187
|
+
def finalize
|
188
|
+
location = self.field('Sec-WebSocket-Location') || self.field('WebSocket-Location')
|
189
|
+
return unless location
|
190
|
+
self.location = location
|
191
|
+
|
192
|
+
url = self.build_url
|
193
|
+
return unless url.parse(self.location)
|
194
|
+
|
195
|
+
self.secure = url.secure
|
196
|
+
self.host = url.host
|
197
|
+
self.resource_name = url.resource_name
|
198
|
+
|
199
|
+
self.origin = self.field('Sec-WebSocket-Origin') || self.field('WebSocket-Origin')
|
200
|
+
|
201
|
+
self.subprotocol = self.field('Sec-WebSocket-Protocol') || self.field('WebSocket-Protocol')
|
202
|
+
|
203
|
+
return true
|
204
|
+
end
|
205
|
+
|
206
|
+
def build_url(hash = {})
|
207
|
+
URL.new(hash)
|
208
|
+
end
|
209
|
+
|
210
|
+
def build_cookie(hash = {})
|
211
|
+
Cookie::Response.new(hash)
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
end
|