rack 2.2.12 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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