rack 2.2.6 → 3.1.2

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +295 -72
  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 +864 -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 +171 -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 +232 -94
  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 +246 -121
  44. data/lib/rack/response.rb +146 -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 +247 -244
  54. data/lib/rack/version.rb +1 -9
  55. data/lib/rack.rb +13 -89
  56. metadata +15 -41
  57. data/README.rdoc +0 -306
  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,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
-
28
28
  class << self
29
29
  attr_accessor :default_query_parser
30
30
  end
31
- # The default number of bytes to allow parameter keys to take up.
32
- # This helps prevent a rogue client from flooding a Request.
33
- self.default_query_parser = QueryParser.make_default(65536, 100)
31
+ # The default amount of nesting to allowed by hash parameters.
32
+ # This helps prevent a rogue client from triggering a possible stack overflow
33
+ # when parsing parameters.
34
+ self.default_query_parser = QueryParser.make_default(32)
34
35
 
35
36
  module_function
36
37
 
@@ -58,13 +59,24 @@ module Rack
58
59
  end
59
60
 
60
61
  class << self
61
- attr_accessor :multipart_part_limit
62
+ attr_accessor :multipart_total_part_limit
63
+
64
+ attr_accessor :multipart_file_limit
65
+
66
+ # multipart_part_limit is the original name of multipart_file_limit, but
67
+ # the limit only counts parts with filenames.
68
+ alias multipart_part_limit multipart_file_limit
69
+ alias multipart_part_limit= multipart_file_limit=
62
70
  end
63
71
 
64
- # The maximum number of parts a request can contain. Accepting too many part
65
- # can lead to the server running out of file handles.
72
+ # The maximum number of file parts a request can contain. Accepting too
73
+ # many parts can lead to the server running out of file handles.
66
74
  # Set to `0` for no limit.
67
- self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
75
+ self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
76
+
77
+ # The maximum total number of parts a request can contain. Accepting too
78
+ # many can lead to excessive memory use and parsing time.
79
+ self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
68
80
 
69
81
  def self.param_depth_limit
70
82
  default_query_parser.param_depth_limit
@@ -74,14 +86,6 @@ module Rack
74
86
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
75
87
  end
76
88
 
77
- def self.key_space_limit
78
- default_query_parser.key_space_limit
79
- end
80
-
81
- def self.key_space_limit=(v)
82
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
83
- end
84
-
85
89
  if defined?(Process::CLOCK_MONOTONIC)
86
90
  def clock_time
87
91
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -120,19 +124,19 @@ module Rack
120
124
  }.join("&")
121
125
  when Hash
122
126
  value.map { |k, v|
123
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
127
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
124
128
  }.delete_if(&:empty?).join('&')
125
129
  when nil
126
- prefix
130
+ escape(prefix)
127
131
  else
128
132
  raise ArgumentError, "value must be a Hash" if prefix.nil?
129
- "#{prefix}=#{escape(value)}"
133
+ "#{escape(prefix)}=#{escape(value)}"
130
134
  end
131
135
  end
132
136
 
133
137
  def q_values(q_value_header)
134
- q_value_header.to_s.split(/\s*,\s*/).map do |part|
135
- value, parameters = part.split(/\s*;\s*/, 2)
138
+ q_value_header.to_s.split(',').map do |part|
139
+ value, parameters = part.split(';', 2).map(&:strip)
136
140
  quality = 1.0
137
141
  if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
138
142
  quality = md[1].to_f
@@ -141,6 +145,20 @@ module Rack
141
145
  end
142
146
  end
143
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
+
144
162
  # Return best accept value to use, based on the algorithm
145
163
  # in RFC 2616 Section 14. If there are multiple best
146
164
  # matches (same specificity and quality), the value returned
@@ -155,24 +173,11 @@ module Rack
155
173
  end.compact.sort_by do |match, quality|
156
174
  (match.split('/', 2).count('*') * -10) + quality
157
175
  end.last
158
- matches && matches.first
176
+ matches&.first
159
177
  end
160
178
 
161
- ESCAPE_HTML = {
162
- "&" => "&amp;",
163
- "<" => "&lt;",
164
- ">" => "&gt;",
165
- "'" => "&#x27;",
166
- '"' => "&quot;",
167
- "/" => "&#x2F;"
168
- }
169
-
170
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
171
-
172
179
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
173
- def escape_html(string)
174
- string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
175
- end
180
+ define_method(:escape_html, CGI.method(:escapeHTML))
176
181
 
177
182
  def select_best_encoding(available_encodings, accept_encoding)
178
183
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
@@ -206,159 +211,214 @@ module Rack
206
211
  (encoding_candidates & available_encodings)[0]
207
212
  end
208
213
 
209
- def parse_cookies(env)
210
- parse_cookies_header env[HTTP_COOKIE]
211
- 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
212
226
 
213
- def parse_cookies_header(header)
214
- # According to RFC 6265:
215
- # The syntax for cookie headers only supports semicolons
216
- # User Agent -> Server ==
217
- # Cookie: SID=31d4d96e407aad42; lang=en-US
218
- return {} unless header
219
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
227
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
220
228
  next if cookie.empty?
221
229
  key, value = cookie.split('=', 2)
222
230
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
223
231
  end
224
232
  end
225
233
 
226
- 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)
227
285
  case value
228
286
  when Hash
287
+ key = escape_cookie_key(key) unless value[:escape_key] == false
229
288
  domain = "; domain=#{value[:domain]}" if value[:domain]
230
289
  path = "; path=#{value[:path]}" if value[:path]
231
290
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
232
291
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
233
292
  secure = "; secure" if value[:secure]
234
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
293
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
235
294
  same_site =
236
295
  case value[:same_site]
237
296
  when false, nil
238
297
  nil
239
298
  when :none, 'None', :None
240
- '; SameSite=None'
299
+ '; samesite=none'
241
300
  when :lax, 'Lax', :Lax
242
- '; SameSite=Lax'
301
+ '; samesite=lax'
243
302
  when true, :strict, 'Strict', :Strict
244
- '; SameSite=Strict'
303
+ '; samesite=strict'
245
304
  else
246
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
305
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
247
306
  end
307
+ partitioned = "; partitioned" if value[:partitioned]
248
308
  value = value[:value]
309
+ else
310
+ key = escape_cookie_key(key)
249
311
  end
312
+
250
313
  value = [value] unless Array === value
251
314
 
252
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
253
- "#{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
254
318
 
255
- case header
256
- when nil, ''
257
- cookie
258
- when String
259
- [header, cookie].join("\n")
260
- when Array
261
- (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
262
334
  else
263
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
335
+ headers[SET_COOKIE] = set_cookie_header(key, value)
264
336
  end
265
337
  end
266
338
 
267
- def set_cookie_header!(header, key, value)
268
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
269
- 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: ''))
270
355
  end
271
356
 
272
- def make_delete_cookie_header(header, key, value)
273
- case header
274
- when nil, ''
275
- cookies = []
276
- when String
277
- cookies = header.split("\n")
278
- when Array
279
- cookies = header
280
- end
281
-
282
- key = escape(key)
283
- domain = value[:domain]
284
- path = value[:path]
285
- regexp = if domain
286
- if path
287
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
288
- else
289
- /\A#{key}=.*domain=#{domain}(?:;|$)/
290
- end
291
- elsif path
292
- /\A#{key}=.*path=#{path}(?:;|$)/
293
- else
294
- /\A#{key}=/
295
- end
296
-
297
- cookies.reject! { |cookie| regexp.match? cookie }
298
-
299
- cookies.join("\n")
300
- end
357
+ def delete_cookie_header!(headers, key, value = {})
358
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
301
359
 
302
- def delete_cookie_header!(header, key, value = {})
303
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
304
- nil
360
+ return nil
305
361
  end
306
362
 
307
- # Adds a cookie that will *remove* a cookie from the client. Hence the
308
- # strange method name.
309
- def add_remove_cookie_to_header(header, key, value = {})
310
- new_header = make_delete_cookie_header(header, key, value)
311
-
312
- add_cookie_to_header(new_header, key,
313
- { value: '', path: nil, domain: nil,
314
- max_age: '0',
315
- 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
316
388
 
389
+ return header
317
390
  end
318
391
 
319
392
  def rfc2822(time)
320
393
  time.rfc2822
321
394
  end
322
395
 
323
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
324
- # of '% %b %Y'.
325
- # It assumes that the time is in GMT to comply to the RFC 2109.
326
- #
327
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
328
- # that I'm certain someone implemented only that option.
329
- # Do not use %a and %b from Time.strptime, it would use localized names for
330
- # weekday and month.
331
- #
332
- def rfc2109(time)
333
- wday = RFC2822_DAY_NAME[time.wday]
334
- mon = RFC2822_MONTH_NAME[time.mon - 1]
335
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
336
- end
337
-
338
396
  # Parses the "Range:" header, if present, into an array of Range objects.
339
397
  # Returns nil if the header is missing or syntactically invalid.
340
398
  # Returns an empty array if none of the ranges are satisfiable.
341
399
  def byte_ranges(env, size)
342
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
343
400
  get_byte_ranges env['HTTP_RANGE'], size
344
401
  end
345
402
 
346
403
  def get_byte_ranges(http_range, size)
347
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?
348
407
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
349
408
  ranges = []
350
409
  $1.split(/,\s*/).each do |range_spec|
351
- return nil unless range_spec =~ /(\d*)-(\d*)/
352
- r0, r1 = $1, $2
353
- if r0.empty?
354
- return nil if r1.empty?
410
+ return nil unless range_spec.include?('-')
411
+ range = range_spec.split('-')
412
+ r0, r1 = range[0], range[1]
413
+ if r0.nil? || r0.empty?
414
+ return nil if r1.nil?
355
415
  # suffix-byte-range-spec, represents trailing suffix of file
356
416
  r0 = size - r1.to_i
357
417
  r0 = 0 if r0 < 0
358
418
  r1 = size - 1
359
419
  else
360
420
  r0 = r0.to_i
361
- if r1.empty?
421
+ if r1.nil?
362
422
  r1 = size - 1
363
423
  else
364
424
  r1 = r1.to_i
@@ -368,23 +428,36 @@ module Rack
368
428
  end
369
429
  ranges << (r0..r1) if r0 <= r1
370
430
  end
431
+
432
+ return [] if ranges.map(&:size).sum > size
433
+
371
434
  ranges
372
435
  end
373
436
 
374
- # Constant time string comparison.
375
- #
376
- # NOTE: the values compared should be of fixed length, such as strings
377
- # that have already been processed by HMAC. This should not be used
378
- # on variable length plaintext strings because it could leak length info
379
- # via timing attacks.
380
- def secure_compare(a, b)
381
- 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
382
454
 
383
- l = a.unpack("C*")
455
+ l = a.unpack("C*")
384
456
 
385
- r, i = 0, -1
386
- b.each_byte { |v| r |= v ^ l[i += 1] }
387
- r == 0
457
+ r, i = 0, -1
458
+ b.each_byte { |v| r |= v ^ l[i += 1] }
459
+ r == 0
460
+ end
388
461
  end
389
462
 
390
463
  # Context allows the use of a compatible middleware at different points
@@ -413,101 +486,12 @@ module Rack
413
486
  end
414
487
  end
415
488
 
416
- # A case-insensitive Hash that preserves the original case of a
417
- # header when set.
418
- #
419
- # @api private
420
- class HeaderHash < Hash # :nodoc:
421
- def self.[](headers)
422
- if headers.is_a?(HeaderHash) && !headers.frozen?
423
- return headers
424
- else
425
- return self.new(headers)
426
- end
427
- end
428
-
429
- def initialize(hash = {})
430
- super()
431
- @names = {}
432
- hash.each { |k, v| self[k] = v }
433
- end
434
-
435
- # on dup/clone, we need to duplicate @names hash
436
- def initialize_copy(other)
437
- super
438
- @names = other.names.dup
439
- end
440
-
441
- # on clear, we need to clear @names hash
442
- def clear
443
- super
444
- @names.clear
445
- end
446
-
447
- def each
448
- super do |k, v|
449
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
450
- end
451
- end
452
-
453
- def to_hash
454
- hash = {}
455
- each { |k, v| hash[k] = v }
456
- hash
457
- end
458
-
459
- def [](k)
460
- super(k) || super(@names[k.downcase])
461
- end
462
-
463
- def []=(k, v)
464
- canonical = k.downcase.freeze
465
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
466
- @names[canonical] = k
467
- super k, v
468
- end
469
-
470
- def delete(k)
471
- canonical = k.downcase
472
- result = super @names.delete(canonical)
473
- result
474
- end
475
-
476
- def include?(k)
477
- super || @names.include?(k.downcase)
478
- end
479
-
480
- alias_method :has_key?, :include?
481
- alias_method :member?, :include?
482
- alias_method :key?, :include?
483
-
484
- def merge!(other)
485
- other.each { |k, v| self[k] = v }
486
- self
487
- end
488
-
489
- def merge(other)
490
- hash = dup
491
- hash.merge! other
492
- end
493
-
494
- def replace(other)
495
- clear
496
- other.each { |k, v| self[k] = v }
497
- self
498
- end
499
-
500
- protected
501
- def names
502
- @names
503
- end
504
- end
505
-
506
489
  # Every standard HTTP code mapped to the appropriate message.
507
490
  # Generated with:
508
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
509
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
510
- # 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)"
511
495
  HTTP_STATUS_CODES = {
512
496
  100 => 'Continue',
513
497
  101 => 'Switching Protocols',
@@ -529,7 +513,6 @@ module Rack
529
513
  303 => 'See Other',
530
514
  304 => 'Not Modified',
531
515
  305 => 'Use Proxy',
532
- 306 => '(Unused)',
533
516
  307 => 'Temporary Redirect',
534
517
  308 => 'Permanent Redirect',
535
518
  400 => 'Bad Request',
@@ -545,13 +528,13 @@ module Rack
545
528
  410 => 'Gone',
546
529
  411 => 'Length Required',
547
530
  412 => 'Precondition Failed',
548
- 413 => 'Payload Too Large',
531
+ 413 => 'Content Too Large',
549
532
  414 => 'URI Too Long',
550
533
  415 => 'Unsupported Media Type',
551
534
  416 => 'Range Not Satisfiable',
552
535
  417 => 'Expectation Failed',
553
536
  421 => 'Misdirected Request',
554
- 422 => 'Unprocessable Entity',
537
+ 422 => 'Unprocessable Content',
555
538
  423 => 'Locked',
556
539
  424 => 'Failed Dependency',
557
540
  425 => 'Too Early',
@@ -559,7 +542,7 @@ module Rack
559
542
  428 => 'Precondition Required',
560
543
  429 => 'Too Many Requests',
561
544
  431 => 'Request Header Fields Too Large',
562
- 451 => 'Unavailable for Legal Reasons',
545
+ 451 => 'Unavailable For Legal Reasons',
563
546
  500 => 'Internal Server Error',
564
547
  501 => 'Not Implemented',
565
548
  502 => 'Bad Gateway',
@@ -569,8 +552,6 @@ module Rack
569
552
  506 => 'Variant Also Negotiates',
570
553
  507 => 'Insufficient Storage',
571
554
  508 => 'Loop Detected',
572
- 509 => 'Bandwidth Limit Exceeded',
573
- 510 => 'Not Extended',
574
555
  511 => 'Network Authentication Required'
575
556
  }
576
557
 
@@ -578,12 +559,34 @@ module Rack
578
559
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
579
560
 
580
561
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
581
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
562
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
582
563
  }.flatten]
583
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
+
584
579
  def status_code(status)
585
580
  if status.is_a?(Symbol)
586
- 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
587
590
  else
588
591
  status.to_i
589
592
  end