rack 2.2.9 → 3.1.10

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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +348 -88
  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 +25 -19
  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 +840 -644
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +8 -3
  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 +161 -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 +213 -95
  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 +260 -123
  44. data/lib/rack/response.rb +151 -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 +235 -235
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -44
  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 -204
  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,33 @@ 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
-
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
+ URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
27
28
 
28
29
  class << self
29
30
  attr_accessor :default_query_parser
30
31
  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)
32
+ # The default amount of nesting to allowed by hash parameters.
33
+ # This helps prevent a rogue client from triggering a possible stack overflow
34
+ # when parsing parameters.
35
+ self.default_query_parser = QueryParser.make_default(32)
34
36
 
35
37
  module_function
36
38
 
@@ -42,13 +44,13 @@ module Rack
42
44
  # Like URI escaping, but with %20 instead of +. Strictly speaking this is
43
45
  # true URI escaping.
44
46
  def escape_path(s)
45
- ::URI::DEFAULT_PARSER.escape s
47
+ URI_PARSER.escape s
46
48
  end
47
49
 
48
50
  # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
49
51
  # unescaping query parameters or form components.
50
52
  def unescape_path(s)
51
- ::URI::DEFAULT_PARSER.unescape s
53
+ URI_PARSER.unescape s
52
54
  end
53
55
 
54
56
  # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
@@ -85,14 +87,6 @@ module Rack
85
87
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
86
88
  end
87
89
 
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
90
  if defined?(Process::CLOCK_MONOTONIC)
97
91
  def clock_time
98
92
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -131,13 +125,13 @@ module Rack
131
125
  }.join("&")
132
126
  when Hash
133
127
  value.map { |k, v|
134
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
128
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
135
129
  }.delete_if(&:empty?).join('&')
136
130
  when nil
137
- prefix
131
+ escape(prefix)
138
132
  else
139
133
  raise ArgumentError, "value must be a Hash" if prefix.nil?
140
- "#{prefix}=#{escape(value)}"
134
+ "#{escape(prefix)}=#{escape(value)}"
141
135
  end
142
136
  end
143
137
 
@@ -152,6 +146,20 @@ module Rack
152
146
  end
153
147
  end
154
148
 
149
+ def forwarded_values(forwarded_header)
150
+ return nil unless forwarded_header
151
+ forwarded_header = forwarded_header.to_s.gsub("\n", ";")
152
+
153
+ forwarded_header.split(';').each_with_object({}) do |field, values|
154
+ field.split(',').each do |pair|
155
+ pair = pair.split('=').map(&:strip).join('=')
156
+ return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
157
+ (values[$1.downcase.to_sym] ||= []) << $2
158
+ end
159
+ end
160
+ end
161
+ module_function :forwarded_values
162
+
155
163
  # Return best accept value to use, based on the algorithm
156
164
  # in RFC 2616 Section 14. If there are multiple best
157
165
  # matches (same specificity and quality), the value returned
@@ -166,23 +174,19 @@ module Rack
166
174
  end.compact.sort_by do |match, quality|
167
175
  (match.split('/', 2).count('*') * -10) + quality
168
176
  end.last
169
- matches && matches.first
177
+ matches&.first
170
178
  end
171
179
 
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] }
180
+ # Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
181
+ # doesn't get monkey-patched by rails
182
+ if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
183
+ define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
184
+ else
185
+ require 'cgi/escape'
186
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
187
+ def escape_html(string)
188
+ CGI.escapeHTML(string.to_s)
189
+ end
186
190
  end
187
191
 
188
192
  def select_best_encoding(available_encodings, accept_encoding)
@@ -217,145 +221,199 @@ module Rack
217
221
  (encoding_candidates & available_encodings)[0]
218
222
  end
219
223
 
220
- def parse_cookies(env)
221
- parse_cookies_header env[HTTP_COOKIE]
222
- end
224
+ # :call-seq:
225
+ # parse_cookies_header(value) -> hash
226
+ #
227
+ # Parse cookies from the provided header +value+ according to RFC6265. The
228
+ # syntax for cookie headers only supports semicolons. Returns a map of
229
+ # cookie +key+ to cookie +value+.
230
+ #
231
+ # parse_cookies_header('myname=myvalue; max-age=0')
232
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
233
+ #
234
+ def parse_cookies_header(value)
235
+ return {} unless value
223
236
 
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|
237
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
231
238
  next if cookie.empty?
232
239
  key, value = cookie.split('=', 2)
233
240
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
234
241
  end
235
242
  end
236
243
 
237
- def add_cookie_to_header(header, key, value)
244
+ # :call-seq:
245
+ # parse_cookies(env) -> hash
246
+ #
247
+ # Parse cookies from the provided request environment using
248
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
249
+ #
250
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
251
+ # # => {'myname' => 'myvalue'}
252
+ #
253
+ def parse_cookies(env)
254
+ parse_cookies_header env[HTTP_COOKIE]
255
+ end
256
+
257
+ # A valid cookie key according to RFC2616.
258
+ # 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: ( ) < > @ , ; : \ " / [ ] ? = { }.
259
+ VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
260
+ private_constant :VALID_COOKIE_KEY
261
+
262
+ private def escape_cookie_key(key)
263
+ if key =~ VALID_COOKIE_KEY
264
+ key
265
+ else
266
+ 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
267
+ escape(key)
268
+ end
269
+ end
270
+
271
+ # :call-seq:
272
+ # set_cookie_header(key, value) -> encoded string
273
+ #
274
+ # Generate an encoded string using the provided +key+ and +value+ suitable
275
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
276
+ # instance of either +String+ or +Hash+.
277
+ #
278
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
279
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
280
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
281
+ # details about the interpretation of these fields, consult
282
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
283
+ #
284
+ # An extra cookie attribute +escape_key+ can be provided to control whether
285
+ # or not the cookie key is URL encoded. If explicitly set to +false+, the
286
+ # cookie key name will not be url encoded (escaped). The default is +true+.
287
+ #
288
+ # set_cookie_header("myname", "myvalue")
289
+ # # => "myname=myvalue"
290
+ #
291
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
292
+ # # => "myname=myvalue; max-age=10"
293
+ #
294
+ def set_cookie_header(key, value)
238
295
  case value
239
296
  when Hash
297
+ key = escape_cookie_key(key) unless value[:escape_key] == false
240
298
  domain = "; domain=#{value[:domain]}" if value[:domain]
241
299
  path = "; path=#{value[:path]}" if value[:path]
242
300
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
243
301
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
244
302
  secure = "; secure" if value[:secure]
245
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
303
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
246
304
  same_site =
247
305
  case value[:same_site]
248
306
  when false, nil
249
307
  nil
250
308
  when :none, 'None', :None
251
- '; SameSite=None'
309
+ '; samesite=none'
252
310
  when :lax, 'Lax', :Lax
253
- '; SameSite=Lax'
311
+ '; samesite=lax'
254
312
  when true, :strict, 'Strict', :Strict
255
- '; SameSite=Strict'
313
+ '; samesite=strict'
256
314
  else
257
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
315
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
258
316
  end
317
+ partitioned = "; partitioned" if value[:partitioned]
259
318
  value = value[:value]
319
+ else
320
+ key = escape_cookie_key(key)
260
321
  end
322
+
261
323
  value = [value] unless Array === value
262
324
 
263
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
264
- "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
325
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
326
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
327
+ end
265
328
 
266
- case header
267
- when nil, ''
268
- cookie
269
- when String
270
- [header, cookie].join("\n")
271
- when Array
272
- (header + [cookie]).join("\n")
329
+ # :call-seq:
330
+ # set_cookie_header!(headers, key, value) -> header value
331
+ #
332
+ # Append a cookie in the specified headers with the given cookie +key+ and
333
+ # +value+ using set_cookie_header.
334
+ #
335
+ # If the headers already contains a +set-cookie+ key, it will be converted
336
+ # to an +Array+ if not already, and appended to.
337
+ def set_cookie_header!(headers, key, value)
338
+ if header = headers[SET_COOKIE]
339
+ if header.is_a?(Array)
340
+ header << set_cookie_header(key, value)
341
+ else
342
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
343
+ end
273
344
  else
274
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
345
+ headers[SET_COOKIE] = set_cookie_header(key, value)
275
346
  end
276
347
  end
277
348
 
278
- def set_cookie_header!(header, key, value)
279
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
280
- nil
349
+ # :call-seq:
350
+ # delete_set_cookie_header(key, value = {}) -> encoded string
351
+ #
352
+ # Generate an encoded string based on the given +key+ and +value+ using
353
+ # set_cookie_header for the purpose of causing the specified cookie to be
354
+ # deleted. The +value+ may be an instance of +Hash+ and can include
355
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
356
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
357
+ # +value+. When used with the +set-cookie+ header, it will cause the client
358
+ # to *remove* any matching cookie.
359
+ #
360
+ # delete_set_cookie_header("myname")
361
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
362
+ #
363
+ def delete_set_cookie_header(key, value = {})
364
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
281
365
  end
282
366
 
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
367
+ def delete_cookie_header!(headers, key, value = {})
368
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
312
369
 
313
- def delete_cookie_header!(header, key, value = {})
314
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
315
- nil
370
+ return nil
316
371
  end
317
372
 
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))
373
+ # :call-seq:
374
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
375
+ #
376
+ # Set an expired cookie in the specified headers with the given cookie
377
+ # +key+ and +value+ using delete_set_cookie_header. This causes
378
+ # the client to immediately delete the specified cookie.
379
+ #
380
+ # delete_set_cookie_header!(nil, "mycookie")
381
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
382
+ #
383
+ # If the header is non-nil, it will be modified in place.
384
+ #
385
+ # header = []
386
+ # delete_set_cookie_header!(header, "mycookie")
387
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
388
+ # header
389
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
390
+ #
391
+ def delete_set_cookie_header!(header, key, value = {})
392
+ if header
393
+ header = Array(header)
394
+ header << delete_set_cookie_header(key, value)
395
+ else
396
+ header = delete_set_cookie_header(key, value)
397
+ end
327
398
 
399
+ return header
328
400
  end
329
401
 
330
402
  def rfc2822(time)
331
403
  time.rfc2822
332
404
  end
333
405
 
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
406
  # Parses the "Range:" header, if present, into an array of Range objects.
350
407
  # Returns nil if the header is missing or syntactically invalid.
351
408
  # Returns an empty array if none of the ranges are satisfiable.
352
409
  def byte_ranges(env, size)
353
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
354
410
  get_byte_ranges env['HTTP_RANGE'], size
355
411
  end
356
412
 
357
413
  def get_byte_ranges(http_range, size)
358
414
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
415
+ # Ignore Range when file size is 0 to avoid a 416 error.
416
+ return nil if size.zero?
359
417
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
360
418
  ranges = []
361
419
  $1.split(/,\s*/).each do |range_spec|
@@ -386,20 +444,30 @@ module Rack
386
444
  ranges
387
445
  end
388
446
 
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
447
+ # :nocov:
448
+ if defined?(OpenSSL.fixed_length_secure_compare)
449
+ # Constant time string comparison.
450
+ #
451
+ # NOTE: the values compared should be of fixed length, such as strings
452
+ # that have already been processed by HMAC. This should not be used
453
+ # on variable length plaintext strings because it could leak length info
454
+ # via timing attacks.
455
+ def secure_compare(a, b)
456
+ return false unless a.bytesize == b.bytesize
457
+
458
+ OpenSSL.fixed_length_secure_compare(a, b)
459
+ end
460
+ # :nocov:
461
+ else
462
+ def secure_compare(a, b)
463
+ return false unless a.bytesize == b.bytesize
397
464
 
398
- l = a.unpack("C*")
465
+ l = a.unpack("C*")
399
466
 
400
- r, i = 0, -1
401
- b.each_byte { |v| r |= v ^ l[i += 1] }
402
- r == 0
467
+ r, i = 0, -1
468
+ b.each_byte { |v| r |= v ^ l[i += 1] }
469
+ r == 0
470
+ end
403
471
  end
404
472
 
405
473
  # Context allows the use of a compatible middleware at different points
@@ -428,101 +496,12 @@ module Rack
428
496
  end
429
497
  end
430
498
 
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
499
  # Every standard HTTP code mapped to the appropriate message.
522
500
  # 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,"'
501
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
502
+ # | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
503
+ # .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
504
+ # .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
526
505
  HTTP_STATUS_CODES = {
527
506
  100 => 'Continue',
528
507
  101 => 'Switching Protocols',
@@ -544,7 +523,6 @@ module Rack
544
523
  303 => 'See Other',
545
524
  304 => 'Not Modified',
546
525
  305 => 'Use Proxy',
547
- 306 => '(Unused)',
548
526
  307 => 'Temporary Redirect',
549
527
  308 => 'Permanent Redirect',
550
528
  400 => 'Bad Request',
@@ -560,13 +538,13 @@ module Rack
560
538
  410 => 'Gone',
561
539
  411 => 'Length Required',
562
540
  412 => 'Precondition Failed',
563
- 413 => 'Payload Too Large',
541
+ 413 => 'Content Too Large',
564
542
  414 => 'URI Too Long',
565
543
  415 => 'Unsupported Media Type',
566
544
  416 => 'Range Not Satisfiable',
567
545
  417 => 'Expectation Failed',
568
546
  421 => 'Misdirected Request',
569
- 422 => 'Unprocessable Entity',
547
+ 422 => 'Unprocessable Content',
570
548
  423 => 'Locked',
571
549
  424 => 'Failed Dependency',
572
550
  425 => 'Too Early',
@@ -574,7 +552,7 @@ module Rack
574
552
  428 => 'Precondition Required',
575
553
  429 => 'Too Many Requests',
576
554
  431 => 'Request Header Fields Too Large',
577
- 451 => 'Unavailable for Legal Reasons',
555
+ 451 => 'Unavailable For Legal Reasons',
578
556
  500 => 'Internal Server Error',
579
557
  501 => 'Not Implemented',
580
558
  502 => 'Bad Gateway',
@@ -584,8 +562,6 @@ module Rack
584
562
  506 => 'Variant Also Negotiates',
585
563
  507 => 'Insufficient Storage',
586
564
  508 => 'Loop Detected',
587
- 509 => 'Bandwidth Limit Exceeded',
588
- 510 => 'Not Extended',
589
565
  511 => 'Network Authentication Required'
590
566
  }
591
567
 
@@ -593,12 +569,36 @@ module Rack
593
569
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
594
570
 
595
571
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
596
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
572
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
597
573
  }.flatten]
598
574
 
575
+ OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
576
+ payload_too_large: 413,
577
+ unprocessable_entity: 422,
578
+ bandwidth_limit_exceeded: 509,
579
+ not_extended: 510
580
+ }.freeze
581
+ private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
582
+
583
+ OBSOLETE_SYMBOL_MAPPINGS = {
584
+ payload_too_large: :content_too_large,
585
+ unprocessable_entity: :unprocessable_content
586
+ }.freeze
587
+ private_constant :OBSOLETE_SYMBOL_MAPPINGS
588
+
599
589
  def status_code(status)
600
590
  if status.is_a?(Symbol)
601
- SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
591
+ SYMBOL_TO_STATUS_CODE.fetch(status) do
592
+ fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
593
+ message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
594
+ if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
595
+ # message = "#{message} Please use #{canonical_symbol.inspect} instead."
596
+ # For now, let's not emit any warning when there is a mapping.
597
+ else
598
+ warn message, uplevel: 3
599
+ end
600
+ fallback_code
601
+ end
602
602
  else
603
603
  status.to_i
604
604
  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.9"
15
+ RELEASE = "3.1.10"
24
16
 
25
17
  # Return the Rack release as a dotted string.
26
18
  def self.release