rack 2.2.10 → 3.1.8

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 +332 -90
  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 +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 +840 -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 +161 -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 +213 -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 +260 -123
  43. data/lib/rack/response.rb +151 -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 +236 -237
  53. data/lib/rack/version.rb +1 -9
  54. data/lib/rack.rb +13 -89
  55. metadata +13 -39
  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,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
- RFC2396_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new
27
+ URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
28
28
 
29
29
  class << self
30
30
  attr_accessor :default_query_parser
31
31
  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)
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)
35
36
 
36
37
  module_function
37
38
 
@@ -43,13 +44,13 @@ module Rack
43
44
  # Like URI escaping, but with %20 instead of +. Strictly speaking this is
44
45
  # true URI escaping.
45
46
  def escape_path(s)
46
- RFC2396_PARSER.escape s
47
+ URI_PARSER.escape s
47
48
  end
48
49
 
49
50
  # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
50
51
  # unescaping query parameters or form components.
51
52
  def unescape_path(s)
52
- RFC2396_PARSER.unescape s
53
+ URI_PARSER.unescape s
53
54
  end
54
55
 
55
56
  # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
@@ -86,14 +87,6 @@ module Rack
86
87
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
87
88
  end
88
89
 
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
90
  if defined?(Process::CLOCK_MONOTONIC)
98
91
  def clock_time
99
92
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -132,13 +125,13 @@ module Rack
132
125
  }.join("&")
133
126
  when Hash
134
127
  value.map { |k, v|
135
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
128
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
136
129
  }.delete_if(&:empty?).join('&')
137
130
  when nil
138
- prefix
131
+ escape(prefix)
139
132
  else
140
133
  raise ArgumentError, "value must be a Hash" if prefix.nil?
141
- "#{prefix}=#{escape(value)}"
134
+ "#{escape(prefix)}=#{escape(value)}"
142
135
  end
143
136
  end
144
137
 
@@ -153,6 +146,20 @@ module Rack
153
146
  end
154
147
  end
155
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
+
156
163
  # Return best accept value to use, based on the algorithm
157
164
  # in RFC 2616 Section 14. If there are multiple best
158
165
  # matches (same specificity and quality), the value returned
@@ -167,23 +174,19 @@ module Rack
167
174
  end.compact.sort_by do |match, quality|
168
175
  (match.split('/', 2).count('*') * -10) + quality
169
176
  end.last
170
- matches && matches.first
177
+ matches&.first
171
178
  end
172
179
 
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
- # 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] }
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
187
190
  end
188
191
 
189
192
  def select_best_encoding(available_encodings, accept_encoding)
@@ -218,145 +221,199 @@ module Rack
218
221
  (encoding_candidates & available_encodings)[0]
219
222
  end
220
223
 
221
- def parse_cookies(env)
222
- parse_cookies_header env[HTTP_COOKIE]
223
- 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
224
236
 
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|
237
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
232
238
  next if cookie.empty?
233
239
  key, value = cookie.split('=', 2)
234
240
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
235
241
  end
236
242
  end
237
243
 
238
- 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)
239
295
  case value
240
296
  when Hash
297
+ key = escape_cookie_key(key) unless value[:escape_key] == false
241
298
  domain = "; domain=#{value[:domain]}" if value[:domain]
242
299
  path = "; path=#{value[:path]}" if value[:path]
243
300
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
244
301
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
245
302
  secure = "; secure" if value[:secure]
246
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
303
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
247
304
  same_site =
248
305
  case value[:same_site]
249
306
  when false, nil
250
307
  nil
251
308
  when :none, 'None', :None
252
- '; SameSite=None'
309
+ '; samesite=none'
253
310
  when :lax, 'Lax', :Lax
254
- '; SameSite=Lax'
311
+ '; samesite=lax'
255
312
  when true, :strict, 'Strict', :Strict
256
- '; SameSite=Strict'
313
+ '; samesite=strict'
257
314
  else
258
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
315
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
259
316
  end
317
+ partitioned = "; partitioned" if value[:partitioned]
260
318
  value = value[:value]
319
+ else
320
+ key = escape_cookie_key(key)
261
321
  end
322
+
262
323
  value = [value] unless Array === value
263
324
 
264
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
265
- "#{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
266
328
 
267
- case header
268
- when nil, ''
269
- cookie
270
- when String
271
- [header, cookie].join("\n")
272
- when Array
273
- (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
274
344
  else
275
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
345
+ headers[SET_COOKIE] = set_cookie_header(key, value)
276
346
  end
277
347
  end
278
348
 
279
- def set_cookie_header!(header, key, value)
280
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
281
- 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: ''))
282
365
  end
283
366
 
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
367
+ def delete_cookie_header!(headers, key, value = {})
368
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
313
369
 
314
- def delete_cookie_header!(header, key, value = {})
315
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
316
- nil
370
+ return nil
317
371
  end
318
372
 
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))
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
328
398
 
399
+ return header
329
400
  end
330
401
 
331
402
  def rfc2822(time)
332
403
  time.rfc2822
333
404
  end
334
405
 
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
406
  # Parses the "Range:" header, if present, into an array of Range objects.
351
407
  # Returns nil if the header is missing or syntactically invalid.
352
408
  # Returns an empty array if none of the ranges are satisfiable.
353
409
  def byte_ranges(env, size)
354
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
355
410
  get_byte_ranges env['HTTP_RANGE'], size
356
411
  end
357
412
 
358
413
  def get_byte_ranges(http_range, size)
359
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?
360
417
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
361
418
  ranges = []
362
419
  $1.split(/,\s*/).each do |range_spec|
@@ -382,25 +439,35 @@ module Rack
382
439
  ranges << (r0..r1) if r0 <= r1
383
440
  end
384
441
 
385
- return [] if ranges.map(&:size).inject(0, :+) > size
442
+ return [] if ranges.map(&:size).sum > size
386
443
 
387
444
  ranges
388
445
  end
389
446
 
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
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
398
464
 
399
- l = a.unpack("C*")
465
+ l = a.unpack("C*")
400
466
 
401
- r, i = 0, -1
402
- b.each_byte { |v| r |= v ^ l[i += 1] }
403
- r == 0
467
+ r, i = 0, -1
468
+ b.each_byte { |v| r |= v ^ l[i += 1] }
469
+ r == 0
470
+ end
404
471
  end
405
472
 
406
473
  # Context allows the use of a compatible middleware at different points
@@ -429,101 +496,12 @@ module Rack
429
496
  end
430
497
  end
431
498
 
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
499
  # Every standard HTTP code mapped to the appropriate message.
523
500
  # 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,"'
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)"
527
505
  HTTP_STATUS_CODES = {
528
506
  100 => 'Continue',
529
507
  101 => 'Switching Protocols',
@@ -545,7 +523,6 @@ module Rack
545
523
  303 => 'See Other',
546
524
  304 => 'Not Modified',
547
525
  305 => 'Use Proxy',
548
- 306 => '(Unused)',
549
526
  307 => 'Temporary Redirect',
550
527
  308 => 'Permanent Redirect',
551
528
  400 => 'Bad Request',
@@ -561,13 +538,13 @@ module Rack
561
538
  410 => 'Gone',
562
539
  411 => 'Length Required',
563
540
  412 => 'Precondition Failed',
564
- 413 => 'Payload Too Large',
541
+ 413 => 'Content Too Large',
565
542
  414 => 'URI Too Long',
566
543
  415 => 'Unsupported Media Type',
567
544
  416 => 'Range Not Satisfiable',
568
545
  417 => 'Expectation Failed',
569
546
  421 => 'Misdirected Request',
570
- 422 => 'Unprocessable Entity',
547
+ 422 => 'Unprocessable Content',
571
548
  423 => 'Locked',
572
549
  424 => 'Failed Dependency',
573
550
  425 => 'Too Early',
@@ -575,7 +552,7 @@ module Rack
575
552
  428 => 'Precondition Required',
576
553
  429 => 'Too Many Requests',
577
554
  431 => 'Request Header Fields Too Large',
578
- 451 => 'Unavailable for Legal Reasons',
555
+ 451 => 'Unavailable For Legal Reasons',
579
556
  500 => 'Internal Server Error',
580
557
  501 => 'Not Implemented',
581
558
  502 => 'Bad Gateway',
@@ -585,8 +562,6 @@ module Rack
585
562
  506 => 'Variant Also Negotiates',
586
563
  507 => 'Insufficient Storage',
587
564
  508 => 'Loop Detected',
588
- 509 => 'Bandwidth Limit Exceeded',
589
- 510 => 'Not Extended',
590
565
  511 => 'Network Authentication Required'
591
566
  }
592
567
 
@@ -594,12 +569,36 @@ module Rack
594
569
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
595
570
 
596
571
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
597
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
572
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
598
573
  }.flatten]
599
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
+
600
589
  def status_code(status)
601
590
  if status.is_a?(Symbol)
602
- 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
603
602
  else
604
603
  status.to_i
605
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.10"
15
+ RELEASE = "3.1.8"
24
16
 
25
17
  # Return the Rack release as a dotted string.
26
18
  def self.release