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,318 @@
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 HTTPProtocol
8
+
9
+ HTTP_METHODS = %w{GET HEAD POST PUT DELETE TRACE OPTIONS}
10
+
11
+ attr_accessor :service
12
+
13
+ def initialize service, params
14
+ @service = service
15
+ @parser_stage = 0
16
+ @parser_data = {}
17
+ @parser_body = ''
18
+ @parser_chunk = ''
19
+ @parser_length = 0
20
+ @locker = Mutex.new
21
+ @@rack_dictionary ||= {"HOST".freeze => :host_name, 'REQUEST_METHOD'.freeze => :method,
22
+ 'PATH_INFO'.freeze => :path, 'QUERY_STRING'.freeze => :query,
23
+ 'SERVER_NAME'.freeze => :host_name, 'SERVER_PORT'.freeze => :port,
24
+ 'rack.url_scheme'.freeze => :requested_protocol}
25
+ end
26
+
27
+ # called when connection is initialized.
28
+ def on_connect service
29
+ end
30
+
31
+ # called when data is recieved.
32
+ #
33
+ # this method is called within a lock on the service (Mutex) - craeful from double locking.
34
+ #
35
+ # typically returns an Array with any data not yet processed (to be returned to the in-que)... but here it always processes (or discards) the data.
36
+ def on_message(service)
37
+ # parse the request
38
+ @locker.synchronize { parse_message }
39
+ if (@parser_stage == 1) && @parser_data[:version] >= 1.1
40
+ # send 100 continue message????? doesn't work! both Crome and Safari go crazy if this is sent after the request was sent (but before all the packets were recieved... msgs over 1 Mb).
41
+ # Plezi.push_event Proc.new { Plezi.info "sending continue signal."; service.send_nonblock "100 Continue\r\n\r\n" }
42
+ # service.send_unsafe_interrupt "100 Continue\r\n\r\n" # causes double lock on service
43
+ end
44
+ true
45
+ end
46
+
47
+ # # called when a disconnect is fired
48
+ # # (socket was disconnected / service should be disconnected / shutdown / socket error)
49
+ def on_disconnect service
50
+ end
51
+
52
+ # called when an exception was raised
53
+ # (socket was disconnected / service should be disconnected / shutdown / socket error)
54
+ def on_exception service, e
55
+ Plezi.error e
56
+ end
57
+
58
+
59
+ # Protocol specific helper methods.
60
+
61
+ # parses incoming data
62
+ def parse_message data = nil
63
+ data ||= service.read.to_s.lines.to_a
64
+ # require 'pry'; binding.pry
65
+ if @parser_stage == 0
66
+ return false unless parse_method data
67
+ end
68
+ if @parser_stage == 1
69
+ return false unless parse_head data
70
+ end
71
+ if @parser_stage == 2
72
+ return false unless parse_body data
73
+ end
74
+ true
75
+ end
76
+
77
+ # parses the method request (the first line in the HTTP request).
78
+ def parse_method data
79
+ return false unless data[0] && data[0].match(/^#{HTTP_METHODS.join('|')}/)
80
+ @parser_data[:time_recieved] = Time.now
81
+ @parser_data[:params] = {}
82
+ @parser_data[:cookies] = Cookies.new
83
+ @parser_data[:method] = ''
84
+ @parser_data[:query] = ''
85
+ @parser_data[:original_path] = ''
86
+ @parser_data[:path] = ''
87
+ if defined? Rack
88
+ @parser_data['rack.version'] = Rack::VERSION
89
+ @parser_data['rack.multithread'] = true
90
+ @parser_data['rack.multiprocess'] = false
91
+ @parser_data['rack.hijack?'] = false
92
+ @parser_data['rack.logger'] = Plezi.logger
93
+ end
94
+ @parser_data[:method], @parser_data[:query], @parser_data[:version] = data.shift.split(/[\s]+/)
95
+ @parser_data[:version] = (@parser_data[:version] || 'HTTP/1.1').match(/[0-9\.]+/).to_s.to_f
96
+ data.shift while data[0].to_s.match /^[\r\n]+/
97
+ @parser_stage = 1
98
+ end
99
+
100
+ #parses the head on a request (headers and values).
101
+ def parse_head data
102
+ until data[0].nil? || data[0].match(/^[\r\n]+$/)
103
+ m = data.shift.match(/^([^:]*):[\s]*([^\r\n]*)/)
104
+ # move cookies to cookie-jar, all else goes to headers
105
+ case m[1].downcase
106
+ when 'cookie'
107
+ HTTP.extract_data m[2].split(/[;,][\s]?/), @parser_data[:cookies], :uri
108
+ end
109
+ @parser_data[ HTTP.make_utf8!(m[1]).downcase ] ? (@parser_data[ HTTP.make_utf8!(m[1]).downcase ] << ", #{HTTP.make_utf8! m[2]}"): (@parser_data[ HTTP.make_utf8!(m[1]).downcase ] = HTTP.make_utf8! m[2])
110
+ end
111
+ return false unless data[0]
112
+ data.shift while data[0] && data[0].match(/^[\r\n]+$/)
113
+ if @parser_data["transfer-coding"] || (@parser_data["content-length"] && @parser_data["content-length"].to_i != 0) || @parser_data["content-type"]
114
+ @parser_stage = 2
115
+ else
116
+ # create request object and hand over to handler
117
+ complete_request
118
+ return parse_message data unless data.empty?
119
+ end
120
+ true
121
+ end
122
+
123
+ #parses the body of a request.
124
+ def parse_body data
125
+ # check for body is needed, if exists and if complete
126
+ if @parser_data["transfer-coding"] == "chunked"
127
+ until data.empty? || data[0].to_s.match(/0(\r)?\n/)
128
+ if @parser_length == 0
129
+ @parser_length = data.to_s.shift.match(/^[a-z0-9A-Z]+/).to_i(16)
130
+ @parser_chunk.clear
131
+ end
132
+ unless @parser_length == 0
133
+ @parser_chunk << data.shift while ( (@parser_length >= @parser_chunk.bytesize) && data[0])
134
+ end
135
+ if @parser_length <= @parser_chunk.bytesize
136
+ @parser_body << @parser_chunk.byteslice(0, @parser_body.bytesize)
137
+ @parser_length = 0
138
+ @parser_chunk.clear
139
+ end
140
+ end
141
+ return false unless data[0].to_s.match(/0(\r)?\n/)
142
+ true until data.empty? || data.shift.match(/^[\r\n]+$/)
143
+ data.shift while data[0].to_s.match /^[\r\n]+$/
144
+ elsif @parser_data["content-length"].to_i
145
+ @parser_length = @parser_data["content-length"].to_i if @parser_length == 0
146
+ @parser_chunk << data.shift while @parser_length > @parser_chunk.bytesize && data[0]
147
+ return false if @parser_length > @parser_chunk.bytesize
148
+ @parser_body = @parser_chunk.byteslice(0, @parser_length)
149
+ @parser_chunk.clear
150
+ else
151
+ Plezi.warn 'bad body request - trying to read'
152
+ @parser_body << data.shift while data[0] && !data[0].match(/^[\r\n]+$/)
153
+ end
154
+ # parse body (POST parameters)
155
+ read_body
156
+
157
+ # complete request
158
+ complete_request
159
+
160
+ #read next request unless data is finished
161
+ return parse_message data unless data.empty?
162
+ true
163
+ end
164
+
165
+ # completes the parsing of the request and sends the request to the handler.
166
+ def complete_request
167
+ #finalize params and query properties
168
+ m = @parser_data[:query].match /(([a-z0-9A-Z]+):\/\/)?(([^\/\:]+))?(:([0-9]+))?([^\?\#]*)(\?([^\#]*))?/
169
+ @parser_data[:requested_protocol] = m[1] || (service.ssl? ? 'https' : 'http')
170
+ @parser_data[:host_name] = m[4] || (@parser_data['host'] ? @parser_data['host'].match(/^[^:]*/).to_s : nil)
171
+ @parser_data[:port] = m[6] || (@parser_data['host'] ? @parser_data['host'].match(/:([0-9]*)/).to_a[1] : nil)
172
+ @parser_data[:original_path] = HTTP.decode(m[7], :uri) || '/'
173
+ @parser_data['host'] ||= "#{@parser_data[:host_name]}:#{@parser_data[:port]}"
174
+ # parse query for params - m[9] is the data part of the query
175
+ if m[9]
176
+ HTTP.extract_data m[9].split(/[&;]/), @parser_data[:params]
177
+ end
178
+
179
+ HTTP.make_utf8! @parser_data[:original_path]
180
+ @parser_data[:path] = @parser_data[:original_path].chomp('/')
181
+ @parser_data[:original_path].freeze
182
+
183
+ HTTP.make_utf8! @parser_data[:host_name] if @parser_data[:host_name]
184
+ HTTP.make_utf8! @parser_data[:query]
185
+
186
+ @parser_data[:client_ip] = @parser_data['x-forwarded-for'].to_s.split(/,[\s]?/)[0] || (service.socket.remote_address.ip_address) rescue 'unknown IP'
187
+
188
+ @@rack_dictionary.each {|k,v| @parser_data[k] = @parser_data[v]}
189
+
190
+ #create request
191
+ request = HTTPRequest.new service
192
+ request.update @parser_data
193
+
194
+ #clear current state
195
+ @parser_data.clear
196
+ @parser_body.clear
197
+ @parser_chunk.clear
198
+ @parser_length = 0
199
+ @parser_stage = 0
200
+
201
+ #check for server-responses
202
+ case request.request_method
203
+ when "TRACE"
204
+ return true
205
+ when "OPTIONS"
206
+ Plezi.push_event Proc.new do
207
+ response = HTTPResponse.new request
208
+ response[:Allow] = "GET,HEAD,POST,PUT,DELETE,OPTIONS"
209
+ response["access-control-allow-origin"] = "*"
210
+ response['content-length'] = 0
211
+ response.finish
212
+ end
213
+ return true
214
+ end
215
+
216
+ #pass it to the handler or decler error.
217
+ if service && service.handler
218
+ Plezi.callback service.handler, :on_request, request
219
+ else
220
+ Plezi.error "No Handler for this HTTP service."
221
+ end
222
+ end
223
+
224
+ # read the body's data and parse any incoming data.
225
+ def read_body
226
+ # parse content
227
+ case @parser_data["content-type"].to_s
228
+ when /x-www-form-urlencoded/
229
+ HTTP.extract_data @parser_body.split(/[&;]/), @parser_data[:params], :uri
230
+ when /multipart\/form-data/
231
+ read_multipart @parser_data, @parser_body
232
+ when /text\/xml/
233
+ # to-do support xml? support json?
234
+ @parser_data[:body] = @parser_body.dup
235
+ when /application\/json/
236
+ @parser_data[:body] = @parser_body.dup
237
+ JSON.parse(HTTP.make_utf8! @parser_data[:body]).each {|k, v| HTTP.add_param_to_hash k, v, @parser_data[:params]}
238
+ else
239
+ @parser_data[:body] = @parser_body.dup
240
+ Plezi.error "POST body type (#{@parser_data["content-type"]}) cannot be parsed. raw body is kept in the request's data as request[:body]: #{@parser_body}"
241
+ end
242
+ end
243
+
244
+ # parse a mime/multipart body or part.
245
+ def read_multipart headers, part, name_prefix = ''
246
+ if headers["content-type"].to_s.match /multipart/
247
+ boundry = headers["content-type"].match(/boundary=([^\s]+)/)[1]
248
+ if headers["content-disposition"].to_s.match /name=/
249
+ if name_prefix.empty?
250
+ name_prefix << HTTP.decode(headers["content-disposition"].to_s.match(/name="([^"]*)"/)[1])
251
+ else
252
+ name_prefix << "[#{HTTP.decode(headers["content-disposition"].to_s.match(/name="([^"]*)"/)[1])}]"
253
+ end
254
+ end
255
+ part.split(/([\r]?\n)?--#{boundry}(--)?[\r]?\n/).each do |p|
256
+ unless p.strip.empty? || p=='--'
257
+ # read headers
258
+ h = {}
259
+ p = p.lines
260
+ while p[0].match(/^[^:]+:[^\r\n]+/)
261
+ m = p.shift.match(/^([^:]+):[\s]?([^\r\n]+)/)
262
+ h[m[1].downcase] = m[2]
263
+ end
264
+ if p[0].strip.empty?
265
+ p.shift
266
+ else
267
+ Plezi.error 'Expected empty line after last header - empty line missing.'
268
+ end
269
+ # send headers and body to be read
270
+ read_multipart h, p.join, name_prefix
271
+ end
272
+ end
273
+ return
274
+ end
275
+
276
+ # require a part body to exist (data exists) for parsing
277
+ return true if part.to_s.empty?
278
+
279
+ # convert part to `charset` if charset is defined?
280
+
281
+ if !headers["content-disposition"]
282
+ Plezi.error "Wrong multipart format with headers: #{headers} and body: #{part}"
283
+ return
284
+ end
285
+
286
+ cd = {}
287
+
288
+ HTTP.extract_data headers["content-disposition"].match(/[^;];([^\r\n]*)/)[1].split(/[;,][\s]?/), cd, :uri
289
+
290
+ name = name_prefix.dup
291
+
292
+ if name_prefix.empty?
293
+ name << HTTP.decode(cd[:name][1..-2])
294
+ else
295
+ name << "[#{HTTP.decode(cd[:name][1..-2])}]"
296
+ end
297
+ if headers["content-type"]
298
+ HTTP.add_param_to_hash "#{name}[data]", part, @parser_data[:params]
299
+ HTTP.add_param_to_hash "#{name}[type]", HTTP.make_utf8!(headers["content-type"]), @parser_data[:params]
300
+ cd.each {|k,v| HTTP.add_param_to_hash "#{name}[#{k.to_s}]", HTTP.make_utf8!(v[1..-2]), @parser_data[:params] unless k == :name}
301
+ else
302
+ HTTP.add_param_to_hash name, HTTP.decode(part, :utf8), @parser_data[:params]
303
+ end
304
+ true
305
+ end
306
+ end
307
+ end
308
+
309
+ ## Heroku/extra headers info
310
+
311
+ # All headers are considered to be case-insensitive, as per HTTP Specification.
312
+ # X-Forwarded-For: the originating IP address of the client connecting to the Heroku router
313
+ # X-Forwarded-Proto: the originating protocol of the HTTP request (example: https)
314
+ # X-Forwarded-Port: the originating port of the HTTP request (example: 443)
315
+ # X-Request-Start: unix timestamp (milliseconds) when the request was received by the router
316
+ # X-Request-Id: the Heroku HTTP Request ID
317
+ # Via: a code name for the Heroku router
318
+
@@ -0,0 +1,133 @@
1
+ module Plezi
2
+
3
+ # class is the base for the HTTP server.
4
+ #
5
+ # the class is initialized with a TCP/IP connection socket and starts
6
+ # an event driven cycle using the `EventStack.push` and `EventStack.reverse_async`.
7
+ #
8
+ # to-do: fox logging.
9
+ class HTTPRequest < Hash
10
+
11
+ def initialize service
12
+ super()
13
+ self[:plezi_service] = service
14
+ end
15
+
16
+ public
17
+
18
+ # the request's headers
19
+ def headers
20
+ self.select {|k,v| k.is_a? String }
21
+ end
22
+ # the request's method (GET, POST... etc').
23
+ def request_method
24
+ self[:method]
25
+ end
26
+ # set request's method (GET, POST... etc').
27
+ def request_method= value
28
+ self[:method] = value
29
+ end
30
+ # the parameters sent by the client.
31
+ def params
32
+ self[:params]
33
+ end
34
+ # the cookies sent by the client.
35
+ def cookies
36
+ self[:cookies]
37
+ end
38
+
39
+ # the query string
40
+ def query
41
+ self[:query]
42
+ end
43
+
44
+ # the original (frozen) path (resource requested).
45
+ def original_path
46
+ self[:original_path]
47
+ end
48
+
49
+ # the requested path (rewritable).
50
+ def path
51
+ self[:path]
52
+ end
53
+ def path=(new_path)
54
+ self[:path] = new_path
55
+ end
56
+
57
+ # the base url ([http/https]://host[:port])
58
+ def base_url switch_protocol = nil
59
+ "#{switch_protocol || self[:requested_protocol]}://#{self[:host_name]}#{self[:port]? ":#{self[:port]}" : ''}"
60
+ end
61
+
62
+ # the service (socket wrapper) that answered this request
63
+ def service
64
+ self[:plezi_service]
65
+ end
66
+ # the protocol managing this request
67
+ def protocol
68
+ self[:requested_protocol]
69
+ end
70
+ # the handler dealing with this request
71
+ def handler
72
+ self[:plezi_service].handler
73
+ end
74
+
75
+ # method recognition
76
+
77
+ # returns true of the method == GET
78
+ def get?
79
+ self[:method] == 'GET'
80
+ end
81
+ # returns true of the method == HEAD
82
+ def head?
83
+ self[:method] == 'HEAD'
84
+ end
85
+ # returns true of the method == POST
86
+ def post?
87
+ self[:method] == 'POST'
88
+ end
89
+ # returns true of the method == PUT
90
+ def put?
91
+ self[:method] == 'PUT'
92
+ end
93
+ # returns true of the method == DELETE
94
+ def delete?
95
+ self[:method] == 'DELETE'
96
+ end
97
+ # returns true of the method == TRACE
98
+ def trace?
99
+ self[:method] == 'TRACE'
100
+ end
101
+ # returns true of the method == OPTIONS
102
+ def options?
103
+ self[:method] == 'OPTIONS'
104
+ end
105
+ # returns true of the method == CONNECT
106
+ def connect?
107
+ self[:method] == 'CONNECT'
108
+ end
109
+ # returns true of the method == PATCH
110
+ def patch?
111
+ self[:method] == 'PATCH'
112
+ end
113
+
114
+ end
115
+ end
116
+
117
+
118
+ ######
119
+ ## example requests
120
+
121
+ # GET / HTTP/1.1
122
+ # Host: localhost:2000
123
+ # Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
124
+ # Cookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
125
+ # 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
126
+ # Accept-Language: en-us
127
+ # Accept-Encoding: gzip, deflate
128
+ # Connection: keep-alive
129
+ #
130
+ # => "GET / HTTP/1.1\n\rHost: localhost:2000\n\rAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n\rCookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\n\rUser-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\n\rAccept-Language: en-us\n\rAccept-Encoding: gzip, deflate\n\rConnection: keep-alive\n\r\n\r"
131
+ # => "GET /people/are/friendly HTTP/1.1\n\rHost: localhost:2000\n\rAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n\rCookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\n\rUser-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\n\rAccept-Language: en-us\n\rAccept-Encoding: gzip, deflate\n\rConnection: keep-alive\n\r\n\r"
132
+ # => "GET /girls?sexy=true HTTP/1.1\n\rHost: localhost:2000\n\rAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n\rCookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\n\rUser-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\n\rAccept-Language: en-us\n\rAccept-Encoding: gzip, deflate\n\rConnection: keep-alive\n\r\n\r"
133
+ # chunked => "17d; ignored data=boaz\r\nGET / HTTP/1.1\r\nHost: localhost:3000\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nCookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\r\nUser-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\r\nAccept-Language: en-us\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nc\r\n\r\nparsed as:\r\n\r\n4f4\r\n{:raw=>\"GET / HTTP/1.1\\r\\nHost: localhost:3000\\r\\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\\r\\nCookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\\r\\nUser-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\\r\\nAccept-Language: en-us\\r\\nAccept-Encoding: gzip, deflate\\r\\nConnection: keep-alive\\r\\n\\r\\n\", :plezi_service=>#<Plezi::BasicService:0x007ff4daab5ac8 @handler=Plezi::HTTPEcho, @socket=#<TCPSocket:fd 9>, @in_que=\"\", @out_que=[], @locker=#<Mutex:0x007ff4daab5a28>, @parameters={:protocol=>Plezi::HTTPProtocol, :handler=>Plezi::HTTPEcho}, @protocol=Plezi::HTTPProtocol>, :params=>{}, :cookies=>{:user_token=>\"2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\"}, :method=>\"GET\", :query=>\"/\", :original_path=>\"/\", :path=>\"/\", :version=>\"HTTP/1.1\", \"host\"=>\"localhost:3000\", \"accept\"=>\"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\", \"cookie\"=>\"user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w\", \"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\", \"accept-language\"=>\"en-us\", \"accept-encoding\"=>\"gzip, deflate\", \"connection\"=>\"keep-alive\"}\r\n0\r\n\r\n"