Class: Iodine::Http::Request

Inherits:
Hash
  • Object
show all
Defined in:
lib/iodine/http/request.rb

Overview

This class is the part of the Iodine server. The request object is a Hash and the Request provides simple shortcuts and access to the request’s Hash data.

An Http Request

Defined Under Namespace

Classes: Cookies

Constant Summary collapse

HTTP_GET =

method recognition

'GET'.freeze
HTTP_HEAD =
'HEAD'.freeze
HTTP_POST =
'POST'.freeze
HTTP_PUT =
'PUT'.freeze
HTTP_DELETE =
'DELETE'.freeze
HTTP_TRACE =
'TRACE'.freeze
HTTP_OPTIONS =
'OPTIONS'.freeze
HTTP_CONNECT =
'CONNECT'.freeze
HTTP_PATCH =
'PATCH'.freeze
HTTP_CTYPE =
'content-type'.freeze
HTTP_JSON =
/application\/json/.freeze
HTTP_XML =
/text\/xml/.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io = nil) ⇒ Request

Returns a new instance of Request.



45
46
47
48
49
50
51
# File 'lib/iodine/http/request.rb', line 45

def initialize io = nil
	super()
	self[:io] = io if io
	self[:cookies] = Cookies.new
	self[:params] = {}
	@parsed = false
end

Class Method Details

.add_param_to_hash(name, value, target, &block) ⇒ Object

Adds paramaters to a Hash object, according to the Iodine’s server conventions.



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/iodine/http/request.rb', line 280

def self.add_param_to_hash name, value, target, &block
	begin
		c = target
		val = rubyfy! value
		a = name.chomp('[]'.freeze).split('['.freeze)

		a[0...-1].inject(target) do |h, n|
			n.chomp!(']'.freeze);
			n.strip!;
			raise "malformed parameter name for #{name}" if n.empty?
			n = (n.to_i.to_s == n) ?  n.to_i : n.to_sym            
			c = (h[n] ||= {})
		end
		n = a.last
		n.chomp!(']'.freeze); n.strip!;
		n = n.empty? ? nil : ( (n.to_i.to_s == n) ?  n.to_i : n.to_sym )
		if n
			if c[n]
				c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
			else
				c[n] = val
			end
			c.default_proc = block if block
		else
			if c[n]
				c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
			else
				c[n] = [val]
			end
		end
		val
	rescue => e
		Iodine.error e
		Iodine.error "(Silent): parameters parse error for #{name} ... maybe conflicts with a different set?"
		target[name] = val
	end
end

.encode_url(str) ⇒ Object

encodes URL data.



275
276
277
# File 'lib/iodine/http/request.rb', line 275

def self.encode_url str
	(str.to_s.gsub(/[^a-z0-9\*\.\_\-]/i.freeze) {|m| '%%%02x'.freeze % m.ord }).force_encoding(::Encoding::ASCII_8BIT)
end

.extract_header(data, target_hash) ⇒ Object

extracts parameters from header data



331
332
333
334
335
336
337
# File 'lib/iodine/http/request.rb', line 331

def self.extract_header data, target_hash
	data.each do |set|
		list = set.split('='.freeze, 2)
		list.each {|s| form_decode!(s) if s}
		add_param_to_hash list.shift, list.shift, target_hash
	end
end

.extract_params(data, target_hash) ⇒ Object

extracts parameters from the query



319
320
321
322
323
324
325
# File 'lib/iodine/http/request.rb', line 319

def self.extract_params data, target_hash
	data.each do |set|
		list = set.split('='.freeze, 2)
		list.each {|s| uri_decode!(s) if s}
		add_param_to_hash list.shift, list.shift, target_hash
	end
end

.form_decode!(s) ⇒ Object

decode percent-encoded data (excluding the ‘+’ sign for encoding).



339
340
341
# File 'lib/iodine/http/request.rb', line 339

def self.form_decode! s
	s.gsub!(/\%[0-9a-f]{2}/i.freeze) {|m| m[1..2].to_i(16).chr}; s.gsub!(/&#[0-9]{4};/i.freeze) {|m| [m[2..5].to_i].pack 'U'.freeze }; s
end

.make_utf8!(string, encoding = ::Encoding::UTF_8) ⇒ Object

re-encodes a string into UTF-8



261
262
263
264
265
# File 'lib/iodine/http/request.rb', line 261

def self.make_utf8!(string, encoding= ::Encoding::UTF_8)
	return false unless string
	string.force_encoding(::Encoding::ASCII_8BIT).encode!(encoding, ::Encoding::ASCII_8BIT, invalid: :replace, undef: :replace, replace: ''.freeze) unless string.force_encoding(encoding).valid_encoding?
	string
end

.parse(request) ⇒ Object

parses an HTTP request (quary, body data etc’)



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/iodine/http/request.rb', line 200

def self.parse request
	# if m = request[:query].match /(([a-z0-9A-Z]+):\/\/)?(([^\/\:]+))?(:([0-9]+))?([^\?\#]*)(\?([^\#]*))?/
		# request[:requested_protocol] = m[1] || request['x-forwarded-proto'] || ( request[:io].ssl? ? 'https' : 'http')
		# request[:host_name] = m[4] || (request['host'] ? request['host'].match(/^[^:]*/).to_s : nil)
		# request[:port] = m[6] || (request['host'] ? request['host'].match(/:([0-9]*)/).to_a[1] : nil)
		# request[:original_path] = HTTP.decode(m[7], :uri) || '/'
		# request['host'] ||= "#{request[:host_name]}:#{request[:port]}"

		# # parse query for params - m[9] is the data part of the query
		# if m[9]
		# 	extract_params m[9].split(/[&;]/), request[:params]
		# end
	# end
	return request if request[:client_ip]

	request.delete :headers_size

	# 2014 RFC 7239 Forwarded: for=192.0.2.60; proto=http; by=203.0.113.43
	if tmp = request['forwarded'.freeze]
		tmp = tmp.join(';'.freeze) if tmp.is_a?(Array)
		tmp.match(/proto=([^\s;]+)/i.freeze).tap {|m| request[:scheme] = m[1].downcase if m } unless request[:scheme]
		tmp.match(/for=[\[]?[\"]?([^\s;\",]+)/i.freeze).tap {|m| request[:client_ip] = m[1] if m } unless request[:client_ip]
	end

	request[:client_ip] ||= (request['x-forwarded-for'.freeze].to_s.match(/[^\s\[\"\,]+/.freeze) || request[:io].io.to_io.remote_address.ip_address).to_s rescue 'unknown'.freeze
	request[:version] ||= '1'.freeze

	request[:scheme] ||= request['x-forwarded-proto'.freeze] ? request['x-forwarded-proto'.freeze].downcase : ( request[:io].ssl? ? 'https'.freeze : 'http'.freeze)
	tmp = (request['host'.freeze] || request[:authority] || ''.freeze).split(':'.freeze)
	request[:host_name] = tmp[0]
	request[:port] = tmp[1]

	tmp = (request[:query] ||= request[:path] ).split('?'.freeze, 2)
	request[:path] = tmp[0].chomp('/'.freeze)
	request[:original_path] = tmp[0].freeze
	request[:quary_params] = tmp[1]
	extract_params tmp[1].split(/[&;]/.freeze), (request[:params] ||= {}) if tmp[1]

	if request['cookie'.freeze]
		if request['cookie'.freeze].is_a?(Array)
			tmp = []
			request['cookie'.freeze].each {|s| s.split(/[;,][\s]?/.freeze).each { |c| tmp << c } }
			request['cookie'.freeze] = tmp
			extract_header tmp, request.cookies
		else
			extract_header request['cookie'.freeze].split(/[;,][\s]?/.freeze), request.cookies
		end
	elsif request['set-cookie'.freeze]
		request['set-cookie'.freeze] = [ request['set-cookie'.freeze] ] unless request['set-cookie'.freeze].is_a?(Array)
		tmp = []
		request['set-cookie'.freeze].each {|s| tmp << s.split(/[;][\s]?/.freeze)[0] }
		request['set-cookie'.freeze] = tmp
		extract_header tmp, request.cookies
	end

	read_body request if request[:body]

	request
end

.read_body(request) ⇒ Object

read the body’s data and parse any incoming data.



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/iodine/http/request.rb', line 357

def self.read_body request
	# save body for Rack, if applicable
	# request[:rack_input] = request[:body]  if ::Iodine::Http.on_http == ::Iodine::Http::Rack
	# parse content
	request[:body].rewind
	case request['content-type'.freeze].to_s
	when /x-www-form-urlencoded/.freeze
		extract_params request[:body].read.split(/[&;]/.freeze), request[:params] #, :form # :uri
	when /multipart\/form-data/.freeze
		read_multipart request, request
	when /text\/xml/.freeze
		# to-do support xml?
		# request[:xml] = make_utf8! request[:body].read
		nil
	when /application\/json/.freeze
		JSON.parse(make_utf8! request[:body].read).each {|k, v| add_param_to_hash k, v, request[:params]} rescue true
	end
	request[:body].rewind if request[:body]
end

.read_multipart(request, headers = {}, boundary = [], name_prefix = String.new) ⇒ Object

parse a mime/multipart body or part.



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/iodine/http/request.rb', line 378

def self.read_multipart request, headers = {}, boundary = [], name_prefix = String.new
	body = request[:body]
	return unless headers['content-type'.freeze].to_s =~ /multipart/i.freeze
	part_headers = {}
	extract_header headers['content-type'.freeze].split(/[;,][\s]?/.freeze), part_headers
	boundary << part_headers[:boundary]
	if part_headers[:name]
		if name_prefix.empty?
			name_prefix << part_headers[:name]
		else
			name_prefix << "[#{part_headers[:name]}]".freeze
		end
	end
	part_headers.delete :name
	part_headers.clear
	line = nil
	boundary_length = nil
	true until ( (line = body.gets) ) && line =~ /\A--(#{boundary.join '|'})(--)?[\r]?\n/
	until body.eof?
		return if line =~ /--[\r]?\n/.freeze
		return boundary.pop if boundary.count > 1 && line.match(/--(#{boundary.join '|'})/)[1] != boundary.last
		boundary_length = line.bytesize
		line = body.gets until line.nil? || line =~ /\:/.freeze
		until line.nil? || line =~ /^[\r]?\n/.freeze
			tmp = line.strip.split ':'.freeze, 2
			return Iodine.error "Http multipart parsing error (multipart header data malformed): #{line}" unless tmp && tmp.count == 2
			tmp[0].strip!; tmp[0].downcase!; tmp[1].strip!; 
			part_headers[tmp[0]] = tmp[1]
			line = body.gets
		end
		return if line.nil?
		if !part_headers['content-disposition'.freeze]
			Iodine.error "Wrong multipart format with headers: #{part_headers}"
			return
		end
		extract_header part_headers['content-disposition'.freeze].split(/[;,][\s]?/.freeze), part_headers
		if name_prefix.empty?
			name = part_headers[:name][1..-2]
		else
			name = "#{name_prefix}[part_headers[:name][1..-2]}]"
		end
		part_headers.delete :name

		start_part_pos = body.pos
		tmp = /\A--(#{boundary.join '|'})(--)?[\r]?\n/
		line.clear until ( (line = body.gets) &&  line =~ tmp)
		end_part_pos = (body.pos - line.bytesize) - 2
		new_part_pos = body.pos 
		body.pos = end_part_pos
		end_part_pos += 1 unless body.getc =~ /[\r\n]/.freeze
		end_part_pos += 1 unless body.getc =~ /[\r\n\-]/.freeze
		if part_headers['content-type'.freeze]
			if part_headers['content-type'.freeze] =~ /multipart/i.freeze
				body.pos = start_part_pos
				read_multipart request, part_headers, boundary, name_prefix
			else
				part_headers.delete 'content-disposition'.freeze
				add_param_to_hash "#{name}[type]", make_utf8!(part_headers['content-type'.freeze]), request[:params]
				part_headers.each {|k,v|  add_param_to_hash "#{name}[#{k.to_s}]", make_utf8!(v[0] == '"' ? v[1..-2].to_s : v), request[:params] if v}

				tmp = Tempfile.new 'upload'.freeze, encoding: 'binary'.freeze
				body.pos = start_part_pos
				((end_part_pos - start_part_pos)/65_536).to_i.times {tmp << body.read(65_536)} 
				tmp << body.read(end_part_pos - body.pos)
				add_param_to_hash "#{name}[size]", tmp.size, request[:params]
				add_param_to_hash "#{name}[file]", tmp, request[:params] do |hash, key|
					if key == :data || key == "data".freeze && hash.has_key?(:file) && hash[:file].is_a?(::Tempfile)
						hash[:file].rewind
						(hash[:data] = hash[:file].read)
					end
				end
				tmp.rewind
			end
		else
			body.pos = start_part_pos
			add_param_to_hash name, form_decode!( body.read(end_part_pos - start_part_pos) ), request[:params] 
		end
		body.pos = new_part_pos
	end

end

.rubyfy!(string) ⇒ Object

Changes String to a Ruby Object, if it’s a special string…



343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/iodine/http/request.rb', line 343

def self.rubyfy!(string)
	return string unless string.is_a?(String)
	try_utf8! string
	if string == 'true'.freeze
		string = true
	elsif string == 'false'.freeze
		string = false
	elsif string.to_i.to_s == string
		string = string.to_i
	end
	string
end

.try_utf8!(string, encoding = ::Encoding::UTF_8) ⇒ Object

re-encodes a string into UTF-8



268
269
270
271
272
# File 'lib/iodine/http/request.rb', line 268

def self.try_utf8!(string, encoding= ::Encoding::UTF_8)
	return false unless string
	string.force_encoding(::Encoding::ASCII_8BIT) unless string.force_encoding(encoding).valid_encoding?
	string
end

.uri_decode!(s) ⇒ Object

decode form / uri data (including the ‘+’ sign as a space (%20) replacement).



327
328
329
# File 'lib/iodine/http/request.rb', line 327

def self.uri_decode! s
	s.gsub!('+'.freeze, '%20'.freeze); s.gsub!(/\%[0-9a-f]{2}/i.freeze) {|m| m[1..2].to_i(16).chr}; s.gsub!(/&#[0-9]{4};/i.freeze) {|m| [m[2..5].to_i].pack 'U'.freeze }; s
end

Instance Method Details

#base_url(switch_scheme = nil) ⇒ Object

the base url ([http/https]://host)



100
101
102
# File 'lib/iodine/http/request.rb', line 100

def base_url switch_scheme = nil
	"#{switch_scheme || self[:scheme]}://#{self[:host_name]}#{self[:port]? ":#{self[:port]}" : ''.freeze}".freeze
end

#connect?Boolean

returns true of the method == CONNECT

Returns:

  • (Boolean)


175
176
177
# File 'lib/iodine/http/request.rb', line 175

def connect?
	self[:method] == HTTP_CONNECT
end

#cookiesObject

the cookies sent by the client.



72
73
74
# File 'lib/iodine/http/request.rb', line 72

def cookies
	self[:cookies]
end

#delete?Boolean

returns true of the method == DELETE

Returns:

  • (Boolean)


160
161
162
# File 'lib/iodine/http/request.rb', line 160

def delete?
	self[:method] == HTTP_DELETE
end

#get?Boolean

returns true of the method == GET

Returns:

  • (Boolean)


139
140
141
# File 'lib/iodine/http/request.rb', line 139

def get?
	self[:method] == HTTP_GET
end

#head?Boolean

returns true of the method == HEAD

Returns:

  • (Boolean)


145
146
147
# File 'lib/iodine/http/request.rb', line 145

def head?
	self[:method] == HTTP_HEAD
end

#headersObject

the request’s headers



56
57
58
# File 'lib/iodine/http/request.rb', line 56

def headers
	self.select {|k,v| k.is_a? String }
end

#host_nameObject

the host’s name (without the port)



115
116
117
# File 'lib/iodine/http/request.rb', line 115

def host_name
	self[:host_name]
end

#ioIodine::Http, ...

Returns the Protocol used for the request.

Returns:

  • (Iodine::Http, Iodine::Http2, Iodine::Websockets)

    the Protocol used for the request.



126
127
128
# File 'lib/iodine/http/request.rb', line 126

def io
	self[:io]
end

#json?Boolean

returns true if the request is of type JSON.

Returns:

  • (Boolean)


185
186
187
# File 'lib/iodine/http/request.rb', line 185

def json?
	self[HTTP_CTYPE] =~ HTTP_JSON
end

#options?Boolean

returns true of the method == OPTIONS

Returns:

  • (Boolean)


170
171
172
# File 'lib/iodine/http/request.rb', line 170

def options?
	self[:method] == HTTP_OPTIONS
end

#original_pathObject

the original (frozen) path (resource requested).



82
83
84
# File 'lib/iodine/http/request.rb', line 82

def original_path
	self[:original_path]
end

#paramsObject

the parameters sent by the client.



68
69
70
# File 'lib/iodine/http/request.rb', line 68

def params
	self[:params]
end

#patch?Boolean

returns true of the method == PATCH

Returns:

  • (Boolean)


180
181
182
# File 'lib/iodine/http/request.rb', line 180

def patch?
	self[:method] == HTTP_PATCH
end

#pathObject

the requested path (rewritable).



87
88
89
# File 'lib/iodine/http/request.rb', line 87

def path
	self[:path]
end

#path=(new_path) ⇒ Object



90
91
92
# File 'lib/iodine/http/request.rb', line 90

def path=(new_path)
	self[:path] = new_path
end

#post?Boolean

returns true of the method == POST

Returns:

  • (Boolean)


150
151
152
# File 'lib/iodine/http/request.rb', line 150

def post?
	self[:method] == HTTP_POST
end

#put?Boolean

returns true of the method == PUT

Returns:

  • (Boolean)


155
156
157
# File 'lib/iodine/http/request.rb', line 155

def put?
	self[:method] == HTTP_PUT
end

#queryObject

the query string



77
78
79
# File 'lib/iodine/http/request.rb', line 77

def query
	self[:query]
end

#request_methodObject

the request’s method (GET, POST… etc’).



60
61
62
# File 'lib/iodine/http/request.rb', line 60

def request_method
	self[:method]
end

#request_method=(value) ⇒ Object

set request’s method (GET, POST… etc’).



64
65
66
# File 'lib/iodine/http/request.rb', line 64

def request_method= value
	self[:method] = value
end

#request_url(switch_scheme = nil) ⇒ Object

the request’s url, without any GET parameters ([http/https]://host/path)



105
106
107
# File 'lib/iodine/http/request.rb', line 105

def request_url switch_scheme = nil
	"#{base_url switch_scheme}#{self[:original_path]}".freeze
end

#schemeObject

the protocol’s scheme (http/https/ws/wss) managing this request



110
111
112
# File 'lib/iodine/http/request.rb', line 110

def scheme
	self[:scheme]
end

#sessionHash like storage

Returns the session storage object IF a session was already initialized (use the response to initialize a session).

Returns:

  • (Hash like storage)

    Returns the session storage object IF a session was already initialized (use the response to initialize a session).



131
132
133
# File 'lib/iodine/http/request.rb', line 131

def session
	self[:session]
end

#ssl?true, false Also known as: secure?

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

Returns:

  • (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).



120
121
122
# File 'lib/iodine/http/request.rb', line 120

def ssl?
	self[:io].ssl? || self[:scheme] == 'https'.freeze || self[:scheme] == 'wss'.freeze
end

#trace?Boolean

returns true of the method == TRACE

Returns:

  • (Boolean)


165
166
167
# File 'lib/iodine/http/request.rb', line 165

def trace?
	self[:method] == HTTP_TRACE
end

#versionObject

The HTTP version for this request



95
96
97
# File 'lib/iodine/http/request.rb', line 95

def version
	self[:version]
end

#websocket?Boolean Also known as: upgrade?

returns true if this is a websocket upgrade request

Returns:

  • (Boolean)


194
195
196
# File 'lib/iodine/http/request.rb', line 194

def websocket?
	@is_websocket ||= (self['upgrade'.freeze] && self['upgrade'.freeze].to_s =~ /websocket/i.freeze &&  self['connection'.freeze].to_s =~ /upg/i.freeze && true)
end

#xml?Boolean

returns true if the request is of type XML.

Returns:

  • (Boolean)


190
191
192
# File 'lib/iodine/http/request.rb', line 190

def xml?
	self[HTTP_CTYPE].match HTTP_XML
end