rack 1.5.5 → 1.6.0.beta

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

Potentially problematic release.


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

Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/KNOWN-ISSUES +14 -0
  3. data/README.rdoc +10 -6
  4. data/Rakefile +3 -4
  5. data/SPEC +59 -23
  6. data/lib/rack.rb +2 -1
  7. data/lib/rack/auth/abstract/request.rb +1 -1
  8. data/lib/rack/auth/basic.rb +1 -1
  9. data/lib/rack/auth/digest/md5.rb +1 -1
  10. data/lib/rack/backports/uri/common_18.rb +1 -1
  11. data/lib/rack/builder.rb +19 -4
  12. data/lib/rack/cascade.rb +2 -2
  13. data/lib/rack/chunked.rb +12 -1
  14. data/lib/rack/commonlogger.rb +13 -5
  15. data/lib/rack/conditionalget.rb +14 -2
  16. data/lib/rack/content_length.rb +5 -1
  17. data/lib/rack/deflater.rb +52 -13
  18. data/lib/rack/directory.rb +8 -2
  19. data/lib/rack/etag.rb +14 -6
  20. data/lib/rack/file.rb +10 -14
  21. data/lib/rack/handler.rb +2 -0
  22. data/lib/rack/handler/fastcgi.rb +4 -1
  23. data/lib/rack/handler/mongrel.rb +8 -2
  24. data/lib/rack/handler/scgi.rb +4 -1
  25. data/lib/rack/handler/thin.rb +8 -2
  26. data/lib/rack/handler/webrick.rb +46 -6
  27. data/lib/rack/head.rb +7 -2
  28. data/lib/rack/lint.rb +73 -25
  29. data/lib/rack/lobster.rb +8 -3
  30. data/lib/rack/methodoverride.rb +14 -3
  31. data/lib/rack/mime.rb +1 -15
  32. data/lib/rack/mock.rb +15 -7
  33. data/lib/rack/multipart.rb +2 -2
  34. data/lib/rack/multipart/parser.rb +107 -53
  35. data/lib/rack/multipart/uploaded_file.rb +2 -2
  36. data/lib/rack/nulllogger.rb +21 -2
  37. data/lib/rack/request.rb +38 -24
  38. data/lib/rack/response.rb +5 -0
  39. data/lib/rack/sendfile.rb +10 -5
  40. data/lib/rack/server.rb +45 -17
  41. data/lib/rack/session/abstract/id.rb +7 -6
  42. data/lib/rack/session/cookie.rb +17 -7
  43. data/lib/rack/session/memcache.rb +4 -4
  44. data/lib/rack/session/pool.rb +3 -6
  45. data/lib/rack/showexceptions.rb +20 -11
  46. data/lib/rack/showstatus.rb +1 -1
  47. data/lib/rack/static.rb +27 -30
  48. data/lib/rack/tempfile_reaper.rb +22 -0
  49. data/lib/rack/urlmap.rb +17 -3
  50. data/lib/rack/utils.rb +78 -47
  51. data/lib/rack/utils/okjson.rb +90 -91
  52. data/rack.gemspec +3 -3
  53. data/test/multipart/filename_and_no_name +6 -0
  54. data/test/multipart/invalid_character +6 -0
  55. data/test/spec_builder.rb +13 -4
  56. data/test/spec_chunked.rb +16 -0
  57. data/test/spec_commonlogger.rb +36 -0
  58. data/test/spec_content_length.rb +3 -1
  59. data/test/spec_deflater.rb +283 -148
  60. data/test/spec_etag.rb +11 -2
  61. data/test/spec_file.rb +11 -3
  62. data/test/spec_head.rb +2 -0
  63. data/test/spec_lobster.rb +1 -1
  64. data/test/spec_mock.rb +8 -0
  65. data/test/spec_multipart.rb +111 -49
  66. data/test/spec_request.rb +109 -25
  67. data/test/spec_response.rb +30 -0
  68. data/test/spec_server.rb +20 -5
  69. data/test/spec_session_cookie.rb +45 -2
  70. data/test/spec_session_memcache.rb +1 -1
  71. data/test/spec_showexceptions.rb +29 -36
  72. data/test/spec_showstatus.rb +19 -0
  73. data/test/spec_tempfile_reaper.rb +63 -0
  74. data/test/spec_urlmap.rb +23 -0
  75. data/test/spec_utils.rb +60 -10
  76. data/test/spec_webrick.rb +41 -0
  77. metadata +12 -9
  78. data/test/cgi/lighttpd.errors +0 -1
  79. data/test/multipart/three_files_three_fields +0 -31
@@ -47,7 +47,7 @@ module Rack
47
47
  end
48
48
 
49
49
  def get_session(env, sid)
50
- with_lock(env, [nil, {}]) do
50
+ with_lock(env) do
51
51
  unless sid and session = @pool.get(sid)
52
52
  sid, session = generate_sid, {}
53
53
  unless /^STORED/ =~ @pool.add(sid, session)
@@ -62,7 +62,7 @@ module Rack
62
62
  expiry = options[:expire_after]
63
63
  expiry = expiry.nil? ? 0 : expiry + 1
64
64
 
65
- with_lock(env, false) do
65
+ with_lock(env) do
66
66
  @pool.set session_id, new_session, expiry
67
67
  session_id
68
68
  end
@@ -75,7 +75,7 @@ module Rack
75
75
  end
76
76
  end
77
77
 
78
- def with_lock(env, default=nil)
78
+ def with_lock(env)
79
79
  @mutex.lock if env['rack.multithread']
80
80
  yield
81
81
  rescue MemCache::MemCacheError, Errno::ECONNREFUSED
@@ -83,7 +83,7 @@ module Rack
83
83
  warn "#{self} is unable to find memcached server."
84
84
  warn $!.inspect
85
85
  end
86
- default
86
+ raise
87
87
  ensure
88
88
  @mutex.unlock if @mutex.locked?
89
89
  end
@@ -42,7 +42,7 @@ module Rack
42
42
  end
43
43
 
44
44
  def get_session(env, sid)
45
- with_lock(env, [nil, {}]) do
45
+ with_lock(env) do
46
46
  unless sid and session = @pool[sid]
47
47
  sid, session = generate_sid, {}
48
48
  @pool.store sid, session
@@ -52,7 +52,7 @@ module Rack
52
52
  end
53
53
 
54
54
  def set_session(env, session_id, new_session, options)
55
- with_lock(env, false) do
55
+ with_lock(env) do
56
56
  @pool.store session_id, new_session
57
57
  session_id
58
58
  end
@@ -65,15 +65,12 @@ module Rack
65
65
  end
66
66
  end
67
67
 
68
- def with_lock(env, default=nil)
68
+ def with_lock(env)
69
69
  @mutex.lock if env['rack.multithread']
70
70
  yield
71
- rescue
72
- default
73
71
  ensure
74
72
  @mutex.unlock if @mutex.locked?
75
73
  end
76
-
77
74
  end
78
75
  end
79
76
  end
@@ -28,23 +28,32 @@ module Rack
28
28
  env["rack.errors"].puts(exception_string)
29
29
  env["rack.errors"].flush
30
30
 
31
- if prefers_plain_text?(env)
32
- content_type = "text/plain"
33
- body = [exception_string]
34
- else
31
+ if accepts_html?(env)
35
32
  content_type = "text/html"
36
33
  body = pretty(env, e)
34
+ else
35
+ content_type = "text/plain"
36
+ body = exception_string
37
37
  end
38
38
 
39
- [500,
40
- {"Content-Type" => content_type,
41
- "Content-Length" => Rack::Utils.bytesize(body.join).to_s},
42
- body]
39
+ [
40
+ 500,
41
+ {
42
+ "Content-Type" => content_type,
43
+ "Content-Length" => Rack::Utils.bytesize(body).to_s,
44
+ },
45
+ [body],
46
+ ]
47
+ end
48
+
49
+ def prefers_plaintext?(env)
50
+ !accepts_html(env)
43
51
  end
44
52
 
45
- def prefers_plain_text?(env)
46
- env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" && (!env["HTTP_ACCEPT"] || !env["HTTP_ACCEPT"].include?("text/html"))
53
+ def accepts_html?(env)
54
+ Rack::Utils.best_q_match(env["HTTP_ACCEPT"], %w[text/html])
47
55
  end
56
+ private :accepts_html?
48
57
 
49
58
  def dump_exception(exception)
50
59
  string = "#{exception.class}: #{exception.message}\n"
@@ -85,7 +94,7 @@ module Rack
85
94
  end
86
95
  }.compact
87
96
 
88
- [@template.result(binding)]
97
+ @template.result(binding)
89
98
  end
90
99
 
91
100
  def h(obj) # :nodoc:
@@ -96,7 +96,7 @@ TEMPLATE = <<'HTML'
96
96
  </table>
97
97
  </div>
98
98
  <div id="info">
99
- <p><%= detail %></p>
99
+ <p><%=h detail %></p>
100
100
  </div>
101
101
 
102
102
  <div id="explanation">
@@ -90,9 +90,8 @@ module Rack
90
90
  @header_rules = options[:header_rules] || []
91
91
  # Allow for legacy :cache_control option while prioritizing global header_rules setting
92
92
  @header_rules.insert(0, [:all, {'Cache-Control' => options[:cache_control]}]) if options[:cache_control]
93
- @headers = {}
94
93
 
95
- @file_server = Rack::File.new(root, @headers)
94
+ @file_server = Rack::File.new(root)
96
95
  end
97
96
 
98
97
  def overwrite_file_path(path)
@@ -112,42 +111,40 @@ module Rack
112
111
 
113
112
  if can_serve(path)
114
113
  env["PATH_INFO"] = (path =~ /\/$/ ? path + @index : @urls[path]) if overwrite_file_path(path)
115
- @path = env["PATH_INFO"]
116
- apply_header_rules
117
- @file_server.call(env)
114
+ path = env["PATH_INFO"]
115
+ response = @file_server.call(env)
116
+
117
+ headers = response[1]
118
+ applicable_rules(path).each do |rule, new_headers|
119
+ new_headers.each { |field, content| headers[field] = content }
120
+ end
121
+
122
+ response
118
123
  else
119
124
  @app.call(env)
120
125
  end
121
126
  end
122
127
 
123
128
  # Convert HTTP header rules to HTTP headers
124
- def apply_header_rules
125
- @header_rules.each do |rule, headers|
126
- apply_rule(rule, headers)
127
- end
128
- end
129
-
130
- def apply_rule(rule, headers)
131
- case rule
132
- when :all # All files
133
- set_headers(headers)
134
- when :fonts # Fonts Shortcut
135
- set_headers(headers) if @path.match(/\.(?:ttf|otf|eot|woff|svg)\z/)
136
- when String # Folder
137
- path = ::Rack::Utils.unescape(@path)
138
- set_headers(headers) if (path.start_with?(rule) || path.start_with?('/' + rule))
139
- when Array # Extension/Extensions
140
- extensions = rule.join('|')
141
- set_headers(headers) if @path.match(/\.(#{extensions})\z/)
142
- when Regexp # Flexible Regexp
143
- set_headers(headers) if @path.match(rule)
144
- else
129
+ def applicable_rules(path)
130
+ @header_rules.find_all do |rule, new_headers|
131
+ case rule
132
+ when :all
133
+ true
134
+ when :fonts
135
+ path =~ /\.(?:ttf|otf|eot|woff|svg)\z/
136
+ when String
137
+ path = ::Rack::Utils.unescape(path)
138
+ path.start_with?(rule) || path.start_with?('/' + rule)
139
+ when Array
140
+ path =~ /\.(#{rule.join('|')})\z/
141
+ when Regexp
142
+ path =~ rule
143
+ else
144
+ false
145
+ end
145
146
  end
146
147
  end
147
148
 
148
- def set_headers(headers)
149
- headers.each { |field, content| @headers[field] = content }
150
- end
151
-
152
149
  end
153
150
  end
@@ -0,0 +1,22 @@
1
+ require 'rack/body_proxy'
2
+
3
+ module Rack
4
+
5
+ # Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart)
6
+ # Ideas/strategy based on posts by Eric Wong and Charles Oliver Nutter
7
+ # https://groups.google.com/forum/#!searchin/rack-devel/temp/rack-devel/brK8eh-MByw/sw61oJJCGRMJ
8
+ class TempfileReaper
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ env['rack.tempfiles'] ||= []
15
+ status, headers, body = @app.call(env)
16
+ body_proxy = BodyProxy.new(body) do
17
+ env['rack.tempfiles'].each { |f| f.close! } unless env['rack.tempfiles'].nil?
18
+ end
19
+ [status, headers, body_proxy]
20
+ end
21
+ end
22
+ end
@@ -48,9 +48,10 @@ module Rack
48
48
  sPort = env['SERVER_PORT']
49
49
 
50
50
  @mapping.each do |host, location, match, app|
51
- unless hHost == host \
52
- || sName == host \
53
- || (!host && (hHost == sName || hHost == sName+':'+sPort))
51
+ unless casecmp?(hHost, host) \
52
+ || casecmp?(sName, host) \
53
+ || (!host && (casecmp?(hHost, sName) ||
54
+ casecmp?(hHost, sName+':'+sPort)))
54
55
  next
55
56
  end
56
57
 
@@ -71,6 +72,19 @@ module Rack
71
72
  env['PATH_INFO'] = path
72
73
  env['SCRIPT_NAME'] = script_name
73
74
  end
75
+
76
+ private
77
+ def casecmp?(v1, v2)
78
+ # if both nil, or they're the same string
79
+ return true if v1 == v2
80
+
81
+ # if either are nil... (but they're not the same)
82
+ return false if v1.nil?
83
+ return false if v2.nil?
84
+
85
+ # otherwise check they're not case-insensitive the same
86
+ v1.casecmp(v2).zero?
87
+ end
74
88
  end
75
89
  end
76
90
 
@@ -9,7 +9,7 @@ major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
9
9
 
10
10
  if major == 1 && minor < 9
11
11
  require 'rack/backports/uri/common_18'
12
- elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 320 && RUBY_ENGINE != 'jruby'
12
+ elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby'
13
13
  require 'rack/backports/uri/common_192'
14
14
  elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
15
15
  require 'rack/backports/uri/common_193'
@@ -22,6 +22,15 @@ module Rack
22
22
  # applications adopted from all kinds of Ruby libraries.
23
23
 
24
24
  module Utils
25
+ # ParameterTypeError is the error that is raised when incoming structural
26
+ # parameters (parsed by parse_nested_query) contain conflicting types.
27
+ class ParameterTypeError < TypeError; end
28
+
29
+ # InvalidParameterError is the error that is raised when incoming structural
30
+ # parameters (parsed by parse_nested_query) contain invalid format or byte
31
+ # sequence.
32
+ class InvalidParameterError < ArgumentError; end
33
+
25
34
  # URI escapes. (CGI style space to +)
26
35
  def escape(s)
27
36
  URI.encode_www_form_component(s)
@@ -52,23 +61,12 @@ module Rack
52
61
 
53
62
  class << self
54
63
  attr_accessor :key_space_limit
55
- attr_accessor :param_depth_limit
56
- attr_accessor :multipart_part_limit
57
64
  end
58
65
 
59
66
  # The default number of bytes to allow parameter keys to take up.
60
67
  # This helps prevent a rogue client from flooding a Request.
61
68
  self.key_space_limit = 65536
62
69
 
63
- # Default depth at which the parameter parser will raise an exception for
64
- # being too deep. This helps prevent SystemStackErrors
65
- self.param_depth_limit = 100
66
- #
67
- # The maximum number of parts a request can contain. Accepting to many part
68
- # can lead to the server running out of file handles.
69
- # Set to `0` for no limit.
70
- self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
71
-
72
70
  # Stolen from Mongrel, with some small modifications:
73
71
  # Parses a query string by breaking it up at the '&'
74
72
  # and ';' characters. You can also use this to parse
@@ -98,6 +96,11 @@ module Rack
98
96
  end
99
97
  module_function :parse_query
100
98
 
99
+ # parse_nested_query expands a query string into structural types. Supported
100
+ # types are Arrays, Hashes and basic value types. It is possible to supply
101
+ # query strings with parameters of conflicting types, in this case a
102
+ # ParameterTypeError is raised. Users are encouraged to return a 400 in this
103
+ # case.
101
104
  def parse_nested_query(qs, d = nil)
102
105
  params = KeySpaceConstrainedParams.new
103
106
 
@@ -108,12 +111,15 @@ module Rack
108
111
  end
109
112
 
110
113
  return params.to_params_hash
114
+ rescue ArgumentError => e
115
+ raise InvalidParameterError, e.message
111
116
  end
112
117
  module_function :parse_nested_query
113
118
 
114
- def normalize_params(params, name, v = nil, depth = Utils.param_depth_limit)
115
- raise RangeError if depth <= 0
116
-
119
+ # normalize_params recursively expands parameters into structural types. If
120
+ # the structural types represented by two different parameter names are in
121
+ # conflict, a ParameterTypeError is raised.
122
+ def normalize_params(params, name, v = nil)
117
123
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
118
124
  k = $1 || ''
119
125
  after = $' || ''
@@ -122,23 +128,25 @@ module Rack
122
128
 
123
129
  if after == ""
124
130
  params[k] = v
131
+ elsif after == "["
132
+ params[name] = v
125
133
  elsif after == "[]"
126
134
  params[k] ||= []
127
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
135
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
128
136
  params[k] << v
129
137
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
130
138
  child_key = $1
131
139
  params[k] ||= []
132
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
140
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
133
141
  if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
134
- normalize_params(params[k].last, child_key, v, depth - 1)
142
+ normalize_params(params[k].last, child_key, v)
135
143
  else
136
- params[k] << normalize_params(params.class.new, child_key, v, depth - 1)
144
+ params[k] << normalize_params(params.class.new, child_key, v)
137
145
  end
138
146
  else
139
147
  params[k] ||= params.class.new
140
- raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
141
- params[k] = normalize_params(params[k], after, v, depth - 1)
148
+ raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
149
+ params[k] = normalize_params(params[k], after, v)
142
150
  end
143
151
 
144
152
  return params
@@ -170,12 +178,12 @@ module Rack
170
178
  when Hash
171
179
  value.map { |k, v|
172
180
  build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
173
- }.join("&")
174
- when String
181
+ }.reject(&:empty?).join('&')
182
+ when nil
183
+ prefix
184
+ else
175
185
  raise ArgumentError, "value must be a Hash" if prefix.nil?
176
186
  "#{prefix}=#{escape(value)}"
177
- else
178
- prefix
179
187
  end
180
188
  end
181
189
  module_function :build_nested_query
@@ -195,13 +203,14 @@ module Rack
195
203
  def best_q_match(q_value_header, available_mimes)
196
204
  values = q_values(q_value_header)
197
205
 
198
- values.map do |req_mime, quality|
199
- match = available_mimes.first { |am| Rack::Mime.match?(am, req_mime) }
206
+ matches = values.map do |req_mime, quality|
207
+ match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
200
208
  next unless match
201
209
  [match, quality]
202
210
  end.compact.sort_by do |match, quality|
203
211
  (match.split('/', 2).count('*') * -10) + quality
204
- end.last.first
212
+ end.last
213
+ matches && matches.first
205
214
  end
206
215
  module_function :best_q_match
207
216
 
@@ -216,7 +225,7 @@ module Rack
216
225
  if //.respond_to?(:encoding)
217
226
  ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
218
227
  else
219
- # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwhise
228
+ # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwise
220
229
  # TODO doesn't apply to jruby, so a better condition above might be preferable?
221
230
  ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
222
231
  end
@@ -247,10 +256,8 @@ module Rack
247
256
  encoding_candidates.push("identity")
248
257
  end
249
258
 
250
- expanded_accept_encoding.find_all { |m, q|
251
- q == 0.0
252
- }.each { |m, _|
253
- encoding_candidates.delete(m)
259
+ expanded_accept_encoding.each { |m, q|
260
+ encoding_candidates.delete(m) if q == 0.0
254
261
  }
255
262
 
256
263
  return (encoding_candidates & available_encodings)[0]
@@ -260,9 +267,9 @@ module Rack
260
267
  def set_cookie_header!(header, key, value)
261
268
  case value
262
269
  when Hash
263
- domain = "; domain=" + value[:domain] if value[:domain]
264
- path = "; path=" + value[:path] if value[:path]
265
- max_age = "; max-age=" + value[:max_age] if value[:max_age]
270
+ domain = "; domain=" + value[:domain] if value[:domain]
271
+ path = "; path=" + value[:path] if value[:path]
272
+ max_age = "; max-age=" + value[:max_age].to_s if value[:max_age]
266
273
  # There is an RFC mess in the area of date formatting for Cookies. Not
267
274
  # only are there contradicting RFCs and examples within RFC text, but
268
275
  # there are also numerous conflicting names of fields and partially
@@ -289,7 +296,7 @@ module Rack
289
296
  expires = "; expires=" +
290
297
  rfc2822(value[:expires].clone.gmtime) if value[:expires]
291
298
  secure = "; secure" if value[:secure]
292
- httponly = "; HttpOnly" if value[:httponly]
299
+ httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
293
300
  value = value[:value]
294
301
  end
295
302
  value = [value] unless Array === value
@@ -363,7 +370,7 @@ module Rack
363
370
  # of '% %b %Y'.
364
371
  # It assumes that the time is in GMT to comply to the RFC 2109.
365
372
  #
366
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough
373
+ # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
367
374
  # that I'm certain someone implemented only that option.
368
375
  # Do not use %a and %b from Time.strptime, it would use localized names for
369
376
  # weekday and month.
@@ -409,6 +416,11 @@ module Rack
409
416
  module_function :byte_ranges
410
417
 
411
418
  # Constant time string comparison.
419
+ #
420
+ # NOTE: the values compared should be of fixed length, such as strings
421
+ # that have aready been processed by HMAC. This should not be used
422
+ # on variable length plaintext strings because it could leak length info
423
+ # via timing attacks.
412
424
  def secure_compare(a, b)
413
425
  return false unless bytesize(a) == bytesize(b)
414
426
 
@@ -540,7 +552,11 @@ module Rack
540
552
  hash.keys.each do |key|
541
553
  value = hash[key]
542
554
  if value.kind_of?(self.class)
543
- hash[key] = value.to_params_hash
555
+ if value.object_id == self.object_id
556
+ hash[key] = hash
557
+ else
558
+ hash[key] = value.to_params_hash
559
+ end
544
560
  elsif value.kind_of?(Array)
545
561
  value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
546
562
  end
@@ -551,9 +567,9 @@ module Rack
551
567
 
552
568
  # Every standard HTTP code mapped to the appropriate message.
553
569
  # Generated with:
554
- # irb -ropen-uri -rnokogiri
555
- # > Nokogiri::XML(open("http://www.iana.org/assignments/http-status-codes/http-status-codes.xml")).css("record").each{|r|
556
- # puts "#{r.css('value').text} => '#{r.css('description').text}'"}
570
+ # ruby -ropen-uri -rnokogiri -e "Nokogiri::XML(open(
571
+ # 'http://www.iana.org/assignments/http-status-codes/http-status-codes.xml')).css('record').each{|r|
572
+ # name = r.css('description').text; puts %Q[#{r.css('value').text} => '#{name}',] unless name == 'Unassigned' }"
557
573
  HTTP_STATUS_CODES = {
558
574
  100 => 'Continue',
559
575
  101 => 'Switching Protocols',
@@ -595,15 +611,13 @@ module Rack
595
611
  415 => 'Unsupported Media Type',
596
612
  416 => 'Requested Range Not Satisfiable',
597
613
  417 => 'Expectation Failed',
614
+ 418 => 'I\'m a teapot',
598
615
  422 => 'Unprocessable Entity',
599
616
  423 => 'Locked',
600
617
  424 => 'Failed Dependency',
601
- 425 => 'Reserved for WebDAV advanced collections expired proposal',
602
618
  426 => 'Upgrade Required',
603
- 427 => 'Unassigned',
604
619
  428 => 'Precondition Required',
605
620
  429 => 'Too Many Requests',
606
- 430 => 'Unassigned',
607
621
  431 => 'Request Header Fields Too Large',
608
622
  500 => 'Internal Server Error',
609
623
  501 => 'Not Implemented',
@@ -614,7 +628,6 @@ module Rack
614
628
  506 => 'Variant Also Negotiates (Experimental)',
615
629
  507 => 'Insufficient Storage',
616
630
  508 => 'Loop Detected',
617
- 509 => 'Unassigned',
618
631
  510 => 'Not Extended',
619
632
  511 => 'Network Authentication Required'
620
633
  }
@@ -623,7 +636,7 @@ module Rack
623
636
  STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304)
624
637
 
625
638
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
626
- [message.downcase.gsub(/\s|-/, '_').to_sym, code]
639
+ [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
627
640
  }.flatten]
628
641
 
629
642
  def status_code(status)
@@ -637,5 +650,23 @@ module Rack
637
650
 
638
651
  Multipart = Rack::Multipart
639
652
 
653
+ PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
654
+
655
+ def clean_path_info(path_info)
656
+ parts = path_info.split PATH_SEPS
657
+
658
+ clean = []
659
+
660
+ parts.each do |part|
661
+ next if part.empty? || part == '.'
662
+ part == '..' ? clean.pop : clean << part
663
+ end
664
+
665
+ clean.unshift '/' if parts.empty? || parts.first.empty?
666
+
667
+ ::File.join(*clean)
668
+ end
669
+ module_function :clean_path_info
670
+
640
671
  end
641
672
  end