plezi 0.7.0

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