rack 2.2.7 → 3.1.3

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +291 -78
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +213 -136
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +3 -1
  9. data/lib/rack/auth/basic.rb +1 -4
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +21 -3
  12. data/lib/rack/builder.rb +102 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +23 -18
  15. data/lib/rack/conditional_get.rb +18 -15
  16. data/lib/rack/constants.rb +67 -0
  17. data/lib/rack/content_length.rb +12 -16
  18. data/lib/rack/content_type.rb +8 -5
  19. data/lib/rack/deflater.rb +40 -26
  20. data/lib/rack/directory.rb +9 -3
  21. data/lib/rack/etag.rb +14 -23
  22. data/lib/rack/events.rb +4 -0
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +9 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +864 -681
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +9 -4
  30. data/lib/rack/method_override.rb +5 -1
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -271
  33. data/lib/rack/mock_request.rb +171 -0
  34. data/lib/rack/mock_response.rb +124 -0
  35. data/lib/rack/multipart/generator.rb +7 -5
  36. data/lib/rack/multipart/parser.rb +218 -91
  37. data/lib/rack/multipart/uploaded_file.rb +4 -0
  38. data/lib/rack/multipart.rb +53 -40
  39. data/lib/rack/null_logger.rb +9 -0
  40. data/lib/rack/query_parser.rb +81 -102
  41. data/lib/rack/recursive.rb +2 -0
  42. data/lib/rack/reloader.rb +0 -2
  43. data/lib/rack/request.rb +248 -123
  44. data/lib/rack/response.rb +146 -66
  45. data/lib/rack/rewindable_input.rb +24 -5
  46. data/lib/rack/runtime.rb +7 -6
  47. data/lib/rack/sendfile.rb +30 -25
  48. data/lib/rack/show_exceptions.rb +21 -4
  49. data/lib/rack/show_status.rb +17 -7
  50. data/lib/rack/static.rb +8 -8
  51. data/lib/rack/tempfile_reaper.rb +15 -4
  52. data/lib/rack/urlmap.rb +3 -1
  53. data/lib/rack/utils.rb +237 -235
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -41
  57. data/README.rdoc +0 -320
  58. data/Rakefile +0 -130
  59. data/bin/rackup +0 -5
  60. data/contrib/rack.png +0 -0
  61. data/contrib/rack.svg +0 -150
  62. data/contrib/rack_logo.svg +0 -164
  63. data/contrib/rdoc.css +0 -412
  64. data/example/lobster.ru +0 -6
  65. data/example/protectedlobster.rb +0 -16
  66. data/example/protectedlobster.ru +0 -10
  67. data/lib/rack/auth/digest/md5.rb +0 -131
  68. data/lib/rack/auth/digest/nonce.rb +0 -54
  69. data/lib/rack/auth/digest/params.rb +0 -54
  70. data/lib/rack/auth/digest/request.rb +0 -43
  71. data/lib/rack/chunked.rb +0 -117
  72. data/lib/rack/core_ext/regexp.rb +0 -14
  73. data/lib/rack/file.rb +0 -7
  74. data/lib/rack/handler/cgi.rb +0 -59
  75. data/lib/rack/handler/fastcgi.rb +0 -100
  76. data/lib/rack/handler/lsws.rb +0 -61
  77. data/lib/rack/handler/scgi.rb +0 -71
  78. data/lib/rack/handler/thin.rb +0 -36
  79. data/lib/rack/handler/webrick.rb +0 -129
  80. data/lib/rack/handler.rb +0 -104
  81. data/lib/rack/lobster.rb +0 -70
  82. data/lib/rack/server.rb +0 -466
  83. data/lib/rack/session/abstract/id.rb +0 -523
  84. data/lib/rack/session/cookie.rb +0 -203
  85. data/lib/rack/session/memcache.rb +0 -10
  86. data/lib/rack/session/pool.rb +0 -85
  87. data/rack.gemspec +0 -46
data/lib/rack/utils.rb CHANGED
@@ -6,31 +6,32 @@ require 'fileutils'
6
6
  require 'set'
7
7
  require 'tempfile'
8
8
  require 'time'
9
+ require 'erb'
9
10
 
10
11
  require_relative 'query_parser'
12
+ require_relative 'mime'
13
+ require_relative 'headers'
14
+ require_relative 'constants'
11
15
 
12
16
  module Rack
13
17
  # Rack::Utils contains a grab-bag of useful methods for writing web
14
18
  # applications adopted from all kinds of Ruby libraries.
15
19
 
16
20
  module Utils
17
- (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
18
-
19
21
  ParameterTypeError = QueryParser::ParameterTypeError
20
22
  InvalidParameterError = QueryParser::InvalidParameterError
23
+ ParamsTooDeepError = QueryParser::ParamsTooDeepError
21
24
  DEFAULT_SEP = QueryParser::DEFAULT_SEP
22
25
  COMMON_SEP = QueryParser::COMMON_SEP
23
26
  KeySpaceConstrainedParams = QueryParser::Params
24
27
 
25
- RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
26
- RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
27
-
28
28
  class << self
29
29
  attr_accessor :default_query_parser
30
30
  end
31
- # The default number of bytes to allow parameter keys to take up.
32
- # This helps prevent a rogue client from flooding a Request.
33
- self.default_query_parser = QueryParser.make_default(65536, 100)
31
+ # The default amount of nesting to allowed by hash parameters.
32
+ # This helps prevent a rogue client from triggering a possible stack overflow
33
+ # when parsing parameters.
34
+ self.default_query_parser = QueryParser.make_default(32)
34
35
 
35
36
  module_function
36
37
 
@@ -85,14 +86,6 @@ module Rack
85
86
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
86
87
  end
87
88
 
88
- def self.key_space_limit
89
- default_query_parser.key_space_limit
90
- end
91
-
92
- def self.key_space_limit=(v)
93
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
94
- end
95
-
96
89
  if defined?(Process::CLOCK_MONOTONIC)
97
90
  def clock_time
98
91
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -131,19 +124,19 @@ module Rack
131
124
  }.join("&")
132
125
  when Hash
133
126
  value.map { |k, v|
134
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
127
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
135
128
  }.delete_if(&:empty?).join('&')
136
129
  when nil
137
- prefix
130
+ escape(prefix)
138
131
  else
139
132
  raise ArgumentError, "value must be a Hash" if prefix.nil?
140
- "#{prefix}=#{escape(value)}"
133
+ "#{escape(prefix)}=#{escape(value)}"
141
134
  end
142
135
  end
143
136
 
144
137
  def q_values(q_value_header)
145
- q_value_header.to_s.split(/\s*,\s*/).map do |part|
146
- value, parameters = part.split(/\s*;\s*/, 2)
138
+ q_value_header.to_s.split(',').map do |part|
139
+ value, parameters = part.split(';', 2).map(&:strip)
147
140
  quality = 1.0
148
141
  if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
149
142
  quality = md[1].to_f
@@ -152,6 +145,20 @@ module Rack
152
145
  end
153
146
  end
154
147
 
148
+ def forwarded_values(forwarded_header)
149
+ return nil unless forwarded_header
150
+ forwarded_header = forwarded_header.to_s.gsub("\n", ";")
151
+
152
+ forwarded_header.split(';').each_with_object({}) do |field, values|
153
+ field.split(',').each do |pair|
154
+ pair = pair.split('=').map(&:strip).join('=')
155
+ return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
156
+ (values[$1.downcase.to_sym] ||= []) << $2
157
+ end
158
+ end
159
+ end
160
+ module_function :forwarded_values
161
+
155
162
  # Return best accept value to use, based on the algorithm
156
163
  # in RFC 2616 Section 14. If there are multiple best
157
164
  # matches (same specificity and quality), the value returned
@@ -166,23 +173,19 @@ module Rack
166
173
  end.compact.sort_by do |match, quality|
167
174
  (match.split('/', 2).count('*') * -10) + quality
168
175
  end.last
169
- matches && matches.first
176
+ matches&.first
170
177
  end
171
178
 
172
- ESCAPE_HTML = {
173
- "&" => "&amp;",
174
- "<" => "&lt;",
175
- ">" => "&gt;",
176
- "'" => "&#x27;",
177
- '"' => "&quot;",
178
- "/" => "&#x2F;"
179
- }
180
-
181
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
182
-
183
- # Escape ampersands, brackets and quotes to their HTML/XML entities.
184
- def escape_html(string)
185
- string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
179
+ # Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
180
+ # doesn't get monkey-patched by rails
181
+ if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
182
+ define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
183
+ else
184
+ require 'cgi/escape'
185
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
186
+ def escape_html(string)
187
+ CGI.escapeHTML(string.to_s)
188
+ end
186
189
  end
187
190
 
188
191
  def select_best_encoding(available_encodings, accept_encoding)
@@ -217,145 +220,199 @@ module Rack
217
220
  (encoding_candidates & available_encodings)[0]
218
221
  end
219
222
 
220
- def parse_cookies(env)
221
- parse_cookies_header env[HTTP_COOKIE]
222
- end
223
+ # :call-seq:
224
+ # parse_cookies_header(value) -> hash
225
+ #
226
+ # Parse cookies from the provided header +value+ according to RFC6265. The
227
+ # syntax for cookie headers only supports semicolons. Returns a map of
228
+ # cookie +key+ to cookie +value+.
229
+ #
230
+ # parse_cookies_header('myname=myvalue; max-age=0')
231
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
232
+ #
233
+ def parse_cookies_header(value)
234
+ return {} unless value
223
235
 
224
- def parse_cookies_header(header)
225
- # According to RFC 6265:
226
- # The syntax for cookie headers only supports semicolons
227
- # User Agent -> Server ==
228
- # Cookie: SID=31d4d96e407aad42; lang=en-US
229
- return {} unless header
230
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
236
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
231
237
  next if cookie.empty?
232
238
  key, value = cookie.split('=', 2)
233
239
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
234
240
  end
235
241
  end
236
242
 
237
- def add_cookie_to_header(header, key, value)
243
+ # :call-seq:
244
+ # parse_cookies(env) -> hash
245
+ #
246
+ # Parse cookies from the provided request environment using
247
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
248
+ #
249
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
250
+ # # => {'myname' => 'myvalue'}
251
+ #
252
+ def parse_cookies(env)
253
+ parse_cookies_header env[HTTP_COOKIE]
254
+ end
255
+
256
+ # A valid cookie key according to RFC2616.
257
+ # A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ " / [ ] ? = { }.
258
+ VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
259
+ private_constant :VALID_COOKIE_KEY
260
+
261
+ private def escape_cookie_key(key)
262
+ if key =~ VALID_COOKIE_KEY
263
+ key
264
+ else
265
+ warn "Cookie key #{key.inspect} is not valid according to RFC2616; it will be escaped. This behaviour is deprecated and will be removed in a future version of Rack.", uplevel: 2
266
+ escape(key)
267
+ end
268
+ end
269
+
270
+ # :call-seq:
271
+ # set_cookie_header(key, value) -> encoded string
272
+ #
273
+ # Generate an encoded string using the provided +key+ and +value+ suitable
274
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
275
+ # instance of either +String+ or +Hash+.
276
+ #
277
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
278
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
279
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
280
+ # details about the interpretation of these fields, consult
281
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
282
+ #
283
+ # An extra cookie attribute +escape_key+ can be provided to control whether
284
+ # or not the cookie key is URL encoded. If explicitly set to +false+, the
285
+ # cookie key name will not be url encoded (escaped). The default is +true+.
286
+ #
287
+ # set_cookie_header("myname", "myvalue")
288
+ # # => "myname=myvalue"
289
+ #
290
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
291
+ # # => "myname=myvalue; max-age=10"
292
+ #
293
+ def set_cookie_header(key, value)
238
294
  case value
239
295
  when Hash
296
+ key = escape_cookie_key(key) unless value[:escape_key] == false
240
297
  domain = "; domain=#{value[:domain]}" if value[:domain]
241
298
  path = "; path=#{value[:path]}" if value[:path]
242
299
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
243
300
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
244
301
  secure = "; secure" if value[:secure]
245
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
302
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
246
303
  same_site =
247
304
  case value[:same_site]
248
305
  when false, nil
249
306
  nil
250
307
  when :none, 'None', :None
251
- '; SameSite=None'
308
+ '; samesite=none'
252
309
  when :lax, 'Lax', :Lax
253
- '; SameSite=Lax'
310
+ '; samesite=lax'
254
311
  when true, :strict, 'Strict', :Strict
255
- '; SameSite=Strict'
312
+ '; samesite=strict'
256
313
  else
257
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
314
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
258
315
  end
316
+ partitioned = "; partitioned" if value[:partitioned]
259
317
  value = value[:value]
318
+ else
319
+ key = escape_cookie_key(key)
260
320
  end
321
+
261
322
  value = [value] unless Array === value
262
323
 
263
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
264
- "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
324
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
325
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
326
+ end
265
327
 
266
- case header
267
- when nil, ''
268
- cookie
269
- when String
270
- [header, cookie].join("\n")
271
- when Array
272
- (header + [cookie]).join("\n")
328
+ # :call-seq:
329
+ # set_cookie_header!(headers, key, value) -> header value
330
+ #
331
+ # Append a cookie in the specified headers with the given cookie +key+ and
332
+ # +value+ using set_cookie_header.
333
+ #
334
+ # If the headers already contains a +set-cookie+ key, it will be converted
335
+ # to an +Array+ if not already, and appended to.
336
+ def set_cookie_header!(headers, key, value)
337
+ if header = headers[SET_COOKIE]
338
+ if header.is_a?(Array)
339
+ header << set_cookie_header(key, value)
340
+ else
341
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
342
+ end
273
343
  else
274
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
344
+ headers[SET_COOKIE] = set_cookie_header(key, value)
275
345
  end
276
346
  end
277
347
 
278
- def set_cookie_header!(header, key, value)
279
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
280
- nil
348
+ # :call-seq:
349
+ # delete_set_cookie_header(key, value = {}) -> encoded string
350
+ #
351
+ # Generate an encoded string based on the given +key+ and +value+ using
352
+ # set_cookie_header for the purpose of causing the specified cookie to be
353
+ # deleted. The +value+ may be an instance of +Hash+ and can include
354
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
355
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
356
+ # +value+. When used with the +set-cookie+ header, it will cause the client
357
+ # to *remove* any matching cookie.
358
+ #
359
+ # delete_set_cookie_header("myname")
360
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
361
+ #
362
+ def delete_set_cookie_header(key, value = {})
363
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
281
364
  end
282
365
 
283
- def make_delete_cookie_header(header, key, value)
284
- case header
285
- when nil, ''
286
- cookies = []
287
- when String
288
- cookies = header.split("\n")
289
- when Array
290
- cookies = header
291
- end
292
-
293
- key = escape(key)
294
- domain = value[:domain]
295
- path = value[:path]
296
- regexp = if domain
297
- if path
298
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
299
- else
300
- /\A#{key}=.*domain=#{domain}(?:;|$)/
301
- end
302
- elsif path
303
- /\A#{key}=.*path=#{path}(?:;|$)/
304
- else
305
- /\A#{key}=/
306
- end
307
-
308
- cookies.reject! { |cookie| regexp.match? cookie }
366
+ def delete_cookie_header!(headers, key, value = {})
367
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
309
368
 
310
- cookies.join("\n")
369
+ return nil
311
370
  end
312
371
 
313
- def delete_cookie_header!(header, key, value = {})
314
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
315
- nil
316
- end
317
-
318
- # Adds a cookie that will *remove* a cookie from the client. Hence the
319
- # strange method name.
320
- def add_remove_cookie_to_header(header, key, value = {})
321
- new_header = make_delete_cookie_header(header, key, value)
322
-
323
- add_cookie_to_header(new_header, key,
324
- { value: '', path: nil, domain: nil,
325
- max_age: '0',
326
- expires: Time.at(0) }.merge(value))
372
+ # :call-seq:
373
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
374
+ #
375
+ # Set an expired cookie in the specified headers with the given cookie
376
+ # +key+ and +value+ using delete_set_cookie_header. This causes
377
+ # the client to immediately delete the specified cookie.
378
+ #
379
+ # delete_set_cookie_header!(nil, "mycookie")
380
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
381
+ #
382
+ # If the header is non-nil, it will be modified in place.
383
+ #
384
+ # header = []
385
+ # delete_set_cookie_header!(header, "mycookie")
386
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
387
+ # header
388
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
389
+ #
390
+ def delete_set_cookie_header!(header, key, value = {})
391
+ if header
392
+ header = Array(header)
393
+ header << delete_set_cookie_header(key, value)
394
+ else
395
+ header = delete_set_cookie_header(key, value)
396
+ end
327
397
 
398
+ return header
328
399
  end
329
400
 
330
401
  def rfc2822(time)
331
402
  time.rfc2822
332
403
  end
333
404
 
334
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
335
- # of '% %b %Y'.
336
- # It assumes that the time is in GMT to comply to the RFC 2109.
337
- #
338
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
339
- # that I'm certain someone implemented only that option.
340
- # Do not use %a and %b from Time.strptime, it would use localized names for
341
- # weekday and month.
342
- #
343
- def rfc2109(time)
344
- wday = RFC2822_DAY_NAME[time.wday]
345
- mon = RFC2822_MONTH_NAME[time.mon - 1]
346
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
347
- end
348
-
349
405
  # Parses the "Range:" header, if present, into an array of Range objects.
350
406
  # Returns nil if the header is missing or syntactically invalid.
351
407
  # Returns an empty array if none of the ranges are satisfiable.
352
408
  def byte_ranges(env, size)
353
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
354
409
  get_byte_ranges env['HTTP_RANGE'], size
355
410
  end
356
411
 
357
412
  def get_byte_ranges(http_range, size)
358
413
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
414
+ # Ignore Range when file size is 0 to avoid a 416 error.
415
+ return nil if size.zero?
359
416
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
360
417
  ranges = []
361
418
  $1.split(/,\s*/).each do |range_spec|
@@ -380,23 +437,36 @@ module Rack
380
437
  end
381
438
  ranges << (r0..r1) if r0 <= r1
382
439
  end
440
+
441
+ return [] if ranges.map(&:size).sum > size
442
+
383
443
  ranges
384
444
  end
385
445
 
386
- # Constant time string comparison.
387
- #
388
- # NOTE: the values compared should be of fixed length, such as strings
389
- # that have already been processed by HMAC. This should not be used
390
- # on variable length plaintext strings because it could leak length info
391
- # via timing attacks.
392
- def secure_compare(a, b)
393
- return false unless a.bytesize == b.bytesize
446
+ # :nocov:
447
+ if defined?(OpenSSL.fixed_length_secure_compare)
448
+ # Constant time string comparison.
449
+ #
450
+ # NOTE: the values compared should be of fixed length, such as strings
451
+ # that have already been processed by HMAC. This should not be used
452
+ # on variable length plaintext strings because it could leak length info
453
+ # via timing attacks.
454
+ def secure_compare(a, b)
455
+ return false unless a.bytesize == b.bytesize
394
456
 
395
- l = a.unpack("C*")
457
+ OpenSSL.fixed_length_secure_compare(a, b)
458
+ end
459
+ # :nocov:
460
+ else
461
+ def secure_compare(a, b)
462
+ return false unless a.bytesize == b.bytesize
463
+
464
+ l = a.unpack("C*")
396
465
 
397
- r, i = 0, -1
398
- b.each_byte { |v| r |= v ^ l[i += 1] }
399
- r == 0
466
+ r, i = 0, -1
467
+ b.each_byte { |v| r |= v ^ l[i += 1] }
468
+ r == 0
469
+ end
400
470
  end
401
471
 
402
472
  # Context allows the use of a compatible middleware at different points
@@ -425,101 +495,12 @@ module Rack
425
495
  end
426
496
  end
427
497
 
428
- # A case-insensitive Hash that preserves the original case of a
429
- # header when set.
430
- #
431
- # @api private
432
- class HeaderHash < Hash # :nodoc:
433
- def self.[](headers)
434
- if headers.is_a?(HeaderHash) && !headers.frozen?
435
- return headers
436
- else
437
- return self.new(headers)
438
- end
439
- end
440
-
441
- def initialize(hash = {})
442
- super()
443
- @names = {}
444
- hash.each { |k, v| self[k] = v }
445
- end
446
-
447
- # on dup/clone, we need to duplicate @names hash
448
- def initialize_copy(other)
449
- super
450
- @names = other.names.dup
451
- end
452
-
453
- # on clear, we need to clear @names hash
454
- def clear
455
- super
456
- @names.clear
457
- end
458
-
459
- def each
460
- super do |k, v|
461
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
462
- end
463
- end
464
-
465
- def to_hash
466
- hash = {}
467
- each { |k, v| hash[k] = v }
468
- hash
469
- end
470
-
471
- def [](k)
472
- super(k) || super(@names[k.downcase])
473
- end
474
-
475
- def []=(k, v)
476
- canonical = k.downcase.freeze
477
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
478
- @names[canonical] = k
479
- super k, v
480
- end
481
-
482
- def delete(k)
483
- canonical = k.downcase
484
- result = super @names.delete(canonical)
485
- result
486
- end
487
-
488
- def include?(k)
489
- super || @names.include?(k.downcase)
490
- end
491
-
492
- alias_method :has_key?, :include?
493
- alias_method :member?, :include?
494
- alias_method :key?, :include?
495
-
496
- def merge!(other)
497
- other.each { |k, v| self[k] = v }
498
- self
499
- end
500
-
501
- def merge(other)
502
- hash = dup
503
- hash.merge! other
504
- end
505
-
506
- def replace(other)
507
- clear
508
- other.each { |k, v| self[k] = v }
509
- self
510
- end
511
-
512
- protected
513
- def names
514
- @names
515
- end
516
- end
517
-
518
498
  # Every standard HTTP code mapped to the appropriate message.
519
499
  # Generated with:
520
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
521
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
522
- # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
500
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
501
+ # | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
502
+ # .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
503
+ # .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
523
504
  HTTP_STATUS_CODES = {
524
505
  100 => 'Continue',
525
506
  101 => 'Switching Protocols',
@@ -541,7 +522,6 @@ module Rack
541
522
  303 => 'See Other',
542
523
  304 => 'Not Modified',
543
524
  305 => 'Use Proxy',
544
- 306 => '(Unused)',
545
525
  307 => 'Temporary Redirect',
546
526
  308 => 'Permanent Redirect',
547
527
  400 => 'Bad Request',
@@ -557,13 +537,13 @@ module Rack
557
537
  410 => 'Gone',
558
538
  411 => 'Length Required',
559
539
  412 => 'Precondition Failed',
560
- 413 => 'Payload Too Large',
540
+ 413 => 'Content Too Large',
561
541
  414 => 'URI Too Long',
562
542
  415 => 'Unsupported Media Type',
563
543
  416 => 'Range Not Satisfiable',
564
544
  417 => 'Expectation Failed',
565
545
  421 => 'Misdirected Request',
566
- 422 => 'Unprocessable Entity',
546
+ 422 => 'Unprocessable Content',
567
547
  423 => 'Locked',
568
548
  424 => 'Failed Dependency',
569
549
  425 => 'Too Early',
@@ -571,7 +551,7 @@ module Rack
571
551
  428 => 'Precondition Required',
572
552
  429 => 'Too Many Requests',
573
553
  431 => 'Request Header Fields Too Large',
574
- 451 => 'Unavailable for Legal Reasons',
554
+ 451 => 'Unavailable For Legal Reasons',
575
555
  500 => 'Internal Server Error',
576
556
  501 => 'Not Implemented',
577
557
  502 => 'Bad Gateway',
@@ -581,8 +561,6 @@ module Rack
581
561
  506 => 'Variant Also Negotiates',
582
562
  507 => 'Insufficient Storage',
583
563
  508 => 'Loop Detected',
584
- 509 => 'Bandwidth Limit Exceeded',
585
- 510 => 'Not Extended',
586
564
  511 => 'Network Authentication Required'
587
565
  }
588
566
 
@@ -590,12 +568,36 @@ module Rack
590
568
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
591
569
 
592
570
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
593
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
571
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
594
572
  }.flatten]
595
573
 
574
+ OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
575
+ payload_too_large: 413,
576
+ unprocessable_entity: 422,
577
+ bandwidth_limit_exceeded: 509,
578
+ not_extended: 510
579
+ }.freeze
580
+ private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
581
+
582
+ OBSOLETE_SYMBOL_MAPPINGS = {
583
+ payload_too_large: :content_too_large,
584
+ unprocessable_entity: :unprocessable_content
585
+ }.freeze
586
+ private_constant :OBSOLETE_SYMBOL_MAPPINGS
587
+
596
588
  def status_code(status)
597
589
  if status.is_a?(Symbol)
598
- SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
590
+ SYMBOL_TO_STATUS_CODE.fetch(status) do
591
+ fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
592
+ message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
593
+ if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
594
+ # message = "#{message} Please use #{canonical_symbol.inspect} instead."
595
+ # For now, let's not emit any warning when there is a mapping.
596
+ else
597
+ warn message, uplevel: 3
598
+ end
599
+ fallback_code
600
+ end
599
601
  else
600
602
  status.to_i
601
603
  end
data/lib/rack/version.rb CHANGED
@@ -12,15 +12,7 @@
12
12
  # so it should be enough just to <tt>require 'rack'</tt> in your code.
13
13
 
14
14
  module Rack
15
- # The Rack protocol version number implemented.
16
- VERSION = [1, 3]
17
-
18
- # Return the Rack protocol version as a dotted string.
19
- def self.version
20
- VERSION.join(".")
21
- end
22
-
23
- RELEASE = "2.2.7"
15
+ RELEASE = "3.1.3"
24
16
 
25
17
  # Return the Rack release as a dotted string.
26
18
  def self.release