rack 2.2.7 → 3.1.8

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