rack 2.2.23 → 3.2.6

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 +574 -71
  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 -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 +108 -69
  13. data/lib/rack/cascade.rb +2 -3
  14. data/lib/rack/common_logger.rb +22 -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 +9 -3
  21. data/lib/rack/etag.rb +17 -23
  22. data/lib/rack/events.rb +25 -6
  23. data/lib/rack/files.rb +15 -17
  24. data/lib/rack/head.rb +8 -8
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +817 -648
  27. data/lib/rack/lock.rb +2 -5
  28. data/lib/rack/media_type.rb +6 -7
  29. data/lib/rack/method_override.rb +5 -1
  30. data/lib/rack/mime.rb +14 -5
  31. data/lib/rack/mock.rb +1 -300
  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 +267 -109
  36. data/lib/rack/multipart/uploaded_file.rb +45 -4
  37. data/lib/rack/multipart.rb +53 -40
  38. data/lib/rack/null_logger.rb +9 -0
  39. data/lib/rack/query_parser.rb +116 -121
  40. data/lib/rack/recursive.rb +2 -0
  41. data/lib/rack/reloader.rb +0 -2
  42. data/lib/rack/request.rb +272 -144
  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 +35 -30
  47. data/lib/rack/show_exceptions.rb +25 -6
  48. data/lib/rack/show_status.rb +17 -9
  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 +287 -236
  53. data/lib/rack/version.rb +3 -15
  54. data/lib/rack.rb +13 -90
  55. metadata +14 -40
  56. data/README.rdoc +0 -355
  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 -34
  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 -90
  87. 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,80 @@ module Rack
153
146
  end
154
147
  end
155
148
 
149
+ ALLOWED_FORWARED_PARAMS = %w[by for host proto].to_h { |name| [name, name.to_sym] }.freeze
150
+ private_constant :ALLOWED_FORWARED_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_FORWARED_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
+
156
223
  # Return best accept value to use, based on the algorithm
157
224
  # in RFC 2616 Section 14. If there are multiple best
158
225
  # matches (same specificity and quality), the value returned
@@ -167,23 +234,23 @@ module Rack
167
234
  end.compact.sort_by do |match, quality|
168
235
  (match.split('/', 2).count('*') * -10) + quality
169
236
  end.last
170
- matches && matches.first
237
+ matches&.first
171
238
  end
172
239
 
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] }
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:
187
254
  end
188
255
 
189
256
  # Given an array of available encoding strings, and an array of
@@ -248,24 +315,69 @@ module Rack
248
315
  (encoding_candidates & available_encodings)[0]
249
316
  end
250
317
 
251
- def parse_cookies(env)
252
- parse_cookies_header env[HTTP_COOKIE]
253
- 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
254
330
 
255
- def parse_cookies_header(header)
256
- # According to RFC 6265:
257
- # The syntax for cookie headers only supports semicolons
258
- # User Agent -> Server ==
259
- # Cookie: SID=31d4d96e407aad42; lang=en-US
260
- return {} unless header
261
- header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
331
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
262
332
  next if cookie.empty?
263
333
  key, value = cookie.split('=', 2)
264
334
  cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
265
335
  end
266
336
  end
267
337
 
268
- 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
+
269
381
  case value
270
382
  when Hash
271
383
  domain = "; domain=#{value[:domain]}" if value[:domain]
@@ -273,110 +385,107 @@ module Rack
273
385
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
274
386
  expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
275
387
  secure = "; secure" if value[:secure]
276
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
388
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
277
389
  same_site =
278
390
  case value[:same_site]
279
391
  when false, nil
280
392
  nil
281
393
  when :none, 'None', :None
282
- '; SameSite=None'
394
+ '; samesite=none'
283
395
  when :lax, 'Lax', :Lax
284
- '; SameSite=Lax'
396
+ '; samesite=lax'
285
397
  when true, :strict, 'Strict', :Strict
286
- '; SameSite=Strict'
398
+ '; samesite=strict'
287
399
  else
288
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
400
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
289
401
  end
402
+ partitioned = "; partitioned" if value[:partitioned]
290
403
  value = value[:value]
291
404
  end
405
+
292
406
  value = [value] unless Array === value
293
407
 
294
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
295
- "#{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
296
411
 
297
- case header
298
- when nil, ''
299
- cookie
300
- when String
301
- [header, cookie].join("\n")
302
- when Array
303
- (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
304
427
  else
305
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
428
+ headers[SET_COOKIE] = set_cookie_header(key, value)
306
429
  end
307
430
  end
308
431
 
309
- def set_cookie_header!(header, key, value)
310
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
311
- 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: ''))
312
448
  end
313
449
 
314
- def make_delete_cookie_header(header, key, value)
315
- case header
316
- when nil, ''
317
- cookies = []
318
- when String
319
- cookies = header.split("\n")
320
- when Array
321
- cookies = header
322
- end
323
-
324
- key = escape(key)
325
- domain = value[:domain]
326
- path = value[:path]
327
- regexp = if domain
328
- if path
329
- /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
330
- else
331
- /\A#{key}=.*domain=#{domain}(?:;|$)/
332
- end
333
- elsif path
334
- /\A#{key}=.*path=#{path}(?:;|$)/
335
- else
336
- /\A#{key}=/
337
- end
450
+ def delete_cookie_header!(headers, key, value = {})
451
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
338
452
 
339
- cookies.reject! { |cookie| regexp.match? cookie }
340
-
341
- cookies.join("\n")
342
- end
343
-
344
- def delete_cookie_header!(header, key, value = {})
345
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
346
- nil
453
+ return nil
347
454
  end
348
455
 
349
- # Adds a cookie that will *remove* a cookie from the client. Hence the
350
- # strange method name.
351
- def add_remove_cookie_to_header(header, key, value = {})
352
- new_header = make_delete_cookie_header(header, key, value)
353
-
354
- add_cookie_to_header(new_header, key,
355
- { value: '', path: nil, domain: nil,
356
- max_age: '0',
357
- 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
358
481
 
482
+ return header
359
483
  end
360
484
 
361
485
  def rfc2822(time)
362
486
  time.rfc2822
363
487
  end
364
488
 
365
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
366
- # of '% %b %Y'.
367
- # It assumes that the time is in GMT to comply to the RFC 2109.
368
- #
369
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
370
- # that I'm certain someone implemented only that option.
371
- # Do not use %a and %b from Time.strptime, it would use localized names for
372
- # weekday and month.
373
- #
374
- def rfc2109(time)
375
- wday = RFC2822_DAY_NAME[time.wday]
376
- mon = RFC2822_MONTH_NAME[time.mon - 1]
377
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
378
- end
379
-
380
489
  # Parses the "Range:" header, if present, into an array of Range objects.
381
490
  # Returns nil if the header is missing or syntactically invalid.
382
491
  # Returns an empty array if none of the ranges are satisfiable.
@@ -386,6 +495,8 @@ module Rack
386
495
 
387
496
  def get_byte_ranges(http_range, size, max_ranges: 100)
388
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?
389
500
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
390
501
  byte_range = $1
391
502
  return nil if byte_range.count(',') >= max_ranges
@@ -413,25 +524,35 @@ module Rack
413
524
  ranges << (r0..r1) if r0 <= r1
414
525
  end
415
526
 
416
- return [] if ranges.map(&:size).inject(0, :+) > size
527
+ return [] if ranges.map(&:size).sum > size
417
528
 
418
529
  ranges
419
530
  end
420
531
 
421
- # Constant time string comparison.
422
- #
423
- # NOTE: the values compared should be of fixed length, such as strings
424
- # that have already been processed by HMAC. This should not be used
425
- # on variable length plaintext strings because it could leak length info
426
- # via timing attacks.
427
- def secure_compare(a, b)
428
- 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
429
542
 
430
- 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
431
549
 
432
- r, i = 0, -1
433
- b.each_byte { |v| r |= v ^ l[i += 1] }
434
- r == 0
550
+ l = a.unpack("C*")
551
+
552
+ r, i = 0, -1
553
+ b.each_byte { |v| r |= v ^ l[i += 1] }
554
+ r == 0
555
+ end
435
556
  end
436
557
 
437
558
  # Context allows the use of a compatible middleware at different points
@@ -460,101 +581,12 @@ module Rack
460
581
  end
461
582
  end
462
583
 
463
- # A case-insensitive Hash that preserves the original case of a
464
- # header when set.
465
- #
466
- # @api private
467
- class HeaderHash < Hash # :nodoc:
468
- def self.[](headers)
469
- if headers.is_a?(HeaderHash) && !headers.frozen?
470
- return headers
471
- else
472
- return self.new(headers)
473
- end
474
- end
475
-
476
- def initialize(hash = {})
477
- super()
478
- @names = {}
479
- hash.each { |k, v| self[k] = v }
480
- end
481
-
482
- # on dup/clone, we need to duplicate @names hash
483
- def initialize_copy(other)
484
- super
485
- @names = other.names.dup
486
- end
487
-
488
- # on clear, we need to clear @names hash
489
- def clear
490
- super
491
- @names.clear
492
- end
493
-
494
- def each
495
- super do |k, v|
496
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
497
- end
498
- end
499
-
500
- def to_hash
501
- hash = {}
502
- each { |k, v| hash[k] = v }
503
- hash
504
- end
505
-
506
- def [](k)
507
- super(k) || super(@names[k.downcase])
508
- end
509
-
510
- def []=(k, v)
511
- canonical = k.downcase.freeze
512
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
513
- @names[canonical] = k
514
- super k, v
515
- end
516
-
517
- def delete(k)
518
- canonical = k.downcase
519
- result = super @names.delete(canonical)
520
- result
521
- end
522
-
523
- def include?(k)
524
- super || @names.include?(k.downcase)
525
- end
526
-
527
- alias_method :has_key?, :include?
528
- alias_method :member?, :include?
529
- alias_method :key?, :include?
530
-
531
- def merge!(other)
532
- other.each { |k, v| self[k] = v }
533
- self
534
- end
535
-
536
- def merge(other)
537
- hash = dup
538
- hash.merge! other
539
- end
540
-
541
- def replace(other)
542
- clear
543
- other.each { |k, v| self[k] = v }
544
- self
545
- end
546
-
547
- protected
548
- def names
549
- @names
550
- end
551
- end
552
-
553
584
  # Every standard HTTP code mapped to the appropriate message.
554
585
  # Generated with:
555
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
556
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
557
- # 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)"
558
590
  HTTP_STATUS_CODES = {
559
591
  100 => 'Continue',
560
592
  101 => 'Switching Protocols',
@@ -576,7 +608,6 @@ module Rack
576
608
  303 => 'See Other',
577
609
  304 => 'Not Modified',
578
610
  305 => 'Use Proxy',
579
- 306 => '(Unused)',
580
611
  307 => 'Temporary Redirect',
581
612
  308 => 'Permanent Redirect',
582
613
  400 => 'Bad Request',
@@ -592,13 +623,13 @@ module Rack
592
623
  410 => 'Gone',
593
624
  411 => 'Length Required',
594
625
  412 => 'Precondition Failed',
595
- 413 => 'Payload Too Large',
626
+ 413 => 'Content Too Large',
596
627
  414 => 'URI Too Long',
597
628
  415 => 'Unsupported Media Type',
598
629
  416 => 'Range Not Satisfiable',
599
630
  417 => 'Expectation Failed',
600
631
  421 => 'Misdirected Request',
601
- 422 => 'Unprocessable Entity',
632
+ 422 => 'Unprocessable Content',
602
633
  423 => 'Locked',
603
634
  424 => 'Failed Dependency',
604
635
  425 => 'Too Early',
@@ -606,7 +637,7 @@ module Rack
606
637
  428 => 'Precondition Required',
607
638
  429 => 'Too Many Requests',
608
639
  431 => 'Request Header Fields Too Large',
609
- 451 => 'Unavailable for Legal Reasons',
640
+ 451 => 'Unavailable For Legal Reasons',
610
641
  500 => 'Internal Server Error',
611
642
  501 => 'Not Implemented',
612
643
  502 => 'Bad Gateway',
@@ -616,8 +647,6 @@ module Rack
616
647
  506 => 'Variant Also Negotiates',
617
648
  507 => 'Insufficient Storage',
618
649
  508 => 'Loop Detected',
619
- 509 => 'Bandwidth Limit Exceeded',
620
- 510 => 'Not Extended',
621
650
  511 => 'Network Authentication Required'
622
651
  }
623
652
 
@@ -625,12 +654,34 @@ module Rack
625
654
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
626
655
 
627
656
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
628
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
657
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
629
658
  }.flatten]
630
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
+
631
674
  def status_code(status)
632
675
  if status.is_a?(Symbol)
633
- 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
634
685
  else
635
686
  status.to_i
636
687
  end