tricycle-rack-contrib 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README.rdoc +78 -0
- data/Rakefile +97 -0
- data/lib/rack/contrib.rb +37 -0
- data/lib/rack/contrib/accept_format.rb +46 -0
- data/lib/rack/contrib/backstage.rb +20 -0
- data/lib/rack/contrib/bounce_favicon.rb +16 -0
- data/lib/rack/contrib/callbacks.rb +37 -0
- data/lib/rack/contrib/config.rb +16 -0
- data/lib/rack/contrib/cookies.rb +50 -0
- data/lib/rack/contrib/csshttprequest.rb +39 -0
- data/lib/rack/contrib/deflect.rb +137 -0
- data/lib/rack/contrib/etag.rb +20 -0
- data/lib/rack/contrib/evil.rb +12 -0
- data/lib/rack/contrib/garbagecollector.rb +14 -0
- data/lib/rack/contrib/jsonp.rb +41 -0
- data/lib/rack/contrib/lighttpd_script_name_fix.rb +16 -0
- data/lib/rack/contrib/locale.rb +31 -0
- data/lib/rack/contrib/mailexceptions.rb +120 -0
- data/lib/rack/contrib/nested_params.rb +143 -0
- data/lib/rack/contrib/not_found.rb +18 -0
- data/lib/rack/contrib/post_body_content_type_parser.rb +40 -0
- data/lib/rack/contrib/proctitle.rb +30 -0
- data/lib/rack/contrib/profiler.rb +108 -0
- data/lib/rack/contrib/relative_redirect.rb +44 -0
- data/lib/rack/contrib/response_cache.rb +116 -0
- data/lib/rack/contrib/route_exceptions.rb +49 -0
- data/lib/rack/contrib/sendfile.rb +142 -0
- data/lib/rack/contrib/signals.rb +63 -0
- data/lib/rack/contrib/time_zone.rb +25 -0
- data/test/404.html +1 -0
- data/test/Maintenance.html +1 -0
- data/test/mail_settings.rb +12 -0
- data/test/spec_rack_accept_format.rb +72 -0
- data/test/spec_rack_backstage.rb +26 -0
- data/test/spec_rack_callbacks.rb +65 -0
- data/test/spec_rack_config.rb +22 -0
- data/test/spec_rack_contrib.rb +8 -0
- data/test/spec_rack_csshttprequest.rb +66 -0
- data/test/spec_rack_deflect.rb +107 -0
- data/test/spec_rack_etag.rb +23 -0
- data/test/spec_rack_evil.rb +19 -0
- data/test/spec_rack_garbagecollector.rb +13 -0
- data/test/spec_rack_jsonp.rb +34 -0
- data/test/spec_rack_lighttpd_script_name_fix.rb +16 -0
- data/test/spec_rack_mailexceptions.rb +97 -0
- data/test/spec_rack_nested_params.rb +46 -0
- data/test/spec_rack_not_found.rb +17 -0
- data/test/spec_rack_post_body_content_type_parser.rb +32 -0
- data/test/spec_rack_proctitle.rb +26 -0
- data/test/spec_rack_profiler.rb +37 -0
- data/test/spec_rack_relative_redirect.rb +78 -0
- data/test/spec_rack_response_cache.rb +181 -0
- data/test/spec_rack_sendfile.rb +86 -0
- data/tricycle-rack-contrib.gemspec +88 -0
- metadata +175 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
# TODO: optional stats
|
4
|
+
# TODO: performance
|
5
|
+
# TODO: clean up tests
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
|
9
|
+
##
|
10
|
+
# Rack middleware for protecting against Denial-of-service attacks
|
11
|
+
# http://en.wikipedia.org/wiki/Denial-of-service_attack.
|
12
|
+
#
|
13
|
+
# This middleware is designed for small deployments, which most likely
|
14
|
+
# are not utilizing load balancing from other software or hardware. Deflect
|
15
|
+
# current supports the following functionality:
|
16
|
+
#
|
17
|
+
# * Saturation prevention (small DoS attacks, or request abuse)
|
18
|
+
# * Blacklisting of remote addresses
|
19
|
+
# * Whitelisting of remote addresses
|
20
|
+
# * Logging
|
21
|
+
#
|
22
|
+
# === Options:
|
23
|
+
#
|
24
|
+
# :log When false logging will be bypassed, otherwise pass an object responding to #puts
|
25
|
+
# :log_format Alter the logging format
|
26
|
+
# :log_date_format Alter the logging date format
|
27
|
+
# :request_threshold Number of requests allowed within the set :interval. Defaults to 100
|
28
|
+
# :interval Duration in seconds until the request counter is reset. Defaults to 5
|
29
|
+
# :block_duration Duration in seconds that a remote address will be blocked. Defaults to 900 (15 minutes)
|
30
|
+
# :whitelist Array of remote addresses which bypass Deflect. NOTE: this does not block others
|
31
|
+
# :blacklist Array of remote addresses immediately considered malicious
|
32
|
+
#
|
33
|
+
# === Examples:
|
34
|
+
#
|
35
|
+
# use Rack::Deflect, :log => $stdout, :request_threshold => 20, :interval => 2, :block_duration => 60
|
36
|
+
#
|
37
|
+
# CREDIT: TJ Holowaychuk <tj@vision-media.ca>
|
38
|
+
#
|
39
|
+
|
40
|
+
class Deflect
|
41
|
+
|
42
|
+
attr_reader :options
|
43
|
+
|
44
|
+
def initialize app, options = {}
|
45
|
+
@mutex = Mutex.new
|
46
|
+
@remote_addr_map = {}
|
47
|
+
@app, @options = app, {
|
48
|
+
:log => false,
|
49
|
+
:log_format => 'deflect(%s): %s',
|
50
|
+
:log_date_format => '%m/%d/%Y',
|
51
|
+
:request_threshold => 100,
|
52
|
+
:interval => 5,
|
53
|
+
:block_duration => 900,
|
54
|
+
:whitelist => [],
|
55
|
+
:blacklist => []
|
56
|
+
}.merge(options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def call env
|
60
|
+
return deflect! if deflect? env
|
61
|
+
status, headers, body = @app.call env
|
62
|
+
[status, headers, body]
|
63
|
+
end
|
64
|
+
|
65
|
+
def deflect!
|
66
|
+
[403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, '']
|
67
|
+
end
|
68
|
+
|
69
|
+
def deflect? env
|
70
|
+
@remote_addr = env['REMOTE_ADDR']
|
71
|
+
return false if options[:whitelist].include? @remote_addr
|
72
|
+
return true if options[:blacklist].include? @remote_addr
|
73
|
+
sync { watch }
|
74
|
+
end
|
75
|
+
|
76
|
+
def log message
|
77
|
+
return unless options[:log]
|
78
|
+
options[:log].puts(options[:log_format] % [Time.now.strftime(options[:log_date_format]), message])
|
79
|
+
end
|
80
|
+
|
81
|
+
def sync &block
|
82
|
+
@mutex.synchronize(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def map
|
86
|
+
@remote_addr_map[@remote_addr] ||= {
|
87
|
+
:expires => Time.now + options[:interval],
|
88
|
+
:requests => 0
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def watch
|
93
|
+
increment_requests
|
94
|
+
clear! if watch_expired? and not blocked?
|
95
|
+
clear! if blocked? and block_expired?
|
96
|
+
block! if watching? and exceeded_request_threshold?
|
97
|
+
blocked?
|
98
|
+
end
|
99
|
+
|
100
|
+
def block!
|
101
|
+
return if blocked?
|
102
|
+
log "blocked #{@remote_addr}"
|
103
|
+
map[:block_expires] = Time.now + options[:block_duration]
|
104
|
+
end
|
105
|
+
|
106
|
+
def blocked?
|
107
|
+
map.has_key? :block_expires
|
108
|
+
end
|
109
|
+
|
110
|
+
def block_expired?
|
111
|
+
map[:block_expires] < Time.now rescue false
|
112
|
+
end
|
113
|
+
|
114
|
+
def watching?
|
115
|
+
@remote_addr_map.has_key? @remote_addr
|
116
|
+
end
|
117
|
+
|
118
|
+
def clear!
|
119
|
+
return unless watching?
|
120
|
+
log "released #{@remote_addr}" if blocked?
|
121
|
+
@remote_addr_map.delete @remote_addr
|
122
|
+
end
|
123
|
+
|
124
|
+
def increment_requests
|
125
|
+
map[:requests] += 1
|
126
|
+
end
|
127
|
+
|
128
|
+
def exceeded_request_threshold?
|
129
|
+
map[:requests] > options[:request_threshold]
|
130
|
+
end
|
131
|
+
|
132
|
+
def watch_expired?
|
133
|
+
map[:expires] <= Time.now
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Automatically sets the ETag header on all String bodies
|
5
|
+
class ETag
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
status, headers, body = @app.call(env)
|
12
|
+
|
13
|
+
if !headers.has_key?('ETag') && body.is_a?(String)
|
14
|
+
headers['ETag'] = %("#{Digest::MD5.hexdigest(body)}")
|
15
|
+
end
|
16
|
+
|
17
|
+
[status, headers, body]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rack
|
2
|
+
|
3
|
+
# A Rack middleware for providing JSON-P support.
|
4
|
+
#
|
5
|
+
# Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
|
6
|
+
#
|
7
|
+
class JSONP
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
# Proxies the request to the application, stripping out the JSON-P callback
|
14
|
+
# method and padding the response with the appropriate callback format.
|
15
|
+
#
|
16
|
+
# Changes nothing if no <tt>callback</tt> param is specified.
|
17
|
+
#
|
18
|
+
def call(env)
|
19
|
+
status, headers, response = @app.call(env)
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
if request.params.include?('callback')
|
22
|
+
response = pad(request.params.delete('callback'), response)
|
23
|
+
headers['Content-Length'] = response.length.to_s
|
24
|
+
end
|
25
|
+
[status, headers, response]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Pads the response with the appropriate callback format according to the
|
29
|
+
# JSON-P spec/requirements.
|
30
|
+
#
|
31
|
+
# The Rack response spec indicates that it should be enumerable. The method
|
32
|
+
# of combining all of the data into a single string makes sense since JSON
|
33
|
+
# is returned as a full string.
|
34
|
+
#
|
35
|
+
def pad(callback, response, body = "")
|
36
|
+
response.each{ |s| body << s.to_s }
|
37
|
+
"#{callback}(#{body})"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
# Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
|
3
|
+
# FastCGI app at "/". This middleware fixes this issue.
|
4
|
+
|
5
|
+
class LighttpdScriptNameFix
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
|
12
|
+
env["SCRIPT_NAME"] = ""
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Locale
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
old_locale = I18n.locale
|
11
|
+
locale = nil
|
12
|
+
|
13
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
14
|
+
if lang = env["HTTP_ACCEPT_LANGUAGE"]
|
15
|
+
lang = lang.split(",").map { |l|
|
16
|
+
l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
|
17
|
+
l.split(';q=')
|
18
|
+
}.first
|
19
|
+
locale = lang.first.split("-").first
|
20
|
+
else
|
21
|
+
locale = I18n.default_locale
|
22
|
+
end
|
23
|
+
|
24
|
+
locale = env['rack.locale'] = I18n.locale = locale.to_s
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
headers['Content-Language'] = locale
|
27
|
+
I18n.locale = old_locale
|
28
|
+
[status, headers, body]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
require 'tmail'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
# Catches all exceptions raised from the app it wraps and
|
7
|
+
# sends a useful email with the exception, stacktrace, and
|
8
|
+
# contents of the environment.
|
9
|
+
|
10
|
+
class MailExceptions
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
@config = {
|
16
|
+
:to => nil,
|
17
|
+
:from => ENV['USER'] || 'rack',
|
18
|
+
:subject => '[exception] %s',
|
19
|
+
:smtp => {
|
20
|
+
:server => 'localhost',
|
21
|
+
:domain => 'localhost',
|
22
|
+
:port => 25,
|
23
|
+
:authentication => :login,
|
24
|
+
:user_name => nil,
|
25
|
+
:password => nil
|
26
|
+
}
|
27
|
+
}
|
28
|
+
@template = ERB.new(TEMPLATE)
|
29
|
+
yield self if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
status, headers, body =
|
34
|
+
begin
|
35
|
+
@app.call(env)
|
36
|
+
rescue => boom
|
37
|
+
# TODO don't allow exceptions from send_notification to
|
38
|
+
# propogate
|
39
|
+
send_notification boom, env
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
send_notification env['mail.exception'], env if env['mail.exception']
|
43
|
+
[status, headers, body]
|
44
|
+
end
|
45
|
+
|
46
|
+
%w[to from subject].each do |meth|
|
47
|
+
define_method(meth) { |value| @config[meth.to_sym] = value }
|
48
|
+
end
|
49
|
+
|
50
|
+
def smtp(settings={})
|
51
|
+
@config[:smtp].merge! settings
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def generate_mail(exception, env)
|
56
|
+
mail = TMail::Mail.new
|
57
|
+
mail.to = Array(config[:to])
|
58
|
+
mail.from = config[:from]
|
59
|
+
mail.subject = config[:subject] % [exception.to_s]
|
60
|
+
mail.date = Time.now
|
61
|
+
mail.set_content_type 'text/plain'
|
62
|
+
mail.charset = 'UTF-8'
|
63
|
+
mail.body = @template.result(binding)
|
64
|
+
mail
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_notification(exception, env)
|
68
|
+
mail = generate_mail(exception, env)
|
69
|
+
smtp = config[:smtp]
|
70
|
+
env['mail.sent'] = true
|
71
|
+
return if smtp[:server] == 'example.com'
|
72
|
+
|
73
|
+
Net::SMTP.start smtp[:server], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
|
74
|
+
mail.to.each do |recipient|
|
75
|
+
server.send_message mail.to_s, mail.from, recipient
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_body(env)
|
81
|
+
if io = env['rack.input']
|
82
|
+
io.rewind if io.respond_to?(:rewind)
|
83
|
+
io.read
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
TEMPLATE = (<<-'EMAIL').gsub(/^ {4}/, '')
|
88
|
+
A <%= exception.class.to_s %> occured: <%= exception.to_s %>
|
89
|
+
<% if body = extract_body(env) %>
|
90
|
+
|
91
|
+
===================================================================
|
92
|
+
Request Body:
|
93
|
+
===================================================================
|
94
|
+
|
95
|
+
<%= body.gsub(/^/, ' ') %>
|
96
|
+
<% end %>
|
97
|
+
|
98
|
+
===================================================================
|
99
|
+
Rack Environment:
|
100
|
+
===================================================================
|
101
|
+
|
102
|
+
PID: <%= $$ %>
|
103
|
+
PWD: <%= Dir.getwd %>
|
104
|
+
|
105
|
+
<%= env.to_a.
|
106
|
+
sort{|a,b| a.first <=> b.first}.
|
107
|
+
map{ |k,v| "%-25s%p" % [k+':', v] }.
|
108
|
+
join("\n ") %>
|
109
|
+
|
110
|
+
<% if exception.respond_to?(:backtrace) %>
|
111
|
+
===================================================================
|
112
|
+
Backtrace:
|
113
|
+
===================================================================
|
114
|
+
|
115
|
+
<%= exception.backtrace.join("\n ") %>
|
116
|
+
<% end %>
|
117
|
+
EMAIL
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
# Rack middleware for parsing POST/PUT body data into nested parameters
|
6
|
+
class NestedParams
|
7
|
+
|
8
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
9
|
+
POST_BODY = 'rack.input'.freeze
|
10
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
11
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
12
|
+
FORM_VARS = 'rack.request.form_vars'.freeze
|
13
|
+
|
14
|
+
# supported content type
|
15
|
+
URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
|
16
|
+
|
17
|
+
def initialize(app)
|
18
|
+
@app = app
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
if form_vars = env[FORM_VARS]
|
23
|
+
env[FORM_HASH] = parse_query_parameters(form_vars)
|
24
|
+
elsif env[CONTENT_TYPE] == URL_ENCODED
|
25
|
+
post_body = env[POST_BODY]
|
26
|
+
env[FORM_INPUT] = post_body
|
27
|
+
env[FORM_HASH] = parse_query_parameters(post_body.read)
|
28
|
+
post_body.rewind if post_body.respond_to?(:rewind)
|
29
|
+
end
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
|
33
|
+
## the rest is nabbed from Rails ##
|
34
|
+
|
35
|
+
def parse_query_parameters(query_string)
|
36
|
+
return {} if query_string.nil? or query_string.empty?
|
37
|
+
|
38
|
+
pairs = query_string.split('&').collect do |chunk|
|
39
|
+
next if chunk.empty?
|
40
|
+
key, value = chunk.split('=', 2)
|
41
|
+
next if key.empty?
|
42
|
+
value = value.nil? ? nil : CGI.unescape(value)
|
43
|
+
[ CGI.unescape(key), value ]
|
44
|
+
end.compact
|
45
|
+
|
46
|
+
UrlEncodedPairParser.new(pairs).result
|
47
|
+
end
|
48
|
+
|
49
|
+
class UrlEncodedPairParser < StringScanner
|
50
|
+
attr_reader :top, :parent, :result
|
51
|
+
|
52
|
+
def initialize(pairs = [])
|
53
|
+
super('')
|
54
|
+
@result = {}
|
55
|
+
pairs.each { |key, value| parse(key, value) }
|
56
|
+
end
|
57
|
+
|
58
|
+
KEY_REGEXP = %r{([^\[\]=&]+)}
|
59
|
+
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
|
60
|
+
|
61
|
+
# Parse the query string
|
62
|
+
def parse(key, value)
|
63
|
+
self.string = key
|
64
|
+
@top, @parent = result, nil
|
65
|
+
|
66
|
+
# First scan the bare key
|
67
|
+
key = scan(KEY_REGEXP) or return
|
68
|
+
key = post_key_check(key)
|
69
|
+
|
70
|
+
# Then scan as many nestings as present
|
71
|
+
until eos?
|
72
|
+
r = scan(BRACKETED_KEY_REGEXP) or return
|
73
|
+
key = self[1]
|
74
|
+
key = post_key_check(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
bind(key, value)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
# After we see a key, we must look ahead to determine our next action. Cases:
|
82
|
+
#
|
83
|
+
# [] follows the key. Then the value must be an array.
|
84
|
+
# = follows the key. (A value comes next)
|
85
|
+
# & or the end of string follows the key. Then the key is a flag.
|
86
|
+
# otherwise, a hash follows the key.
|
87
|
+
def post_key_check(key)
|
88
|
+
if scan(/\[\]/) # a[b][] indicates that b is an array
|
89
|
+
container(key, Array)
|
90
|
+
nil
|
91
|
+
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
|
92
|
+
container(key, Hash)
|
93
|
+
nil
|
94
|
+
else # End of key? We do nothing.
|
95
|
+
key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a container to the stack.
|
100
|
+
def container(key, klass)
|
101
|
+
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
|
102
|
+
value = bind(key, klass.new)
|
103
|
+
type_conflict! klass, value unless value.is_a?(klass)
|
104
|
+
push(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Push a value onto the 'stack', which is actually only the top 2 items.
|
108
|
+
def push(value)
|
109
|
+
@parent, @top = @top, value
|
110
|
+
end
|
111
|
+
|
112
|
+
# Bind a key (which may be nil for items in an array) to the provided value.
|
113
|
+
def bind(key, value)
|
114
|
+
if top.is_a? Array
|
115
|
+
if key
|
116
|
+
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
|
117
|
+
top[-1][key] = value
|
118
|
+
else
|
119
|
+
top << {key => value}
|
120
|
+
end
|
121
|
+
push top.last
|
122
|
+
return top[key]
|
123
|
+
else
|
124
|
+
top << value
|
125
|
+
return value
|
126
|
+
end
|
127
|
+
elsif top.is_a? Hash
|
128
|
+
key = CGI.unescape(key)
|
129
|
+
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
|
130
|
+
top[key] ||= value
|
131
|
+
return top[key]
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def type_conflict!(klass, value)
|
138
|
+
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|