plezi 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/CHANGELOG.md +450 -0
- data/Gemfile +4 -0
- data/KNOWN_ISSUES.md +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +341 -0
- data/Rakefile +2 -0
- data/TODO.md +19 -0
- data/bin/plezi +301 -0
- data/lib/plezi.rb +125 -0
- data/lib/plezi/base/cache.rb +77 -0
- data/lib/plezi/base/connections.rb +33 -0
- data/lib/plezi/base/dsl.rb +177 -0
- data/lib/plezi/base/engine.rb +85 -0
- data/lib/plezi/base/events.rb +84 -0
- data/lib/plezi/base/io_reactor.rb +41 -0
- data/lib/plezi/base/logging.rb +62 -0
- data/lib/plezi/base/rack_app.rb +89 -0
- data/lib/plezi/base/services.rb +57 -0
- data/lib/plezi/base/timers.rb +71 -0
- data/lib/plezi/handlers/controller_magic.rb +383 -0
- data/lib/plezi/handlers/http_echo.rb +27 -0
- data/lib/plezi/handlers/http_host.rb +215 -0
- data/lib/plezi/handlers/http_router.rb +69 -0
- data/lib/plezi/handlers/magic_helpers.rb +43 -0
- data/lib/plezi/handlers/route.rb +272 -0
- data/lib/plezi/handlers/stubs.rb +143 -0
- data/lib/plezi/server/README.md +33 -0
- data/lib/plezi/server/helpers/http.rb +169 -0
- data/lib/plezi/server/helpers/mime_types.rb +999 -0
- data/lib/plezi/server/protocols/http_protocol.rb +318 -0
- data/lib/plezi/server/protocols/http_request.rb +133 -0
- data/lib/plezi/server/protocols/http_response.rb +294 -0
- data/lib/plezi/server/protocols/websocket.rb +208 -0
- data/lib/plezi/server/protocols/ws_response.rb +92 -0
- data/lib/plezi/server/services/basic_service.rb +224 -0
- data/lib/plezi/server/services/no_service.rb +196 -0
- data/lib/plezi/server/services/ssl_service.rb +193 -0
- data/lib/plezi/version.rb +3 -0
- data/plezi.gemspec +26 -0
- data/resources/404.erb +68 -0
- data/resources/404.haml +64 -0
- data/resources/404.html +67 -0
- data/resources/404.slim +63 -0
- data/resources/500.erb +68 -0
- data/resources/500.haml +63 -0
- data/resources/500.html +67 -0
- data/resources/500.slim +63 -0
- data/resources/Gemfile +85 -0
- data/resources/anorexic_gray.png +0 -0
- data/resources/anorexic_websockets.html +47 -0
- data/resources/code.rb +8 -0
- data/resources/config.ru +39 -0
- data/resources/controller.rb +139 -0
- data/resources/db_ac_config.rb +58 -0
- data/resources/db_dm_config.rb +51 -0
- data/resources/db_sequel_config.rb +42 -0
- data/resources/en.yml +204 -0
- data/resources/environment.rb +41 -0
- data/resources/haml_config.rb +6 -0
- data/resources/i18n_config.rb +14 -0
- data/resources/rakefile.rb +22 -0
- data/resources/redis_config.rb +35 -0
- data/resources/routes.rb +26 -0
- data/resources/welcome_page.html +72 -0
- data/websocket chatroom.md +639 -0
- metadata +141 -0
@@ -0,0 +1,294 @@
|
|
1
|
+
module Plezi
|
2
|
+
|
3
|
+
# this class handles HTTP response objects.
|
4
|
+
#
|
5
|
+
# learning from rack, the basic response objects imitates the [0, {}, []] structure... with some updates.
|
6
|
+
#
|
7
|
+
# the Response's body should respond to each (and optionally to close).
|
8
|
+
#
|
9
|
+
# The response can be sent asynchronously, but headers and status cannot be changed once the response started sending data.
|
10
|
+
class HTTPResponse
|
11
|
+
|
12
|
+
#the response's status code
|
13
|
+
attr_accessor :status
|
14
|
+
#the response's headers
|
15
|
+
attr_reader :headers
|
16
|
+
#the flash cookie-jar (single-use cookies, that survive only one request)
|
17
|
+
attr_reader :flash
|
18
|
+
#the response's body container (defaults to an array, but can be replaces by any obect that supports `each` - `close` is NOT supported - call `close` as a callback block after `send` if you need to close the object).
|
19
|
+
attr_accessor :body
|
20
|
+
#bytes sent to the asynchronous que so far - excluding headers (only the body object).
|
21
|
+
attr_reader :bytes_sent
|
22
|
+
#the service through which the response will be sent.
|
23
|
+
attr_reader :service
|
24
|
+
#the request.
|
25
|
+
attr_accessor :request
|
26
|
+
#the http version header
|
27
|
+
attr_accessor :http_version
|
28
|
+
#Danger Zone! direct access to cookie headers - don't use this unless you know what you're doing!
|
29
|
+
attr_reader :cookies
|
30
|
+
|
31
|
+
# the response object responds to a specific request on a specific service.
|
32
|
+
# hence, to initialize a response object, a request must be set.
|
33
|
+
#
|
34
|
+
# use, at the very least `HTTPResponse.new request`
|
35
|
+
def initialize request, status = 200, headers = {}, body = []
|
36
|
+
@request, @status, @headers, @body, @service = request, status, headers, body, (defined?(PLEZI_ON_RACK) ? false : request.service)
|
37
|
+
@http_version = 'HTTP/1.1' # request.version
|
38
|
+
@bytes_sent = 0
|
39
|
+
@finished = @streaming = false
|
40
|
+
@cookies = {}
|
41
|
+
# propegate flash object
|
42
|
+
@flash = Hash.new do |hs,k|
|
43
|
+
hs["plezi_flash_#{k.to_s}"] if hs.has_key? "plezi_flash_#{k.to_s}"
|
44
|
+
end
|
45
|
+
request.cookies.each do |k,v|
|
46
|
+
@flash[k] = v if k.to_s.start_with? "plezi_flash_"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# returns true if headers were already sent
|
51
|
+
def headers_sent?
|
52
|
+
@headers.frozen?
|
53
|
+
end
|
54
|
+
|
55
|
+
# returns true if the response is already finished (the client isn't expecting any more data).
|
56
|
+
def finished?
|
57
|
+
@finished
|
58
|
+
end
|
59
|
+
|
60
|
+
# returns true if the response is set to http streaming (you will need to close the response manually by calling #finish).
|
61
|
+
def streaming?
|
62
|
+
@streaming
|
63
|
+
end
|
64
|
+
|
65
|
+
# sets the http streaming flag, so that the response could be handled asynchronously.
|
66
|
+
#
|
67
|
+
# if this flag is not set, the response will try to automatically finish its job
|
68
|
+
# (send its data and close the connection) once the controllers method has finished.
|
69
|
+
#
|
70
|
+
# If HTTP streaming is set, you will need to manually call `response.finish`
|
71
|
+
# of the connection will not close properly.
|
72
|
+
def start_http_streaming
|
73
|
+
@streaming = true
|
74
|
+
end
|
75
|
+
|
76
|
+
# pushes data to the body of the response. this is the preferred way to add data to the response.
|
77
|
+
#
|
78
|
+
# if HTTP streaming is used, remember to call #send to send the data.
|
79
|
+
# it is also possible to only use #send while streaming, although performance should be considered when streaming using #send rather then caching using #<<.
|
80
|
+
def << str
|
81
|
+
body.push str
|
82
|
+
# send if streaming?
|
83
|
+
end
|
84
|
+
|
85
|
+
# returns a response header, if set.
|
86
|
+
def [] header
|
87
|
+
headers[header] # || @cookies[header]
|
88
|
+
end
|
89
|
+
|
90
|
+
# sets a response header. response headers should be a down-case String or Symbol.
|
91
|
+
#
|
92
|
+
# this is the prefered to set a header.
|
93
|
+
#
|
94
|
+
# returns the value set for the header.
|
95
|
+
#
|
96
|
+
# see HTTP response headers for valid headers and values: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
|
97
|
+
def []= header, value
|
98
|
+
header.is_a?(String) ? header.downcase! : (header.is_a?(Symbol) ? (header = header.to_s.downcase.to_sym) : (return false))
|
99
|
+
headers[header] = value
|
100
|
+
end
|
101
|
+
|
102
|
+
# sets/deletes cookies when headers are sent.
|
103
|
+
#
|
104
|
+
# accepts:
|
105
|
+
# name:: the cookie's name
|
106
|
+
# value:: the cookie's value
|
107
|
+
# parameters:: a parameters Hash for cookie creation.
|
108
|
+
#
|
109
|
+
# parameters accept any of the following Hash keys and values:
|
110
|
+
#
|
111
|
+
# expires:: a Time object with the expiration date. defaults to 10 years in the future.
|
112
|
+
# max_age:: a Max-Age HTTP cookie string.
|
113
|
+
# path:: the path from which the cookie is acessible. defaults to '/'.
|
114
|
+
# domain:: the domain for the cookie (best used to manage subdomains). defaults to the active domain (sub-domain limitations might apply).
|
115
|
+
# secure:: if set to `true`, the cookie will only be available over secure connections. defaults to false.
|
116
|
+
# http_only:: if true, the HttpOnly flag will be set (not accessible to javascript). defaults to false.
|
117
|
+
#
|
118
|
+
def set_cookie name, value, params = {}
|
119
|
+
params[:expires] = (Time.now - 315360000) unless value
|
120
|
+
value ||= 'deleted'
|
121
|
+
params[:expires] ||= (Time.now + 315360000) unless params[:max_age]
|
122
|
+
params[:path] ||= '/'
|
123
|
+
value = HTTP.encode(value.to_s)
|
124
|
+
if params[:max_age]
|
125
|
+
value << ("; Max-Age=%s" % params[:max_age])
|
126
|
+
else
|
127
|
+
value << ("; Expires=%s" % params[:expires].httpdate)
|
128
|
+
end
|
129
|
+
value << "; Path=#{params[:path]}"
|
130
|
+
value << "; Domain=#{params[:domain]}" if params[:domain]
|
131
|
+
value << "; Secure" if params[:secure]
|
132
|
+
value << "; HttpOnly" if params[:http_only]
|
133
|
+
@cookies[HTTP.encode(name.to_s).to_sym] = value
|
134
|
+
end
|
135
|
+
|
136
|
+
# deletes a cookie (actually calls `set_cookie name, nil`)
|
137
|
+
def delete_cookie name
|
138
|
+
set_cookie name, nil
|
139
|
+
end
|
140
|
+
|
141
|
+
# clears the response object, unless headers were already sent (use `response.body.clear` to clear only the unsent body).
|
142
|
+
#
|
143
|
+
# returns false if the response was already sent.
|
144
|
+
def clear
|
145
|
+
return false if headers.frozen? || @finished
|
146
|
+
@status, @body, @headers, @cookies = 200, [], {}, {}
|
147
|
+
true
|
148
|
+
end
|
149
|
+
|
150
|
+
# sends the response object. headers will be frozen (they can only be sent at the head of the response).
|
151
|
+
#
|
152
|
+
# the response will remain open for more data to be sent through (using `response << data` and `response.send`).
|
153
|
+
def send(str = nil)
|
154
|
+
raise 'HTTPResponse SERVICE MISSING: cannot send http response without a service.' unless service
|
155
|
+
body << str if str && body.is_a?(Array)
|
156
|
+
send_headers
|
157
|
+
return if request.head?
|
158
|
+
if headers["transfer-encoding"] == "chunked"
|
159
|
+
body.each do |s|
|
160
|
+
service.send "#{s.bytesize.to_s(16)}\r\n"
|
161
|
+
service.send s
|
162
|
+
service.send "\r\n"
|
163
|
+
@bytes_sent += s.bytesize
|
164
|
+
end
|
165
|
+
else
|
166
|
+
body.each do |s|
|
167
|
+
service.send s
|
168
|
+
@bytes_sent += s.bytesize
|
169
|
+
end
|
170
|
+
end
|
171
|
+
@body.is_a?(Array) ? @body.clear : ( @body = [] )
|
172
|
+
end
|
173
|
+
|
174
|
+
# sends the response and flags the response as complete. future data should not be sent. the flag will only be enforced be the Plezi router. your code might attempt sending data (which would probbaly be ignored by the client or raise an exception).
|
175
|
+
def finish
|
176
|
+
@headers['content-length'] ||= body[0].bytesize if !headers_sent? && body.is_a?(Array) && body.length == 1
|
177
|
+
return self if defined?(PLEZI_ON_RACK)
|
178
|
+
raise 'HTTPResponse SERVICE MISSING: cannot send http response without a service.' unless service
|
179
|
+
self.send
|
180
|
+
service.send( (headers["transfer-encoding"] == "chunked") ? "0\r\n\r\n" : nil)
|
181
|
+
@finished = true
|
182
|
+
# log
|
183
|
+
Plezi.log_raw "#{request[:client_ip]} [#{Time.now.utc}] \"#{request[:method]} #{request[:original_path]} #{request[:requested_protocol]}\/#{request[:version]}\" #{status} #{bytes_sent.to_s} #{"%0.3f" % ((Time.now - request[:time_recieved])*1000)}ms\n"
|
184
|
+
end
|
185
|
+
|
186
|
+
# Danger Zone (internally used method, use with care): attempts to finish the response - if it was not flaged as streaming or completed.
|
187
|
+
def try_finish
|
188
|
+
finish unless @finished || @streaming
|
189
|
+
end
|
190
|
+
|
191
|
+
# Danger Zone (internally used method, use with care): fix response's headers before sending them (date, connection and transfer-coding).
|
192
|
+
def fix_headers
|
193
|
+
# headers['Connection'] ||= "Keep-Alive"
|
194
|
+
headers['date'] = Time.now.httpdate
|
195
|
+
headers['transfer-encoding'] ||= 'chunked' if !headers['content-length']
|
196
|
+
headers['cache-control'] ||= 'no-cache'
|
197
|
+
# remove old flash cookies
|
198
|
+
request.cookies.keys.each do |k|
|
199
|
+
if k.to_s.start_with? "plezi_flash_"
|
200
|
+
set_cookie k, nil
|
201
|
+
flash.delete k
|
202
|
+
end
|
203
|
+
end
|
204
|
+
#set new flash cookies
|
205
|
+
@flash.each do |k,v|
|
206
|
+
set_cookie "plezi_flash_#{k.to_s}", v
|
207
|
+
end
|
208
|
+
end
|
209
|
+
# Danger Zone (internally used method, use with care): fix response's headers before sending them (date, connection and transfer-coding).
|
210
|
+
def send_headers
|
211
|
+
return false if @headers.frozen?
|
212
|
+
fix_headers
|
213
|
+
service.send "#{@http_version} #{status} #{STATUS_CODES[status] || 'unknown'}\r\n"
|
214
|
+
headers.each {|k,v| service.send "#{k.to_s}: #{v}\r\n"}
|
215
|
+
@cookies.each {|k,v| service.send "Set-Cookie: #{k.to_s}=#{v.to_s}\r\n"}
|
216
|
+
service.send "\r\n"
|
217
|
+
@headers.freeze
|
218
|
+
# @cookies.freeze
|
219
|
+
end
|
220
|
+
|
221
|
+
# response status codes, as defined.
|
222
|
+
STATUS_CODES = {100=>"Continue",
|
223
|
+
101=>"Switching Protocols",
|
224
|
+
102=>"Processing",
|
225
|
+
200=>"OK",
|
226
|
+
201=>"Created",
|
227
|
+
202=>"Accepted",
|
228
|
+
203=>"Non-Authoritative Information",
|
229
|
+
204=>"No Content",
|
230
|
+
205=>"Reset Content",
|
231
|
+
206=>"Partial Content",
|
232
|
+
207=>"Multi-Status",
|
233
|
+
208=>"Already Reported",
|
234
|
+
226=>"IM Used",
|
235
|
+
300=>"Multiple Choices",
|
236
|
+
301=>"Moved Permanently",
|
237
|
+
302=>"Found",
|
238
|
+
303=>"See Other",
|
239
|
+
304=>"Not Modified",
|
240
|
+
305=>"Use Proxy",
|
241
|
+
306=>"(Unused)",
|
242
|
+
307=>"Temporary Redirect",
|
243
|
+
308=>"Permanent Redirect",
|
244
|
+
400=>"Bad Request",
|
245
|
+
401=>"Unauthorized",
|
246
|
+
402=>"Payment Required",
|
247
|
+
403=>"Forbidden",
|
248
|
+
404=>"Not Found",
|
249
|
+
405=>"Method Not Allowed",
|
250
|
+
406=>"Not Acceptable",
|
251
|
+
407=>"Proxy Authentication Required",
|
252
|
+
408=>"Request Timeout",
|
253
|
+
409=>"Conflict",
|
254
|
+
410=>"Gone",
|
255
|
+
411=>"Length Required",
|
256
|
+
412=>"Precondition Failed",
|
257
|
+
413=>"Payload Too Large",
|
258
|
+
414=>"URI Too Long",
|
259
|
+
415=>"Unsupported Media Type",
|
260
|
+
416=>"Range Not Satisfiable",
|
261
|
+
417=>"Expectation Failed",
|
262
|
+
422=>"Unprocessable Entity",
|
263
|
+
423=>"Locked",
|
264
|
+
424=>"Failed Dependency",
|
265
|
+
426=>"Upgrade Required",
|
266
|
+
428=>"Precondition Required",
|
267
|
+
429=>"Too Many Requests",
|
268
|
+
431=>"Request Header Fields Too Large",
|
269
|
+
500=>"Internal Server Error",
|
270
|
+
501=>"Not Implemented",
|
271
|
+
502=>"Bad Gateway",
|
272
|
+
503=>"Service Unavailable",
|
273
|
+
504=>"Gateway Timeout",
|
274
|
+
505=>"HTTP Version Not Supported",
|
275
|
+
506=>"Variant Also Negotiates",
|
276
|
+
507=>"Insufficient Storage",
|
277
|
+
508=>"Loop Detected",
|
278
|
+
510=>"Not Extended",
|
279
|
+
511=>"Network Authentication Required"
|
280
|
+
}
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
######
|
285
|
+
## example requests
|
286
|
+
|
287
|
+
# GET / HTTP/1.1
|
288
|
+
# Host: localhost:2000
|
289
|
+
# Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
290
|
+
# Cookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
|
291
|
+
# User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
|
292
|
+
# Accept-Language: en-us
|
293
|
+
# Accept-Encoding: gzip, deflate
|
294
|
+
# Connection: keep-alive
|
@@ -0,0 +1,208 @@
|
|
1
|
+
module Plezi
|
2
|
+
|
3
|
+
# this module is the protocol (controller) for the HTTP server.
|
4
|
+
#
|
5
|
+
#
|
6
|
+
# to do: implemet logging, support body types: multipart (non-ASCII form data / uploaded files), json & xml
|
7
|
+
class WSProtocol
|
8
|
+
|
9
|
+
SUPPORTED_EXTENTIONS = {}
|
10
|
+
# SUPPORTED_EXTENTIONS['x-webkit-deflate-frame'] = Proc.new {|body, params| }
|
11
|
+
# SUPPORTED_EXTENTIONS['permessage-deflate'] = Proc.new {|body, params| } # client_max_window_bits
|
12
|
+
|
13
|
+
# get the timeout interval for this websockt (the number of seconds the socket can remain with no activity - will be reset every ping, message etc').
|
14
|
+
def timeout_interval
|
15
|
+
@timeout_interval
|
16
|
+
end
|
17
|
+
# set the timeout interval for this websockt (the number of seconds the socket can remain with no activity - will be reset every ping, message etc').
|
18
|
+
def timeout_interval= value
|
19
|
+
@timeout_interval = value
|
20
|
+
Plezi.callback service, :set_timeout, @timeout_interval
|
21
|
+
end
|
22
|
+
|
23
|
+
# the service (holding the socket) over which this protocol is running.
|
24
|
+
attr_reader :service
|
25
|
+
# the extentions registered for the websockets connection.
|
26
|
+
attr_reader :extentions
|
27
|
+
|
28
|
+
def initialize service, params
|
29
|
+
@params = params
|
30
|
+
@service = service
|
31
|
+
@extentions = []
|
32
|
+
@locker = Mutex.new
|
33
|
+
@parser_stage = 0
|
34
|
+
@parser_data = {}
|
35
|
+
@parser_data[:body] = []
|
36
|
+
@parser_data[:step] = 0
|
37
|
+
@in_que = []
|
38
|
+
@message = ''
|
39
|
+
@timeout_interval = 60
|
40
|
+
end
|
41
|
+
|
42
|
+
# called when connection is initialized.
|
43
|
+
def on_connect service
|
44
|
+
# cancel service timeout? (for now, reset to 60 seconds)
|
45
|
+
service.timeout = @timeout_interval
|
46
|
+
# Plezi.callback service, :timeout=, @timeout_interval
|
47
|
+
Plezi.callback @service.handler, :on_connect if @service.handler.methods.include?(:on_connect)
|
48
|
+
Plezi.info "Upgraded HTTP to WebSockets. Logging only errors."
|
49
|
+
end
|
50
|
+
|
51
|
+
# called when data is recieved
|
52
|
+
# returns an Array with any data not yet processed (to be returned to the in-que).
|
53
|
+
def on_message(service)
|
54
|
+
# parse the request
|
55
|
+
return @locker.synchronize {extract_message service.read.bytes}
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
# called when a disconnect is fired
|
60
|
+
# (socket was disconnected / service should be disconnected / shutdown / socket error)
|
61
|
+
def on_disconnect service
|
62
|
+
Plezi.callback @service.handler, :on_disconnect if @service.handler.methods.include?(:on_disconnect)
|
63
|
+
end
|
64
|
+
|
65
|
+
# called when an exception was raised
|
66
|
+
# (socket was disconnected / service should be disconnected / shutdown / socket error)
|
67
|
+
def on_exception service, e
|
68
|
+
Plezi.error e
|
69
|
+
end
|
70
|
+
|
71
|
+
########
|
72
|
+
# Protocol Specific Helpers
|
73
|
+
|
74
|
+
# perform the HTTP handshake for WebSockets. send a 400 Bad Request error if handshake fails.
|
75
|
+
def http_handshake request, response, handler
|
76
|
+
# review handshake (version, extentions)
|
77
|
+
# should consider adopting the websocket gem for handshake and framing:
|
78
|
+
# https://github.com/imanel/websocket-ruby
|
79
|
+
# http://www.rubydoc.info/github/imanel/websocket-ruby
|
80
|
+
return request.service.handler.hosts[request[:host] || :default].send_by_code request, 400 , response.headers.merge('sec-websocket-extensions' => SUPPORTED_EXTENTIONS.keys.join(', ')) unless request['upgrade'].to_s.downcase == 'websocket' &&
|
81
|
+
request['sec-websocket-key'] &&
|
82
|
+
request['connection'].to_s.downcase == 'upgrade' &&
|
83
|
+
# (request['sec-websocket-extensions'].split(/[\s]*[,][\s]*/).reject {|ex| ex == '' || SUPPORTED_EXTENTIONS[ex.split(/[\s]*;[\s]*/)[0]] } ).empty? &&
|
84
|
+
(request['sec-websocket-version'].to_s.downcase.split(/[, ]/).map {|s| s.strip} .include?( '13' ))
|
85
|
+
response.status = 101
|
86
|
+
response['upgrade'] = 'websocket'
|
87
|
+
response['content-length'] = '0'
|
88
|
+
response['connection'] = 'Upgrade'
|
89
|
+
response['sec-websocket-version'] = '13'
|
90
|
+
# Note that the client is only offering to use any advertised extensions
|
91
|
+
# and MUST NOT use them unless the server indicates that it wishes to use the extension.
|
92
|
+
request['sec-websocket-extensions'].split(/[\s]*[,][\s]*/).each {|ex| @extentions << ex.split(/[\s]*;[\s]*/) if SUPPORTED_EXTENTIONS[ex.split(/[\s]*;[\s]*/)[0]]}
|
93
|
+
response['sec-websocket-extensions'] = @extentions.map {|e| e[0] } .join (',')
|
94
|
+
response.headers.delete 'sec-websocket-extensions' if response['sec-websocket-extensions'].empty?
|
95
|
+
response['Sec-WebSocket-Accept'] = Digest::SHA1.base64digest(request['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
96
|
+
response.finish
|
97
|
+
@extentions.freeze
|
98
|
+
response.service.protocol = self
|
99
|
+
response.service.handler = handler
|
100
|
+
Plezi.callback self, :on_connect, response.service
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
|
104
|
+
# parse the message and send it to the handler
|
105
|
+
#
|
106
|
+
# test: frame = ["819249fcd3810b93b2fb69afb6e62c8af3e83adc94ee2ddd"].pack("H*").bytes; @parser_stage = 0; @parser_data = {}
|
107
|
+
# accepts:
|
108
|
+
# frame:: an array of bytes
|
109
|
+
def extract_message data
|
110
|
+
until data.empty?
|
111
|
+
if @parser_stage == 0 && !data.empty?
|
112
|
+
@parser_data[:fin] = data[0][7] == 1
|
113
|
+
@parser_data[:rsv1] = data[0][6] == 1
|
114
|
+
@parser_data[:rsv2] = data[0][5] == 1
|
115
|
+
@parser_data[:rsv3] = data[0][4] == 1
|
116
|
+
@parser_data[:op_code] = data[0] & 0b00001111
|
117
|
+
@parser_op_code ||= data[0] & 0b00001111
|
118
|
+
@parser_stage += 1
|
119
|
+
data.shift
|
120
|
+
end
|
121
|
+
if @parser_stage == 1
|
122
|
+
@parser_data[:mask] = data[0][7]
|
123
|
+
@parser_data[:len] = data[0] & 0b01111111
|
124
|
+
data.shift
|
125
|
+
if @parser_data[:len] == 126
|
126
|
+
@parser_data[:len] = merge_bytes( *(data.slice!(0,2)) ) # should be = ?
|
127
|
+
elsif @parser_data[:len] == 127
|
128
|
+
len = 0
|
129
|
+
@parser_data[:len] = merge_bytes( *(data.slice!(0,8)) ) # should be = ?
|
130
|
+
end
|
131
|
+
@parser_data[:step] = 0
|
132
|
+
@parser_stage += 1
|
133
|
+
end
|
134
|
+
if @parser_stage == 2 && @parser_data[:mask] == 1
|
135
|
+
@parser_data[:mask_key] = data.slice!(0,4)
|
136
|
+
@parser_stage += 1
|
137
|
+
elsif @parser_data[:mask] != 1
|
138
|
+
@parser_stage += 1
|
139
|
+
end
|
140
|
+
if @parser_stage == 3 && @parser_data[:step] < @parser_data[:len]
|
141
|
+
# data.length.times {|i| data[0] = data[0] ^ @parser_data[:mask_key][@parser_data[:step] % 4] if @parser_data[:mask_key]; @parser_data[:step] += 1; @parser_data[:body] << data.shift; break if @parser_data[:step] == @parser_data[:len]}
|
142
|
+
slice_length = [data.length, (@parser_data[:len]-@parser_data[:step])].min
|
143
|
+
if @parser_data[:mask_key]
|
144
|
+
masked = data.slice!(0, slice_length)
|
145
|
+
masked.map!.with_index {|b, i| b ^ @parser_data[:mask_key][ ( i + @parser_data[:step] ) % 4] }
|
146
|
+
@parser_data[:body].concat masked
|
147
|
+
else
|
148
|
+
@parser_data[:body].concat data.slice!(0, slice_length)
|
149
|
+
end
|
150
|
+
@parser_data[:step] += slice_length
|
151
|
+
end
|
152
|
+
complete_frame unless @parser_data[:step] < @parser_data[:len]
|
153
|
+
end
|
154
|
+
true
|
155
|
+
end
|
156
|
+
|
157
|
+
# takes and Array of bytes and combines them to an int(16 Bit), 32Bit or 64Bit number
|
158
|
+
def merge_bytes *bytes
|
159
|
+
return bytes.pop if bytes.length == 1
|
160
|
+
bytes.pop ^ (merge_bytes(*bytes) << 8)
|
161
|
+
end
|
162
|
+
|
163
|
+
# handles the completed frame and sends a message to the handler once all the data has arrived.
|
164
|
+
def complete_frame
|
165
|
+
@extentions.each {|ex| SUPPORTED_EXTENTIONS[ex[0]][1].call(@parser_data[:body], ex[1..-1]) if SUPPORTED_EXTENTIONS[ex[0]]}
|
166
|
+
|
167
|
+
case @parser_data[:op_code]
|
168
|
+
when 9, 10
|
169
|
+
# handle @parser_data[:op_code] == 9 (ping) / @parser_data[:op_code] == 10 (pong)
|
170
|
+
Plezi.callback @service, :send_nonblock, WSResponse.frame_data(@parser_data[:body].pack('C*'), 10)
|
171
|
+
@parser_op_code = nil if @parser_op_code == 9 || @parser_op_code == 10
|
172
|
+
when 8
|
173
|
+
# handle @parser_data[:op_code] == 8 (close)
|
174
|
+
Plezi.callback( @service, :send_nonblock, WSResponse.frame_data('', 8) ) { @service.disconnect }
|
175
|
+
@parser_op_code = nil if @parser_op_code == 8
|
176
|
+
else
|
177
|
+
@message << @parser_data[:body].pack('C*')
|
178
|
+
# handle @parser_data[:op_code] == 0 / fin == false (continue a frame that hasn't ended yet)
|
179
|
+
if @parser_data[:fin]
|
180
|
+
HTTP.make_utf8! @message if @parser_op_code == 1
|
181
|
+
Plezi.callback @service.handler, :on_message, @message
|
182
|
+
@message = ''
|
183
|
+
@parser_op_code = nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
@parser_stage = 0
|
187
|
+
@parser_data[:body].clear
|
188
|
+
@parser_data[:step] = 0
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
######
|
195
|
+
## example requests
|
196
|
+
|
197
|
+
# GET /?encoding=text HTTP/1.1
|
198
|
+
# Upgrade: websocket
|
199
|
+
# Connection: Upgrade
|
200
|
+
# Host: localhost:3001
|
201
|
+
# Origin: https://www.websocket.org
|
202
|
+
# Cookie: test=my%20cookies; user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
|
203
|
+
# Pragma: no-cache
|
204
|
+
# Cache-Control: no-cache
|
205
|
+
# Sec-WebSocket-Key: 1W9B64oYSpyRL/yuc4k+Ww==
|
206
|
+
# Sec-WebSocket-Version: 13
|
207
|
+
# Sec-WebSocket-Extensions: x-webkit-deflate-frame
|
208
|
+
# User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/600.1.25 (KHTML, like Gecko) Version/8.0 Safari/600.1.25
|