rack 2.2.3 → 3.2.7

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 +774 -69
  3. data/CONTRIBUTING.md +63 -55
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +384 -0
  6. data/SPEC.rdoc +243 -277
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +5 -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 +108 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +26 -17
  15. data/lib/rack/conditional_get.rb +20 -16
  16. data/lib/rack/constants.rb +68 -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 +15 -6
  21. data/lib/rack/etag.rb +17 -21
  22. data/lib/rack/events.rb +25 -6
  23. data/lib/rack/files.rb +16 -18
  24. data/lib/rack/head.rb +8 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +863 -705
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/media_type.rb +18 -9
  29. data/lib/rack/method_override.rb +6 -2
  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 +156 -0
  34. data/lib/rack/multipart/generator.rb +7 -5
  35. data/lib/rack/multipart/parser.rb +349 -92
  36. data/lib/rack/multipart/uploaded_file.rb +45 -4
  37. data/lib/rack/multipart.rb +53 -41
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +150 -106
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +272 -141
  43. data/lib/rack/response.rb +151 -66
  44. data/lib/rack/rewindable_input.rb +27 -5
  45. data/lib/rack/runtime.rb +7 -6
  46. data/lib/rack/sendfile.rb +70 -35
  47. data/lib/rack/show_exceptions.rb +25 -6
  48. data/lib/rack/show_status.rb +17 -9
  49. data/lib/rack/static.rb +17 -12
  50. data/lib/rack/tempfile_reaper.rb +15 -4
  51. data/lib/rack/urlmap.rb +4 -2
  52. data/lib/rack/utils.rb +351 -250
  53. data/lib/rack/version.rb +3 -15
  54. data/lib/rack.rb +13 -90
  55. metadata +16 -45
  56. data/README.rdoc +0 -306
  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 -54
  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/logger.rb +0 -20
  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,28 +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
27
+ URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
24
28
 
25
29
  class << self
26
30
  attr_accessor :default_query_parser
27
31
  end
28
- # The default number of bytes to allow parameter keys to take up.
29
- # This helps prevent a rogue client from flooding a Request.
30
- 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)
31
36
 
32
37
  module_function
33
38
 
@@ -39,13 +44,13 @@ module Rack
39
44
  # Like URI escaping, but with %20 instead of +. Strictly speaking this is
40
45
  # true URI escaping.
41
46
  def escape_path(s)
42
- ::URI::DEFAULT_PARSER.escape s
47
+ URI_PARSER.escape s
43
48
  end
44
49
 
45
50
  # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
46
51
  # unescaping query parameters or form components.
47
52
  def unescape_path(s)
48
- ::URI::DEFAULT_PARSER.unescape s
53
+ URI_PARSER.unescape s
49
54
  end
50
55
 
51
56
  # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
@@ -55,13 +60,24 @@ module Rack
55
60
  end
56
61
 
57
62
  class << self
58
- attr_accessor :multipart_part_limit
63
+ attr_accessor :multipart_total_part_limit
64
+
65
+ attr_accessor :multipart_file_limit
66
+
67
+ # multipart_part_limit is the original name of multipart_file_limit, but
68
+ # the limit only counts parts with filenames.
69
+ alias multipart_part_limit multipart_file_limit
70
+ alias multipart_part_limit= multipart_file_limit=
59
71
  end
60
72
 
61
- # The maximum number of parts a request can contain. Accepting too many part
62
- # can lead to the server running out of file handles.
73
+ # The maximum number of file parts a request can contain. Accepting too
74
+ # many parts can lead to the server running out of file handles.
63
75
  # Set to `0` for no limit.
64
- self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
76
+ self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
77
+
78
+ # The maximum total number of parts a request can contain. Accepting too
79
+ # many can lead to excessive memory use and parsing time.
80
+ self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
65
81
 
66
82
  def self.param_depth_limit
67
83
  default_query_parser.param_depth_limit
@@ -71,14 +87,6 @@ module Rack
71
87
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
72
88
  end
73
89
 
74
- def self.key_space_limit
75
- default_query_parser.key_space_limit
76
- end
77
-
78
- def self.key_space_limit=(v)
79
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
80
- end
81
-
82
90
  if defined?(Process::CLOCK_MONOTONIC)
83
91
  def clock_time
84
92
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -117,19 +125,19 @@ module Rack
117
125
  }.join("&")
118
126
  when Hash
119
127
  value.map { |k, v|
120
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
128
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
121
129
  }.delete_if(&:empty?).join('&')
122
130
  when nil
123
- prefix
131
+ escape(prefix)
124
132
  else
125
133
  raise ArgumentError, "value must be a Hash" if prefix.nil?
126
- "#{prefix}=#{escape(value)}"
134
+ "#{escape(prefix)}=#{escape(value)}"
127
135
  end
128
136
  end
129
137
 
130
138
  def q_values(q_value_header)
131
- q_value_header.to_s.split(/\s*,\s*/).map do |part|
132
- 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)
133
141
  quality = 1.0
134
142
  if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
135
143
  quality = md[1].to_f
@@ -138,6 +146,80 @@ module Rack
138
146
  end
139
147
  end
140
148
 
149
+ ALLOWED_FORWARDED_PARAMS = %w[by for host proto].map { |name| [name, name.to_sym] }.to_h.freeze
150
+ private_constant :ALLOWED_FORWARDED_PARAMS
151
+
152
+ def forwarded_values(forwarded_header)
153
+ return unless forwarded_header
154
+ header = forwarded_header.to_s.tr("\n", ";")
155
+ header.sub!(/\A[\s;,]+/, '')
156
+ num_params = num_escapes = 0
157
+ max_params = max_escapes = 1024
158
+ params = {}
159
+
160
+ # Parse parameter list
161
+ while i = header.index('=')
162
+ # Only parse up to max parameters, to avoid potential denial of service
163
+ num_params += 1
164
+ return if num_params > max_params
165
+
166
+ # Found end of parameter name, ensure forward progress in loop
167
+ param = header.slice!(0, i+1)
168
+
169
+ # Remove ending equals and preceding whitespace from parameter name
170
+ param.chomp!('=')
171
+ param.strip!
172
+ param.downcase!
173
+ return unless param = ALLOWED_FORWARDED_PARAMS[param]
174
+
175
+ if header[0] == '"'
176
+ # Parameter value is quoted, parse it, handling backslash escapes
177
+ header.slice!(0, 1)
178
+ value = String.new
179
+
180
+ while i = header.index(/(["\\])/)
181
+ c = $1
182
+
183
+ # Append all content until ending quote or escape
184
+ value << header.slice!(0, i)
185
+
186
+ # Remove either backslash or ending quote,
187
+ # ensures forward progress in loop
188
+ header.slice!(0, 1)
189
+
190
+ # stop parsing parameter value if found ending quote
191
+ break if c == '"'
192
+
193
+ # Only allow up to max escapes, to avoid potential denial of service
194
+ num_escapes += 1
195
+ return if num_escapes > max_escapes
196
+ escaped_char = header.slice!(0, 1)
197
+ value << escaped_char
198
+ end
199
+ else
200
+ if i = header.index(/[;,]/)
201
+ # Parameter value unquoted (which may be invalid), value ends at comma or semicolon
202
+ value = header.slice!(0, i)
203
+ value.sub!(/[\s;,]+\z/, '')
204
+ else
205
+ # If no ending semicolon, assume remainder of line is value and stop parsing
206
+ header.strip!
207
+ value = header
208
+ header = ''
209
+ end
210
+ value.lstrip!
211
+ end
212
+
213
+ (params[param] ||= []) << value
214
+
215
+ # skip trailing semicolons/commas/whitespace, to proceed to next parameter
216
+ header.sub!(/\A[\s;,]+/, '') unless header.empty?
217
+ end
218
+
219
+ params
220
+ end
221
+ module_function :forwarded_values
222
+
141
223
  # Return best accept value to use, based on the algorithm
142
224
  # in RFC 2616 Section 14. If there are multiple best
143
225
  # matches (same specificity and quality), the value returned
@@ -152,36 +234,60 @@ module Rack
152
234
  end.compact.sort_by do |match, quality|
153
235
  (match.split('/', 2).count('*') * -10) + quality
154
236
  end.last
155
- matches && matches.first
237
+ matches&.first
156
238
  end
157
239
 
158
- ESCAPE_HTML = {
159
- "&" => "&amp;",
160
- "<" => "&lt;",
161
- ">" => "&gt;",
162
- "'" => "&#x27;",
163
- '"' => "&quot;",
164
- "/" => "&#x2F;"
165
- }
166
-
167
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
168
-
169
- # Escape ampersands, brackets and quotes to their HTML/XML entities.
170
- def escape_html(string)
171
- string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
240
+ # Introduced in ERB 4.0. ERB::Escape is an alias for ERB::Utils which
241
+ # doesn't get monkey-patched by rails
242
+ if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
243
+ define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
244
+ # :nocov:
245
+ # Ruby 3.2/ERB 4.0 added ERB::Escape#html_escape, so the else
246
+ # branch cannot be hit on the current Ruby version.
247
+ else
248
+ require 'cgi/escape'
249
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
250
+ def escape_html(string)
251
+ CGI.escapeHTML(string.to_s)
252
+ end
253
+ # :nocov:
172
254
  end
173
255
 
256
+ # Given an array of available encoding strings, and an array of
257
+ # acceptable encodings for a request, where each element of the
258
+ # acceptable encodings array is an array where the first element
259
+ # is an encoding name and the second element is the numeric
260
+ # priority for the encoding, return the available encoding with
261
+ # the highest priority.
262
+ #
263
+ # The accept_encoding argument is typically generated by calling
264
+ # Request#accept_encoding.
265
+ #
266
+ # Example:
267
+ #
268
+ # select_best_encoding(%w(compress gzip identity),
269
+ # [["compress", 0.5], ["gzip", 1.0]])
270
+ # # => "gzip"
271
+ #
272
+ # To reduce denial of service potential, only the first 16
273
+ # acceptable encodings are considered.
174
274
  def select_best_encoding(available_encodings, accept_encoding)
175
275
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
176
276
 
277
+ # Only process the first 16 encodings
278
+ accept_encoding = accept_encoding[0...16]
177
279
  expanded_accept_encoding = []
280
+ wildcard_seen = false
178
281
 
179
282
  accept_encoding.each do |m, q|
180
283
  preference = available_encodings.index(m) || available_encodings.size
181
284
 
182
285
  if m == "*"
183
- (available_encodings - accept_encoding.map(&:first)).each do |m2|
184
- expanded_accept_encoding << [m2, q, preference]
286
+ unless wildcard_seen
287
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
288
+ expanded_accept_encoding << [m2, q, preference]
289
+ end
290
+ wildcard_seen = true
185
291
  end
186
292
  else
187
293
  expanded_accept_encoding << [m, q, preference]
@@ -189,7 +295,13 @@ module Rack
189
295
  end
190
296
 
191
297
  encoding_candidates = expanded_accept_encoding
192
- .sort_by { |_, q, p| [-q, p] }
298
+ .sort do |(_, q1, p1), (_, q2, p2)|
299
+ if r = (q1 <=> q2).nonzero?
300
+ -r
301
+ else
302
+ (p1 <=> p2).nonzero? || 0
303
+ end
304
+ end
193
305
  .map!(&:first)
194
306
 
195
307
  unless encoding_candidates.include?("identity")
@@ -203,24 +315,69 @@ module Rack
203
315
  (encoding_candidates & available_encodings)[0]
204
316
  end
205
317
 
206
- def parse_cookies(env)
207
- parse_cookies_header env[HTTP_COOKIE]
208
- end
318
+ # :call-seq:
319
+ # parse_cookies_header(value) -> hash
320
+ #
321
+ # Parse cookies from the provided header +value+ according to RFC6265. The
322
+ # syntax for cookie headers only supports semicolons. Returns a map of
323
+ # cookie +key+ to cookie +value+.
324
+ #
325
+ # parse_cookies_header('myname=myvalue; max-age=0')
326
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
327
+ #
328
+ def parse_cookies_header(value)
329
+ return {} unless value
209
330
 
210
- def parse_cookies_header(header)
211
- # According to RFC 6265:
212
- # The syntax for cookie headers only supports semicolons
213
- # User Agent -> Server ==
214
- # Cookie: SID=31d4d96e407aad42; lang=en-US
215
- return {} unless header
216
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
331
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
217
332
  next if cookie.empty?
218
333
  key, value = cookie.split('=', 2)
219
334
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
220
335
  end
221
336
  end
222
337
 
223
- def add_cookie_to_header(header, key, value)
338
+ # :call-seq:
339
+ # parse_cookies(env) -> hash
340
+ #
341
+ # Parse cookies from the provided request environment using
342
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
343
+ #
344
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
345
+ # # => {'myname' => 'myvalue'}
346
+ #
347
+ def parse_cookies(env)
348
+ parse_cookies_header env[HTTP_COOKIE]
349
+ end
350
+
351
+ # A valid cookie key according to RFC6265 and RFC2616.
352
+ # 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: ( ) < > @ , ; : \ " / [ ] ? = { }.
353
+ VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
354
+ private_constant :VALID_COOKIE_KEY
355
+
356
+ # :call-seq:
357
+ # set_cookie_header(key, value) -> encoded string
358
+ #
359
+ # Generate an encoded string using the provided +key+ and +value+ suitable
360
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
361
+ # instance of either +String+ or +Hash+. If the cookie key is invalid (as
362
+ # defined by RFC6265), an +ArgumentError+ will be raised.
363
+ #
364
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
365
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
366
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
367
+ # details about the interpretation of these fields, consult
368
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
369
+ #
370
+ # set_cookie_header("myname", "myvalue")
371
+ # # => "myname=myvalue"
372
+ #
373
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
374
+ # # => "myname=myvalue; max-age=10"
375
+ #
376
+ def set_cookie_header(key, value)
377
+ unless key =~ VALID_COOKIE_KEY
378
+ raise ArgumentError, "invalid cookie key: #{key.inspect}"
379
+ end
380
+
224
381
  case value
225
382
  when Hash
226
383
  domain = "; domain=#{value[:domain]}" if value[:domain]
@@ -228,134 +385,135 @@ module Rack
228
385
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
229
386
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
230
387
  secure = "; secure" if value[:secure]
231
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
388
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
232
389
  same_site =
233
390
  case value[:same_site]
234
391
  when false, nil
235
392
  nil
236
393
  when :none, 'None', :None
237
- '; SameSite=None'
394
+ '; samesite=none'
238
395
  when :lax, 'Lax', :Lax
239
- '; SameSite=Lax'
396
+ '; samesite=lax'
240
397
  when true, :strict, 'Strict', :Strict
241
- '; SameSite=Strict'
398
+ '; samesite=strict'
242
399
  else
243
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
400
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
244
401
  end
402
+ partitioned = "; partitioned" if value[:partitioned]
245
403
  value = value[:value]
246
404
  end
405
+
247
406
  value = [value] unless Array === value
248
407
 
249
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
250
- "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
408
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
409
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
410
+ end
251
411
 
252
- case header
253
- when nil, ''
254
- cookie
255
- when String
256
- [header, cookie].join("\n")
257
- when Array
258
- (header + [cookie]).join("\n")
412
+ # :call-seq:
413
+ # set_cookie_header!(headers, key, value) -> header value
414
+ #
415
+ # Append a cookie in the specified headers with the given cookie +key+ and
416
+ # +value+ using set_cookie_header.
417
+ #
418
+ # If the headers already contains a +set-cookie+ key, it will be converted
419
+ # to an +Array+ if not already, and appended to.
420
+ def set_cookie_header!(headers, key, value)
421
+ if header = headers[SET_COOKIE]
422
+ if header.is_a?(Array)
423
+ header << set_cookie_header(key, value)
424
+ else
425
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
426
+ end
259
427
  else
260
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
428
+ headers[SET_COOKIE] = set_cookie_header(key, value)
261
429
  end
262
430
  end
263
431
 
264
- def set_cookie_header!(header, key, value)
265
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
266
- nil
432
+ # :call-seq:
433
+ # delete_set_cookie_header(key, value = {}) -> encoded string
434
+ #
435
+ # Generate an encoded string based on the given +key+ and +value+ using
436
+ # set_cookie_header for the purpose of causing the specified cookie to be
437
+ # deleted. The +value+ may be an instance of +Hash+ and can include
438
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
439
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
440
+ # +value+. When used with the +set-cookie+ header, it will cause the client
441
+ # to *remove* any matching cookie.
442
+ #
443
+ # delete_set_cookie_header("myname")
444
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
445
+ #
446
+ def delete_set_cookie_header(key, value = {})
447
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
267
448
  end
268
449
 
269
- def make_delete_cookie_header(header, key, value)
270
- case header
271
- when nil, ''
272
- cookies = []
273
- when String
274
- cookies = header.split("\n")
275
- when Array
276
- cookies = header
277
- end
278
-
279
- key = escape(key)
280
- domain = value[:domain]
281
- path = value[:path]
282
- regexp = if domain
283
- if path
284
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
285
- else
286
- /\A#{key}=.*domain=#{domain}(?:;|$)/
287
- end
288
- elsif path
289
- /\A#{key}=.*path=#{path}(?:;|$)/
290
- else
291
- /\A#{key}=/
292
- end
450
+ def delete_cookie_header!(headers, key, value = {})
451
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
293
452
 
294
- cookies.reject! { |cookie| regexp.match? cookie }
295
-
296
- cookies.join("\n")
453
+ return nil
297
454
  end
298
455
 
299
- def delete_cookie_header!(header, key, value = {})
300
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
301
- nil
302
- end
303
-
304
- # Adds a cookie that will *remove* a cookie from the client. Hence the
305
- # strange method name.
306
- def add_remove_cookie_to_header(header, key, value = {})
307
- new_header = make_delete_cookie_header(header, key, value)
308
-
309
- add_cookie_to_header(new_header, key,
310
- { value: '', path: nil, domain: nil,
311
- max_age: '0',
312
- expires: Time.at(0) }.merge(value))
456
+ # :call-seq:
457
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
458
+ #
459
+ # Set an expired cookie in the specified headers with the given cookie
460
+ # +key+ and +value+ using delete_set_cookie_header. This causes
461
+ # the client to immediately delete the specified cookie.
462
+ #
463
+ # delete_set_cookie_header!(nil, "mycookie")
464
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
465
+ #
466
+ # If the header is non-nil, it will be modified in place.
467
+ #
468
+ # header = []
469
+ # delete_set_cookie_header!(header, "mycookie")
470
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
471
+ # header
472
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
473
+ #
474
+ def delete_set_cookie_header!(header, key, value = {})
475
+ if header
476
+ header = Array(header)
477
+ header << delete_set_cookie_header(key, value)
478
+ else
479
+ header = delete_set_cookie_header(key, value)
480
+ end
313
481
 
482
+ return header
314
483
  end
315
484
 
316
485
  def rfc2822(time)
317
486
  time.rfc2822
318
487
  end
319
488
 
320
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
321
- # of '% %b %Y'.
322
- # It assumes that the time is in GMT to comply to the RFC 2109.
323
- #
324
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
325
- # that I'm certain someone implemented only that option.
326
- # Do not use %a and %b from Time.strptime, it would use localized names for
327
- # weekday and month.
328
- #
329
- def rfc2109(time)
330
- wday = Time::RFC2822_DAY_NAME[time.wday]
331
- mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
332
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
333
- end
334
-
335
489
  # Parses the "Range:" header, if present, into an array of Range objects.
336
490
  # Returns nil if the header is missing or syntactically invalid.
337
491
  # Returns an empty array if none of the ranges are satisfiable.
338
- def byte_ranges(env, size)
339
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
340
- get_byte_ranges env['HTTP_RANGE'], size
492
+ def byte_ranges(env, size, max_ranges: 100)
493
+ get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
341
494
  end
342
495
 
343
- def get_byte_ranges(http_range, size)
496
+ def get_byte_ranges(http_range, size, max_ranges: 100)
344
497
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
498
+ # Ignore Range when file size is 0 to avoid a 416 error.
499
+ return nil if size.zero?
345
500
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
501
+ byte_range = $1
502
+ return nil if byte_range.count(',') >= max_ranges
346
503
  ranges = []
347
- $1.split(/,\s*/).each do |range_spec|
348
- return nil unless range_spec =~ /(\d*)-(\d*)/
349
- r0, r1 = $1, $2
350
- if r0.empty?
351
- return nil if r1.empty?
504
+ byte_range.split(/,[ \t]*/).each do |range_spec|
505
+ return nil unless range_spec.include?('-')
506
+ range = range_spec.split('-')
507
+ r0, r1 = range[0], range[1]
508
+ if r0.nil? || r0.empty?
509
+ return nil if r1.nil?
352
510
  # suffix-byte-range-spec, represents trailing suffix of file
353
511
  r0 = size - r1.to_i
354
512
  r0 = 0 if r0 < 0
355
513
  r1 = size - 1
356
514
  else
357
515
  r0 = r0.to_i
358
- if r1.empty?
516
+ if r1.nil?
359
517
  r1 = size - 1
360
518
  else
361
519
  r1 = r1.to_i
@@ -365,23 +523,36 @@ module Rack
365
523
  end
366
524
  ranges << (r0..r1) if r0 <= r1
367
525
  end
526
+
527
+ return [] if ranges.map(&:size).sum > size
528
+
368
529
  ranges
369
530
  end
370
531
 
371
- # Constant time string comparison.
372
- #
373
- # NOTE: the values compared should be of fixed length, such as strings
374
- # that have already been processed by HMAC. This should not be used
375
- # on variable length plaintext strings because it could leak length info
376
- # via timing attacks.
377
- def secure_compare(a, b)
378
- return false unless a.bytesize == b.bytesize
532
+ # :nocov:
533
+ if defined?(OpenSSL.fixed_length_secure_compare)
534
+ # Constant time string comparison.
535
+ #
536
+ # NOTE: the values compared should be of fixed length, such as strings
537
+ # that have already been processed by HMAC. This should not be used
538
+ # on variable length plaintext strings because it could leak length info
539
+ # via timing attacks.
540
+ def secure_compare(a, b)
541
+ return false unless a.bytesize == b.bytesize
379
542
 
380
- l = a.unpack("C*")
543
+ OpenSSL.fixed_length_secure_compare(a, b)
544
+ end
545
+ # :nocov:
546
+ else
547
+ def secure_compare(a, b)
548
+ return false unless a.bytesize == b.bytesize
549
+
550
+ l = a.unpack("C*")
381
551
 
382
- r, i = 0, -1
383
- b.each_byte { |v| r |= v ^ l[i += 1] }
384
- r == 0
552
+ r, i = 0, -1
553
+ b.each_byte { |v| r |= v ^ l[i += 1] }
554
+ r == 0
555
+ end
385
556
  end
386
557
 
387
558
  # Context allows the use of a compatible middleware at different points
@@ -410,101 +581,12 @@ module Rack
410
581
  end
411
582
  end
412
583
 
413
- # A case-insensitive Hash that preserves the original case of a
414
- # header when set.
415
- #
416
- # @api private
417
- class HeaderHash < Hash # :nodoc:
418
- def self.[](headers)
419
- if headers.is_a?(HeaderHash) && !headers.frozen?
420
- return headers
421
- else
422
- return self.new(headers)
423
- end
424
- end
425
-
426
- def initialize(hash = {})
427
- super()
428
- @names = {}
429
- hash.each { |k, v| self[k] = v }
430
- end
431
-
432
- # on dup/clone, we need to duplicate @names hash
433
- def initialize_copy(other)
434
- super
435
- @names = other.names.dup
436
- end
437
-
438
- # on clear, we need to clear @names hash
439
- def clear
440
- super
441
- @names.clear
442
- end
443
-
444
- def each
445
- super do |k, v|
446
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
447
- end
448
- end
449
-
450
- def to_hash
451
- hash = {}
452
- each { |k, v| hash[k] = v }
453
- hash
454
- end
455
-
456
- def [](k)
457
- super(k) || super(@names[k.downcase])
458
- end
459
-
460
- def []=(k, v)
461
- canonical = k.downcase.freeze
462
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
463
- @names[canonical] = k
464
- super k, v
465
- end
466
-
467
- def delete(k)
468
- canonical = k.downcase
469
- result = super @names.delete(canonical)
470
- result
471
- end
472
-
473
- def include?(k)
474
- super || @names.include?(k.downcase)
475
- end
476
-
477
- alias_method :has_key?, :include?
478
- alias_method :member?, :include?
479
- alias_method :key?, :include?
480
-
481
- def merge!(other)
482
- other.each { |k, v| self[k] = v }
483
- self
484
- end
485
-
486
- def merge(other)
487
- hash = dup
488
- hash.merge! other
489
- end
490
-
491
- def replace(other)
492
- clear
493
- other.each { |k, v| self[k] = v }
494
- self
495
- end
496
-
497
- protected
498
- def names
499
- @names
500
- end
501
- end
502
-
503
584
  # Every standard HTTP code mapped to the appropriate message.
504
585
  # Generated with:
505
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
506
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
507
- # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
586
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
587
+ # | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
588
+ # .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
589
+ # .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
508
590
  HTTP_STATUS_CODES = {
509
591
  100 => 'Continue',
510
592
  101 => 'Switching Protocols',
@@ -526,7 +608,6 @@ module Rack
526
608
  303 => 'See Other',
527
609
  304 => 'Not Modified',
528
610
  305 => 'Use Proxy',
529
- 306 => '(Unused)',
530
611
  307 => 'Temporary Redirect',
531
612
  308 => 'Permanent Redirect',
532
613
  400 => 'Bad Request',
@@ -542,13 +623,13 @@ module Rack
542
623
  410 => 'Gone',
543
624
  411 => 'Length Required',
544
625
  412 => 'Precondition Failed',
545
- 413 => 'Payload Too Large',
626
+ 413 => 'Content Too Large',
546
627
  414 => 'URI Too Long',
547
628
  415 => 'Unsupported Media Type',
548
629
  416 => 'Range Not Satisfiable',
549
630
  417 => 'Expectation Failed',
550
631
  421 => 'Misdirected Request',
551
- 422 => 'Unprocessable Entity',
632
+ 422 => 'Unprocessable Content',
552
633
  423 => 'Locked',
553
634
  424 => 'Failed Dependency',
554
635
  425 => 'Too Early',
@@ -556,7 +637,7 @@ module Rack
556
637
  428 => 'Precondition Required',
557
638
  429 => 'Too Many Requests',
558
639
  431 => 'Request Header Fields Too Large',
559
- 451 => 'Unavailable for Legal Reasons',
640
+ 451 => 'Unavailable For Legal Reasons',
560
641
  500 => 'Internal Server Error',
561
642
  501 => 'Not Implemented',
562
643
  502 => 'Bad Gateway',
@@ -566,8 +647,6 @@ module Rack
566
647
  506 => 'Variant Also Negotiates',
567
648
  507 => 'Insufficient Storage',
568
649
  508 => 'Loop Detected',
569
- 509 => 'Bandwidth Limit Exceeded',
570
- 510 => 'Not Extended',
571
650
  511 => 'Network Authentication Required'
572
651
  }
573
652
 
@@ -575,12 +654,34 @@ module Rack
575
654
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
576
655
 
577
656
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
578
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
657
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
579
658
  }.flatten]
580
659
 
660
+ OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
661
+ payload_too_large: 413,
662
+ unprocessable_entity: 422,
663
+ bandwidth_limit_exceeded: 509,
664
+ not_extended: 510
665
+ }.freeze
666
+ private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
667
+
668
+ OBSOLETE_SYMBOL_MAPPINGS = {
669
+ payload_too_large: :content_too_large,
670
+ unprocessable_entity: :unprocessable_content
671
+ }.freeze
672
+ private_constant :OBSOLETE_SYMBOL_MAPPINGS
673
+
581
674
  def status_code(status)
582
675
  if status.is_a?(Symbol)
583
- SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
676
+ SYMBOL_TO_STATUS_CODE.fetch(status) do
677
+ fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
678
+ message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
679
+ if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
680
+ message = "#{message} Please use #{canonical_symbol.inspect} instead."
681
+ end
682
+ warn message, uplevel: 3
683
+ fallback_code
684
+ end
584
685
  else
585
686
  status.to_i
586
687
  end