rack 2.2.8.1 → 3.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +301 -81
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +204 -131
  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 +838 -644
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -271
  32. data/lib/rack/mock_request.rb +171 -0
  33. data/lib/rack/mock_response.rb +124 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +215 -90
  36. data/lib/rack/multipart/uploaded_file.rb +4 -0
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +81 -102
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +250 -123
  43. data/lib/rack/response.rb +146 -66
  44. data/lib/rack/rewindable_input.rb +24 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +30 -25
  47. data/lib/rack/show_exceptions.rb +21 -4
  48. data/lib/rack/show_status.rb +17 -7
  49. data/lib/rack/static.rb +8 -8
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +3 -1
  52. data/lib/rack/utils.rb +232 -233
  53. data/lib/rack/version.rb +1 -9
  54. data/lib/rack.rb +13 -89
  55. metadata +15 -41
  56. data/README.rdoc +0 -320
  57. data/Rakefile +0 -130
  58. data/bin/rackup +0 -5
  59. data/contrib/rack.png +0 -0
  60. data/contrib/rack.svg +0 -150
  61. data/contrib/rack_logo.svg +0 -164
  62. data/contrib/rdoc.css +0 -412
  63. data/example/lobster.ru +0 -6
  64. data/example/protectedlobster.rb +0 -16
  65. data/example/protectedlobster.ru +0 -10
  66. data/lib/rack/auth/digest/md5.rb +0 -131
  67. data/lib/rack/auth/digest/nonce.rb +0 -54
  68. data/lib/rack/auth/digest/params.rb +0 -54
  69. data/lib/rack/auth/digest/request.rb +0 -43
  70. data/lib/rack/chunked.rb +0 -117
  71. data/lib/rack/core_ext/regexp.rb +0 -14
  72. data/lib/rack/file.rb +0 -7
  73. data/lib/rack/handler/cgi.rb +0 -59
  74. data/lib/rack/handler/fastcgi.rb +0 -100
  75. data/lib/rack/handler/lsws.rb +0 -61
  76. data/lib/rack/handler/scgi.rb +0 -71
  77. data/lib/rack/handler/thin.rb +0 -36
  78. data/lib/rack/handler/webrick.rb +0 -129
  79. data/lib/rack/handler.rb +0 -104
  80. data/lib/rack/lobster.rb +0 -70
  81. data/lib/rack/server.rb +0 -466
  82. data/lib/rack/session/abstract/id.rb +0 -523
  83. data/lib/rack/session/cookie.rb +0 -204
  84. data/lib/rack/session/memcache.rb +0 -10
  85. data/lib/rack/session/pool.rb +0 -85
  86. 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,13 +124,13 @@ 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
 
@@ -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 }
309
-
310
- cookies.join("\n")
311
- end
366
+ def delete_cookie_header!(headers, key, value = {})
367
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
312
368
 
313
- def delete_cookie_header!(header, key, value = {})
314
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
315
- nil
369
+ return nil
316
370
  end
317
371
 
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|
@@ -386,20 +443,30 @@ module Rack
386
443
  ranges
387
444
  end
388
445
 
389
- # Constant time string comparison.
390
- #
391
- # NOTE: the values compared should be of fixed length, such as strings
392
- # that have already been processed by HMAC. This should not be used
393
- # on variable length plaintext strings because it could leak length info
394
- # via timing attacks.
395
- def secure_compare(a, b)
396
- 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
456
+
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
397
463
 
398
- l = a.unpack("C*")
464
+ l = a.unpack("C*")
399
465
 
400
- r, i = 0, -1
401
- b.each_byte { |v| r |= v ^ l[i += 1] }
402
- r == 0
466
+ r, i = 0, -1
467
+ b.each_byte { |v| r |= v ^ l[i += 1] }
468
+ r == 0
469
+ end
403
470
  end
404
471
 
405
472
  # Context allows the use of a compatible middleware at different points
@@ -428,101 +495,12 @@ module Rack
428
495
  end
429
496
  end
430
497
 
431
- # A case-insensitive Hash that preserves the original case of a
432
- # header when set.
433
- #
434
- # @api private
435
- class HeaderHash < Hash # :nodoc:
436
- def self.[](headers)
437
- if headers.is_a?(HeaderHash) && !headers.frozen?
438
- return headers
439
- else
440
- return self.new(headers)
441
- end
442
- end
443
-
444
- def initialize(hash = {})
445
- super()
446
- @names = {}
447
- hash.each { |k, v| self[k] = v }
448
- end
449
-
450
- # on dup/clone, we need to duplicate @names hash
451
- def initialize_copy(other)
452
- super
453
- @names = other.names.dup
454
- end
455
-
456
- # on clear, we need to clear @names hash
457
- def clear
458
- super
459
- @names.clear
460
- end
461
-
462
- def each
463
- super do |k, v|
464
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
465
- end
466
- end
467
-
468
- def to_hash
469
- hash = {}
470
- each { |k, v| hash[k] = v }
471
- hash
472
- end
473
-
474
- def [](k)
475
- super(k) || super(@names[k.downcase])
476
- end
477
-
478
- def []=(k, v)
479
- canonical = k.downcase.freeze
480
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
481
- @names[canonical] = k
482
- super k, v
483
- end
484
-
485
- def delete(k)
486
- canonical = k.downcase
487
- result = super @names.delete(canonical)
488
- result
489
- end
490
-
491
- def include?(k)
492
- super || @names.include?(k.downcase)
493
- end
494
-
495
- alias_method :has_key?, :include?
496
- alias_method :member?, :include?
497
- alias_method :key?, :include?
498
-
499
- def merge!(other)
500
- other.each { |k, v| self[k] = v }
501
- self
502
- end
503
-
504
- def merge(other)
505
- hash = dup
506
- hash.merge! other
507
- end
508
-
509
- def replace(other)
510
- clear
511
- other.each { |k, v| self[k] = v }
512
- self
513
- end
514
-
515
- protected
516
- def names
517
- @names
518
- end
519
- end
520
-
521
498
  # Every standard HTTP code mapped to the appropriate message.
522
499
  # Generated with:
523
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
524
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
525
- # 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)"
526
504
  HTTP_STATUS_CODES = {
527
505
  100 => 'Continue',
528
506
  101 => 'Switching Protocols',
@@ -544,7 +522,6 @@ module Rack
544
522
  303 => 'See Other',
545
523
  304 => 'Not Modified',
546
524
  305 => 'Use Proxy',
547
- 306 => '(Unused)',
548
525
  307 => 'Temporary Redirect',
549
526
  308 => 'Permanent Redirect',
550
527
  400 => 'Bad Request',
@@ -560,13 +537,13 @@ module Rack
560
537
  410 => 'Gone',
561
538
  411 => 'Length Required',
562
539
  412 => 'Precondition Failed',
563
- 413 => 'Payload Too Large',
540
+ 413 => 'Content Too Large',
564
541
  414 => 'URI Too Long',
565
542
  415 => 'Unsupported Media Type',
566
543
  416 => 'Range Not Satisfiable',
567
544
  417 => 'Expectation Failed',
568
545
  421 => 'Misdirected Request',
569
- 422 => 'Unprocessable Entity',
546
+ 422 => 'Unprocessable Content',
570
547
  423 => 'Locked',
571
548
  424 => 'Failed Dependency',
572
549
  425 => 'Too Early',
@@ -574,7 +551,7 @@ module Rack
574
551
  428 => 'Precondition Required',
575
552
  429 => 'Too Many Requests',
576
553
  431 => 'Request Header Fields Too Large',
577
- 451 => 'Unavailable for Legal Reasons',
554
+ 451 => 'Unavailable For Legal Reasons',
578
555
  500 => 'Internal Server Error',
579
556
  501 => 'Not Implemented',
580
557
  502 => 'Bad Gateway',
@@ -584,8 +561,6 @@ module Rack
584
561
  506 => 'Variant Also Negotiates',
585
562
  507 => 'Insufficient Storage',
586
563
  508 => 'Loop Detected',
587
- 509 => 'Bandwidth Limit Exceeded',
588
- 510 => 'Not Extended',
589
564
  511 => 'Network Authentication Required'
590
565
  }
591
566
 
@@ -593,12 +568,36 @@ module Rack
593
568
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
594
569
 
595
570
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
596
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
571
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
597
572
  }.flatten]
598
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
+
599
588
  def status_code(status)
600
589
  if status.is_a?(Symbol)
601
- 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
602
601
  else
603
602
  status.to_i
604
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.8.1"
15
+ RELEASE = "3.1.5"
24
16
 
25
17
  # Return the Rack release as a dotted string.
26
18
  def self.release