libwebsocket 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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