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.
@@ -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