plezi 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/CHANGELOG.md +450 -0
  4. data/Gemfile +4 -0
  5. data/KNOWN_ISSUES.md +13 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +341 -0
  8. data/Rakefile +2 -0
  9. data/TODO.md +19 -0
  10. data/bin/plezi +301 -0
  11. data/lib/plezi.rb +125 -0
  12. data/lib/plezi/base/cache.rb +77 -0
  13. data/lib/plezi/base/connections.rb +33 -0
  14. data/lib/plezi/base/dsl.rb +177 -0
  15. data/lib/plezi/base/engine.rb +85 -0
  16. data/lib/plezi/base/events.rb +84 -0
  17. data/lib/plezi/base/io_reactor.rb +41 -0
  18. data/lib/plezi/base/logging.rb +62 -0
  19. data/lib/plezi/base/rack_app.rb +89 -0
  20. data/lib/plezi/base/services.rb +57 -0
  21. data/lib/plezi/base/timers.rb +71 -0
  22. data/lib/plezi/handlers/controller_magic.rb +383 -0
  23. data/lib/plezi/handlers/http_echo.rb +27 -0
  24. data/lib/plezi/handlers/http_host.rb +215 -0
  25. data/lib/plezi/handlers/http_router.rb +69 -0
  26. data/lib/plezi/handlers/magic_helpers.rb +43 -0
  27. data/lib/plezi/handlers/route.rb +272 -0
  28. data/lib/plezi/handlers/stubs.rb +143 -0
  29. data/lib/plezi/server/README.md +33 -0
  30. data/lib/plezi/server/helpers/http.rb +169 -0
  31. data/lib/plezi/server/helpers/mime_types.rb +999 -0
  32. data/lib/plezi/server/protocols/http_protocol.rb +318 -0
  33. data/lib/plezi/server/protocols/http_request.rb +133 -0
  34. data/lib/plezi/server/protocols/http_response.rb +294 -0
  35. data/lib/plezi/server/protocols/websocket.rb +208 -0
  36. data/lib/plezi/server/protocols/ws_response.rb +92 -0
  37. data/lib/plezi/server/services/basic_service.rb +224 -0
  38. data/lib/plezi/server/services/no_service.rb +196 -0
  39. data/lib/plezi/server/services/ssl_service.rb +193 -0
  40. data/lib/plezi/version.rb +3 -0
  41. data/plezi.gemspec +26 -0
  42. data/resources/404.erb +68 -0
  43. data/resources/404.haml +64 -0
  44. data/resources/404.html +67 -0
  45. data/resources/404.slim +63 -0
  46. data/resources/500.erb +68 -0
  47. data/resources/500.haml +63 -0
  48. data/resources/500.html +67 -0
  49. data/resources/500.slim +63 -0
  50. data/resources/Gemfile +85 -0
  51. data/resources/anorexic_gray.png +0 -0
  52. data/resources/anorexic_websockets.html +47 -0
  53. data/resources/code.rb +8 -0
  54. data/resources/config.ru +39 -0
  55. data/resources/controller.rb +139 -0
  56. data/resources/db_ac_config.rb +58 -0
  57. data/resources/db_dm_config.rb +51 -0
  58. data/resources/db_sequel_config.rb +42 -0
  59. data/resources/en.yml +204 -0
  60. data/resources/environment.rb +41 -0
  61. data/resources/haml_config.rb +6 -0
  62. data/resources/i18n_config.rb +14 -0
  63. data/resources/rakefile.rb +22 -0
  64. data/resources/redis_config.rb +35 -0
  65. data/resources/routes.rb +26 -0
  66. data/resources/welcome_page.html +72 -0
  67. data/websocket chatroom.md +639 -0
  68. 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