iodine 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

@@ -0,0 +1,105 @@
1
+ module Iodine
2
+
3
+ module Base
4
+ # This (will be) a Rack handler for the Iodine HTTP server.
5
+ module Rack
6
+ module_function
7
+ def run(app, options = {})
8
+ @app = app
9
+
10
+ Iodine.protocol ||= Iodine::HTTP
11
+ Iodine.threads = 18
12
+ @pre_rack_handler = Iodine.protocol.on_http
13
+ Iodine.protocol.on_http self
14
+ true
15
+ end
16
+ def call request, response
17
+ if @pre_rack_handler
18
+ tmp = @pre_rack_handler.call(request, response)
19
+ return tmp if tmp
20
+ end
21
+ # response.quite!
22
+ res = @app.call rack_env(request)
23
+ raise "Rack app returned an unexpected value: #{res.to_s}" unless res && res.is_a?(Array)
24
+ response.status = res[0]
25
+ response.headers.clear
26
+ response.headers.update res[1]
27
+ response.body = res[2]
28
+ response.raw_cookies.clear
29
+ response.headers['Set-Cookie'] = response.headers.delete('Set-Cookie').split("\n").join("\r\nSet-Cookie: ") if response.headers['Set-Cookie']
30
+ response.request[:no_log] = true
31
+ true
32
+ end
33
+
34
+ protected
35
+
36
+
37
+ def self.rack_env request
38
+ env = RACK_DICTIONARY.dup
39
+ # env['pl.request'] = @request
40
+ # env.each {|k, v| env[k] = @request[v] if v.is_a?(Symbol)}
41
+ RACK_ADDON.each {|k, v| env[k] = (request[v].is_a?(String) ? ( request[v].frozen? ? request[v].dup.force_encoding('ASCII-8BIT') : request[v].force_encoding('ASCII-8BIT') ): request[v])}
42
+ request.each {|k, v| env["HTTP_#{k.upcase.tr('-', '_')}"] = v if k.is_a?(String) }
43
+ env['rack.input'.freeze] ||= StringIO.new(''.force_encoding('ASCII-8BIT'.freeze))
44
+ env['CONTENT_LENGTH'.freeze] = env.delete 'HTTP_CONTENT_LENGTH'.freeze if env['HTTP_CONTENT_LENGTH'.freeze]
45
+ env['CONTENT_TYPE'.freeze] = env.delete 'HTTP_CONTENT_TYPE'.freeze if env['HTTP_CONTENT_TYPE'.freeze]
46
+ env['HTTP_VERSION'.freeze] = "HTTP/#{request[:version].to_s}"
47
+ env['QUERY_STRING'.freeze] ||= ''
48
+ env['rack.errors'.freeze] = StringIO.new('')
49
+ env
50
+ end
51
+
52
+ RACK_ADDON = {
53
+ 'PATH_INFO' => :original_path,
54
+ 'REQUEST_PATH' => :path,
55
+ 'QUERY_STRING' => :quary_params,
56
+ 'SERVER_NAME' => :host_name,
57
+ 'REQUEST_URI' => :query,
58
+ 'SERVER_PORT' => :port,
59
+ 'REMOTE_ADDR' => :client_ip,
60
+ # 'gr.params' => :params,
61
+ # 'gr.cookies' => :cookies,
62
+ 'REQUEST_METHOD' => :method,
63
+ 'rack.url_scheme' => :scheme,
64
+ 'rack.input' => :rack_input
65
+ }
66
+
67
+ RACK_DICTIONARY = {
68
+ "GATEWAY_INTERFACE" =>"CGI/1.2",
69
+ 'SERVER_SOFTWARE' => "Iodine v. #{Iodine::VERSION}",
70
+ 'SCRIPT_NAME' => ''.force_encoding('ASCII-8BIT'),
71
+ 'rack.logger' => Iodine,
72
+ 'rack.multithread' => true,
73
+ 'rack.multiprocess' => false,
74
+ # 'rack.hijack?' => false,
75
+ # 'rack.hijack_io' => nil,
76
+ 'rack.run_once' => false
77
+ }
78
+ RACK_DICTIONARY['rack.version'] = ::Rack.version.split('.') if defined?(::Rack)
79
+ HASH_SYM_PROC = Proc.new {|h,k| k = (Symbol === k ? k.to_s : k.to_s.to_sym); h.has_key?(k) ? h[k] : (h["gr.#{k.to_s}"] if h.has_key?("gr.#{k.to_s}") ) }
80
+ end
81
+ end
82
+ end
83
+
84
+ # ENV["RACK_HANDLER"] = 'grhttp'
85
+
86
+ # make Iodine the default fallback position for Rack.
87
+ begin
88
+ require 'rack/handler'
89
+ Rack::Handler::WEBrick = Rack::Handler.get(:iodine)
90
+ rescue Exception => e
91
+
92
+ end
93
+ ::Rack::Handler.register( 'iodine', 'Iodine::Base::Rack') if defined?(::Rack)
94
+
95
+ ######
96
+ ## example requests
97
+
98
+ # GET /stream HTTP/1.1
99
+ # Host: localhost:3000
100
+ # Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
101
+ # Cookie: user_token=2INa32_vDgx8Aa1qe43oILELpSdIe9xwmT8GTWjkS-w
102
+ # 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
103
+ # Accept-Language: en-us
104
+ # Accept-Encoding: gzip, deflate
105
+ # Connection: keep-alive
@@ -0,0 +1,413 @@
1
+ module Iodine
2
+
3
+ class Http < ::Iodine::Protocol
4
+
5
+ # This class is the part of the Iodine server.
6
+ # The request object is a Hash and the Request provides
7
+ # simple shortcuts and access to the request's Hash data.
8
+ #
9
+ #
10
+ # An Http Request
11
+ class Request < Hash
12
+
13
+ # Sets magic cookies - NOT part of the API.
14
+ #
15
+ # magic cookies keep track of both incoming and outgoing cookies, setting the response's cookies as well as the combined cookie respetory (held by the request object).
16
+ #
17
+ # use only the []= for magic cookies. merge and update might not set the response cookies.
18
+ class Cookies < ::Hash
19
+ # sets the Magic Cookie's controller object (which holds the response object and it's `set_cookie` method).
20
+ def set_response response
21
+ @response = response
22
+ end
23
+ # overrides the []= method to set the cookie for the response (by encoding it and preparing it to be sent), as well as to save the cookie in the combined cookie jar (unencoded and available).
24
+ def []= key, val
25
+ return super unless @response
26
+ if key.is_a?(Symbol) && self.has_key?( key.to_s)
27
+ key = key.to_s
28
+ elsif self.has_key?( key.to_s.to_sym)
29
+ key = key.to_s.to_sym
30
+ end
31
+ @response.set_cookie key, (val ? val.to_s.dup : nil)
32
+ super
33
+ end
34
+ end
35
+
36
+ def initialize io = nil
37
+ super()
38
+ self[:io] = io if io
39
+ self[:cookies] = Cookies.new
40
+ self[:params] = {}
41
+ @parsed = false
42
+ end
43
+
44
+ public
45
+
46
+ # the request's headers
47
+ def headers
48
+ self.select {|k,v| k.is_a? String }
49
+ end
50
+ # the request's method (GET, POST... etc').
51
+ def request_method
52
+ self[:method]
53
+ end
54
+ # set request's method (GET, POST... etc').
55
+ def request_method= value
56
+ self[:method] = value
57
+ end
58
+ # the parameters sent by the client.
59
+ def params
60
+ self[:params]
61
+ end
62
+ # the cookies sent by the client.
63
+ def cookies
64
+ self[:cookies]
65
+ end
66
+
67
+ # the query string
68
+ def query
69
+ self[:query]
70
+ end
71
+
72
+ # the original (frozen) path (resource requested).
73
+ def original_path
74
+ self[:original_path]
75
+ end
76
+
77
+ # the requested path (rewritable).
78
+ def path
79
+ self[:path]
80
+ end
81
+ def path=(new_path)
82
+ self[:path] = new_path
83
+ end
84
+
85
+ # The HTTP version for this request
86
+ def version
87
+ self[:version]
88
+ end
89
+
90
+ # the base url ([http/https]://host[:port])
91
+ def base_url switch_scheme = nil
92
+ "#{switch_scheme || self[:scheme]}://#{self[:host_name]}#{self[:port]? ":#{self[:port]}" : ''}"
93
+ end
94
+
95
+ # the request's url, without any GET parameters ([http/https]://host[:port]/path)
96
+ def request_url switch_scheme = nil
97
+ "#{base_url switch_scheme}#{self[:original_path]}"
98
+ end
99
+
100
+ # the protocol's scheme (http/https/ws/wss) managing this request
101
+ def scheme
102
+ self[:scheme]
103
+ end
104
+
105
+ # @return [true, false] returns true if the requested was an SSL protocol (true also if the connection is clear-text behind an SSL Proxy, such as with some PaaS providers).
106
+ def ssl?
107
+ self[:io].ssl? || self[:scheme] == 'https'.freeze || self[:scheme] == 'wss'.freeze
108
+ end
109
+ alias :secure? :ssl?
110
+
111
+ # @return [Iodine::Http, Iodine::Http2, Iodine::Websockets] the Protocol used for the request.
112
+ def io
113
+ self[:io]
114
+ end
115
+
116
+ # @return [Hash like storage] Returns the session storage object IF a session was already initialized (use the response to initialize a session).
117
+ def session
118
+ self[:session]
119
+ end
120
+
121
+
122
+ # method recognition
123
+
124
+ HTTP_GET = 'GET'.freeze
125
+ # returns true of the method == GET
126
+ def get?
127
+ self[:method] == HTTP_GET
128
+ end
129
+
130
+ HTTP_HEAD = 'HEAD'.freeze
131
+ # returns true of the method == HEAD
132
+ def head?
133
+ self[:method] == HTTP_HEAD
134
+ end
135
+ HTTP_POST = 'POST'.freeze
136
+ # returns true of the method == POST
137
+ def post?
138
+ self[:method] == HTTP_POST
139
+ end
140
+ HTTP_PUT = 'PUT'.freeze
141
+ # returns true of the method == PUT
142
+ def put?
143
+ self[:method] == HTTP_PUT
144
+ end
145
+ HTTP_DELETE = 'DELETE'.freeze
146
+ # returns true of the method == DELETE
147
+ def delete?
148
+ self[:method] == HTTP_DELETE
149
+ end
150
+ HTTP_TRACE = 'TRACE'.freeze
151
+ # returns true of the method == TRACE
152
+ def trace?
153
+ self[:method] == HTTP_TRACE
154
+ end
155
+ HTTP_OPTIONS = 'OPTIONS'.freeze
156
+ # returns true of the method == OPTIONS
157
+ def options?
158
+ self[:method] == HTTP_OPTIONS
159
+ end
160
+ HTTP_CONNECT = 'CONNECT'.freeze
161
+ # returns true of the method == CONNECT
162
+ def connect?
163
+ self[:method] == HTTP_CONNECT
164
+ end
165
+ HTTP_PATCH = 'PATCH'.freeze
166
+ # returns true of the method == PATCH
167
+ def patch?
168
+ self[:method] == HTTP_PATCH
169
+ end
170
+ HTTP_CTYPE = 'content-type'.freeze; HTTP_JSON = /application\/json/
171
+ # returns true if the request is of type JSON.
172
+ def json?
173
+ self[HTTP_CTYPE] =~ HTTP_JSON
174
+ end
175
+ HTTP_XML = /text\/xml/
176
+ # returns true if the request is of type XML.
177
+ def xml?
178
+ self[HTTP_CTYPE].match HTTP_XML
179
+ end
180
+ # returns true if this is a websocket upgrade request
181
+ def websocket?
182
+ @is_websocket ||= (self['upgrade'.freeze] && self['upgrade'.freeze].to_s =~ /websocket/i.freeze && self['connection'.freeze].to_s =~ /upg/i.freeze && true)
183
+ end
184
+ alias :upgrade? :websocket?
185
+
186
+ # parses an HTTP request (quary, body data etc')
187
+ def self.parse request
188
+ # if m = request[:query].match /(([a-z0-9A-Z]+):\/\/)?(([^\/\:]+))?(:([0-9]+))?([^\?\#]*)(\?([^\#]*))?/
189
+ # request[:requested_protocol] = m[1] || request['x-forwarded-proto'] || ( request[:io].ssl? ? 'https' : 'http')
190
+ # request[:host_name] = m[4] || (request['host'] ? request['host'].match(/^[^:]*/).to_s : nil)
191
+ # request[:port] = m[6] || (request['host'] ? request['host'].match(/:([0-9]*)/).to_a[1] : nil)
192
+ # request[:original_path] = HTTP.decode(m[7], :uri) || '/'
193
+ # request['host'] ||= "#{request[:host_name]}:#{request[:port]}"
194
+
195
+ # # parse query for params - m[9] is the data part of the query
196
+ # if m[9]
197
+ # extract_params m[9].split(/[&;]/), request[:params]
198
+ # end
199
+ # end
200
+ return request if request[:client_ip]
201
+ request[:client_ip] = request['x-forwarded-for'.freeze].to_s.split(/,[\s]?/)[0] || (request[:io].io.to_io.remote_address.ip_address) rescue 'unknown IP'.freeze
202
+ request[:version] ||= '1'
203
+
204
+ request[:scheme] ||= request['x-forwarded-proto'.freeze] ? request['x-forwarded-proto'.freeze].downcase : ( request[:io].ssl? ? 'https'.freeze : 'http'.freeze)
205
+ tmp = (request['host'.freeze] || request[:authority] || ''.freeze).split(':')
206
+ request[:host_name] = tmp[0]
207
+ request[:port] = tmp[1] || nil
208
+
209
+ tmp = (request[:query] ||= request[:path] ).split('?', 2)
210
+ request[:path] = tmp[0].chomp('/')
211
+ request[:original_path] = tmp[0].freeze
212
+ request[:quary_params] = tmp[1]
213
+ extract_params tmp[1].split(/[&;]/.freeze), (request[:params] ||= {}) if tmp[1]
214
+
215
+ if request['cookie'.freeze]
216
+ if request['cookie'.freeze].is_a?(Array)
217
+ tmp = []
218
+ request['cookie'.freeze].each {|s| s.split(/[;,][\s]?/.freeze).each { |c| tmp << c } }
219
+ request['cookie'.freeze] = tmp
220
+ extract_header tmp, request.cookies
221
+ else
222
+ extract_header request['cookie'.freeze].split(/[;,][\s]?/.freeze), request.cookies
223
+ end
224
+ elsif request['set-cookie'.freeze]
225
+ request['set-cookie'.freeze] = [ request['set-cookie'.freeze] ] unless request['set-cookie'.freeze].is_a?(Array)
226
+ tmp = []
227
+ request['set-cookie'.freeze].each {|s| tmp << s.split(/[;][\s]?/.freeze)[0] }
228
+ request['set-cookie'.freeze] = tmp
229
+ extract_header tmp, request.cookies
230
+ end
231
+
232
+ read_body request if request[:body]
233
+
234
+ request
235
+ end
236
+
237
+ # re-encodes a string into UTF-8
238
+ def self.make_utf8!(string, encoding= ::Encoding::UTF_8)
239
+ return false unless string
240
+ string.force_encoding(::Encoding::ASCII_8BIT).encode!(encoding, ::Encoding::ASCII_8BIT, invalid: :replace, undef: :replace, replace: ''.freeze) unless string.force_encoding(encoding).valid_encoding?
241
+ string
242
+ end
243
+
244
+ # re-encodes a string into UTF-8
245
+ def self.try_utf8!(string, encoding= ::Encoding::UTF_8)
246
+ return false unless string
247
+ string.force_encoding(::Encoding::ASCII_8BIT) unless string.force_encoding(encoding).valid_encoding?
248
+ string
249
+ end
250
+
251
+ # encodes URL data.
252
+ def self.encode_url str
253
+ (str.to_s.gsub(/[^a-z0-9\*\.\_\-]/i) {|m| '%%%02x'.freeze % m.ord }).force_encoding(::Encoding::ASCII_8BIT)
254
+ end
255
+
256
+ # Adds paramaters to a Hash object, according to the Iodine's server conventions.
257
+ def self.add_param_to_hash name, value, target
258
+ begin
259
+ c = target
260
+ val = rubyfy! value
261
+ a = name.chomp('[]'.freeze).split('['.freeze)
262
+
263
+ a[0...-1].inject(target) do |h, n|
264
+ n.chomp!(']'.freeze);
265
+ n.strip!;
266
+ raise "malformed parameter name for #{name}" if n.empty?
267
+ n = (n.to_i.to_s == n) ? n.to_i : n.to_sym
268
+ c = (h[n] ||= {})
269
+ end
270
+ n = a.last
271
+ n.chomp!(']'); n.strip!;
272
+ n = n.empty? ? nil : ( (n.to_i.to_s == n) ? n.to_i : n.to_sym )
273
+ if n
274
+ if c[n]
275
+ c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
276
+ else
277
+ c[n] = val
278
+ end
279
+ else
280
+ if c[n]
281
+ c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
282
+ else
283
+ c[n] = [val]
284
+ end
285
+ end
286
+ val
287
+ rescue => e
288
+ Iodine.error e
289
+ Iodine.error "(Silent): parameters parse error for #{name} ... maybe conflicts with a different set?"
290
+ target[name] = val
291
+ end
292
+ end
293
+
294
+ # extracts parameters from the query
295
+ def self.extract_params data, target_hash
296
+ data.each do |set|
297
+ list = set.split('='.freeze, 2)
298
+ list.each {|s| uri_decode!(s) if s}
299
+ add_param_to_hash list.shift, list.shift, target_hash
300
+ end
301
+ end
302
+ # decode form / uri data (including the '+' sign as a space (%20) replacement).
303
+ def self.uri_decode! s
304
+ s.gsub!('+'.freeze, '%20'.freeze); s.gsub!(/\%[0-9a-f]{2}/i) {|m| m[1..2].to_i(16).chr}; s.gsub!(/&#[0-9]{4};/i) {|m| [m[2..5].to_i].pack 'U'.freeze }; s
305
+ end
306
+ # extracts parameters from header data
307
+ def self.extract_header data, target_hash
308
+ data.each do |set|
309
+ list = set.split('='.freeze, 2)
310
+ list.each {|s| form_decode!(s) if s}
311
+ add_param_to_hash list.shift, list.shift, target_hash
312
+ end
313
+ end
314
+ # decode percent-encoded data (excluding the '+' sign for encoding).
315
+ def self.form_decode! s
316
+ s.gsub!(/\%[0-9a-f]{2}/i) {|m| m[1..2].to_i(16).chr}; s.gsub!(/&#[0-9]{4};/i) {|m| [m[2..5].to_i].pack 'U'.freeze }; s
317
+ end
318
+ # Changes String to a Ruby Object, if it's a special string...
319
+ def self.rubyfy!(string)
320
+ return string unless string.is_a?(String)
321
+ try_utf8! string
322
+ if string == 'true'.freeze
323
+ string = true
324
+ elsif string == 'false'.freeze
325
+ string = false
326
+ elsif string.to_i.to_s == string
327
+ string = string.to_i
328
+ end
329
+ string
330
+ end
331
+
332
+ # read the body's data and parse any incoming data.
333
+ def self.read_body request
334
+ # save body for Rack, if applicable
335
+ request[:rack_input] = StringIO.new(request[:body].dup.force_encoding(::Encoding::ASCII_8BIT)) if request[:io].params[:http_handler] == ::Iodine::Base::Rack
336
+ # parse content
337
+ case request['content-type'.freeze].to_s
338
+ when /x-www-form-urlencoded/
339
+ extract_params request.delete(:body).split(/[&;]/), request[:params] #, :form # :uri
340
+ when /multipart\/form-data/
341
+ read_multipart request, request, request.delete(:body)
342
+ when /text\/xml/
343
+ # to-do support xml?
344
+ make_utf8! request[:body]
345
+ nil
346
+ when /application\/json/
347
+ JSON.parse(make_utf8! request[:body]).each {|k, v| add_param_to_hash k, v, request[:params]} rescue true
348
+ end
349
+ end
350
+
351
+ # parse a mime/multipart body or part.
352
+ def self.read_multipart request, headers, part, name_prefix = ''
353
+ if headers['content-type'].to_s =~ /multipart/i
354
+ tmp = {}
355
+ extract_header headers['content-type'].split(/[;,][\s]?/), tmp
356
+ boundry = tmp[:boundary]
357
+ if tmp[:name]
358
+ if name_prefix.empty?
359
+ name_prefix << tmp[:name]
360
+ else
361
+ name_prefix << "[#{tmp[:name]}]"
362
+ end
363
+ end
364
+ part.split(/([\r]?\n)?--#{boundry}(--)?[\r]?\n/).each do |p|
365
+ unless p.strip.empty? || p=='--'.freeze
366
+ # read headers
367
+ h = {}
368
+ m = p.slice! /\A[^\r\n]*[\r]?\n/
369
+ while m
370
+ break if m =~ /\A[\r]?\n/
371
+ m = m.match(/^([^:]+):[\s]?([^\r\n]+)/)
372
+ h[m[1].downcase] = m[2] if m
373
+ m = p.slice! /\A[^\r\n]*[\r]?\n/
374
+ end
375
+ # send headers and body to be read
376
+ read_multipart request, h, p, name_prefix
377
+ end
378
+ end
379
+ return
380
+ end
381
+
382
+ # require a part body to exist (data exists) for parsing
383
+ return true if part.to_s.empty?
384
+
385
+ # convert part to `charset` if charset is defined?
386
+
387
+ if !headers['content-disposition'.freeze]
388
+ Iodine.error "Wrong multipart format with headers: #{headers} and body: #{part}"
389
+ return
390
+ end
391
+
392
+ cd = {}
393
+
394
+ extract_header headers['content-disposition'.freeze].split(/[;,][\s]?/), cd
395
+
396
+ if name_prefix.empty?
397
+ name = cd[:name][1..-2]
398
+ else
399
+ name = "#{name_prefix}[cd[:name][1..-2]}]"
400
+ end
401
+ if headers['content-type'.freeze]
402
+ add_param_to_hash "#{name}[data]", part, request[:params]
403
+ add_param_to_hash "#{name}[type]", make_utf8!(headers['content-type'.freeze]), request[:params]
404
+ cd.each {|k,v| add_param_to_hash "#{name}[#{k.to_s}]", make_utf8!(v[1..-2].to_s), request[:params] unless k == :name || !v}
405
+ else
406
+ add_param_to_hash name, uri_decode!(part), request[:params]
407
+ end
408
+ true
409
+ end
410
+
411
+ end
412
+ end
413
+ end