rack 3.1.16 → 3.2.0

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.
@@ -14,12 +14,11 @@ module Rack
14
14
  # For more information on the use of media types in HTTP, see:
15
15
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
16
16
  def type(content_type)
17
- return nil unless content_type
18
- if type = content_type.split(SPLIT_PATTERN, 2).first
19
- type.rstrip!
20
- type.downcase!
21
- type
22
- end
17
+ return nil unless content_type && !content_type.empty?
18
+ type = content_type.split(SPLIT_PATTERN, 2).first
19
+ type.rstrip!
20
+ type.downcase!
21
+ type
23
22
  end
24
23
 
25
24
  # The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -33,7 +32,7 @@ module Rack
33
32
  # and "text/plain;charset" will return { 'charset' => '' }, similarly to
34
33
  # the query params parser (barring the latter case, which returns nil instead)).
35
34
  def params(content_type)
36
- return {} if content_type.nil?
35
+ return {} if content_type.nil? || content_type.empty?
37
36
 
38
37
  content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
39
38
  s.strip!
@@ -10,33 +10,27 @@ module Rack
10
10
  # MockRequest.
11
11
 
12
12
  class MockResponse < Rack::Response
13
- begin
14
- # Recent versions of the CGI gem may not provide `CGI::Cookie`.
15
- require 'cgi/cookie'
16
- Cookie = CGI::Cookie
17
- rescue LoadError
18
- class Cookie
19
- attr_reader :name, :value, :path, :domain, :expires, :secure
20
-
21
- def initialize(args)
22
- @name = args["name"]
23
- @value = args["value"]
24
- @path = args["path"]
25
- @domain = args["domain"]
26
- @expires = args["expires"]
27
- @secure = args["secure"]
28
- end
13
+ class Cookie
14
+ attr_reader :name, :value, :path, :domain, :expires, :secure
15
+
16
+ def initialize(args)
17
+ @name = args["name"]
18
+ @value = args["value"]
19
+ @path = args["path"]
20
+ @domain = args["domain"]
21
+ @expires = args["expires"]
22
+ @secure = args["secure"]
23
+ end
29
24
 
30
- def method_missing(method_name, *args, &block)
31
- @value.send(method_name, *args, &block)
32
- end
33
- # :nocov:
34
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
35
- # :nocov:
25
+ def method_missing(method_name, *args, &block)
26
+ @value.send(method_name, *args, &block)
27
+ end
28
+ # :nocov:
29
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
30
+ # :nocov:
36
31
 
37
- def respond_to_missing?(method_name, include_all = false)
38
- @value.respond_to?(method_name, include_all) || super
39
- end
32
+ def respond_to_missing?(method_name, include_all = false)
33
+ @value.respond_to?(method_name, include_all) || super
40
34
  end
41
35
  end
42
36
 
@@ -38,6 +38,18 @@ module Rack
38
38
  MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
39
  MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
40
40
 
41
+ # Rack::Multipart::Parser handles parsing of multipart/form-data requests.
42
+ #
43
+ # File Parameter Contents
44
+ #
45
+ # When processing file uploads, the parser returns a hash containing
46
+ # information about uploaded files. For +file+ parameters, the hash includes:
47
+ #
48
+ # * +:filename+ - The original filename, already URL decoded by the parser
49
+ # * +:type+ - The content type of the uploaded file
50
+ # * +:name+ - The parameter name from the form
51
+ # * +:tempfile+ - A Tempfile object containing the uploaded data
52
+ # * +:head+ - The raw header content for this part
41
53
  class Parser
42
54
  BUFSIZE = 1_048_576
43
55
  TEXT_PLAIN = "text/plain"
@@ -243,7 +255,8 @@ module Rack
243
255
  @collector.each do |part|
244
256
  part.get_data do |data|
245
257
  tag_multipart_encoding(part.filename, part.content_type, part.name, data)
246
- @query_parser.normalize_params(@params, part.name, data)
258
+ name, data = handle_dummy_encoding(part.name, data)
259
+ @query_parser.normalize_params(@params, name, data)
247
260
  end
248
261
  end
249
262
  MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -251,12 +264,6 @@ module Rack
251
264
 
252
265
  private
253
266
 
254
- def dequote(str) # From WEBrick::HTTPUtils
255
- ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
256
- ret.gsub!(/\\(.)/, "\\1")
257
- ret
258
- end
259
-
260
267
  def read_data(io, outbuf)
261
268
  content = io.read(@bufsize, outbuf)
262
269
  handle_empty_content!(content)
@@ -494,6 +501,25 @@ module Rack
494
501
  Encoding::BINARY
495
502
  end
496
503
 
504
+ REENCODE_DUMMY_ENCODINGS = {
505
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
506
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
507
+ Encoding::ISO_2022_JP => true
508
+
509
+ # Other dummy encodings are rarely used and have not been supported yet.
510
+ # Adding support for them will require careful considerations.
511
+ }
512
+
513
+ def handle_dummy_encoding(name, body)
514
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
515
+ # So here we covert it to UTF-8 so that it can be handled properly.
516
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
517
+ name = name.encode(Encoding::UTF_8)
518
+ body = body.encode(Encoding::UTF_8)
519
+ end
520
+ return name, body
521
+ end
522
+
497
523
  def handle_empty_content!(content)
498
524
  if content.nil? || content.empty?
499
525
  raise EmptyContentError
@@ -5,14 +5,47 @@ require 'fileutils'
5
5
 
6
6
  module Rack
7
7
  module Multipart
8
+ # Despite the misleading name, UploadedFile is designed for use for
9
+ # preparing multipart file upload bodies, generally for use in tests.
10
+ # It is not designed for and should not be used for handling uploaded
11
+ # files (there is no need for that, since Rack's multipart parser
12
+ # already creates Tempfiles for that). Using this with non-trusted
13
+ # filenames can create a security vulnerability.
14
+ #
15
+ # You should only use this class if you plan on passing the instances
16
+ # to Rack::MockRequest for use in creating multipart request bodies.
17
+ #
18
+ # UploadedFile delegates most methods to the tempfile it contains.
8
19
  class UploadedFile
9
-
10
- # The filename, *not* including the path, of the "uploaded" file
20
+ # The provided name of the file. This generally is the basename of
21
+ # path provided during initialization, but it can contain slashes if they
22
+ # were present in the filename argument when the instance was created.
11
23
  attr_reader :original_filename
12
24
 
13
- # The content type of the "uploaded" file
25
+ # The content type of the instance.
14
26
  attr_accessor :content_type
15
27
 
28
+ # Create a new UploadedFile. For backwards compatibility, this accepts
29
+ # both positional and keyword versions of the same arguments:
30
+ #
31
+ # filepath/path :: The path to the file
32
+ # ct/content_type :: The content_type of the file
33
+ # bin/binary :: Whether to set binmode on the file before copying data into it.
34
+ #
35
+ # If both positional and keyword arguments are present, the keyword arguments
36
+ # take precedence.
37
+ #
38
+ # The following keyword-only arguments are also accepted:
39
+ #
40
+ # filename :: Override the filename to use for the file. This is so the
41
+ # filename for the upload does not need to match the basename of
42
+ # the file path. This should not contain slashes, unless you are
43
+ # trying to test how an application handles invalid filenames in
44
+ # multipart upload bodies.
45
+ # io :: Use the given IO-like instance as the tempfile, instead of creating
46
+ # a Tempfile instance. This is useful for building multipart file
47
+ # upload bodies without a file being present on the filesystem. If you are
48
+ # providing this, you should also provide the filename argument.
16
49
  def initialize(filepath = nil, ct = "text/plain", bin = false,
17
50
  path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
18
51
  if io
@@ -28,15 +61,19 @@ module Rack
28
61
  @content_type = content_type
29
62
  end
30
63
 
64
+ # The path of the tempfile for the instance, if the tempfile has a path.
65
+ # nil if the tempfile does not have a path.
31
66
  def path
32
67
  @tempfile.path if @tempfile.respond_to?(:path)
33
68
  end
34
69
  alias_method :local_path, :path
35
70
 
36
- def respond_to?(*args)
37
- super or @tempfile.respond_to?(*args)
71
+ # Return true if the tempfile responds to the method.
72
+ def respond_to_missing?(*args)
73
+ @tempfile.respond_to?(*args)
38
74
  end
39
75
 
76
+ # Delegate method missing calls to the tempfile.
40
77
  def method_missing(method_name, *args, &block) #:nodoc:
41
78
  @tempfile.__send__(method_name, *args, &block)
42
79
  end
@@ -69,14 +69,9 @@ module Rack
69
69
  # to parse cookies by changing the characters used in the second parameter
70
70
  # (which defaults to '&').
71
71
  def parse_query(qs, separator = nil, &unescaper)
72
- unescaper ||= method(:unescape)
73
-
74
72
  params = make_params
75
73
 
76
- check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
77
- next if p.empty?
78
- k, v = p.split('=', 2).map!(&unescaper)
79
-
74
+ each_query_pair(qs, separator, unescaper) do |k, v|
80
75
  if cur = params[k]
81
76
  if cur.class == Array
82
77
  params[k] << v
@@ -91,6 +86,19 @@ module Rack
91
86
  return params.to_h
92
87
  end
93
88
 
89
+ # Parses a query string by breaking it up at the '&', returning all key-value
90
+ # pairs as an array of [key, value] arrays. Unlike parse_query, this preserves
91
+ # all duplicate keys rather than collapsing them.
92
+ def parse_query_pairs(qs, separator = nil)
93
+ pairs = []
94
+
95
+ each_query_pair(qs, separator) do |k, v|
96
+ pairs << [k, v]
97
+ end
98
+
99
+ pairs
100
+ end
101
+
94
102
  # parse_nested_query expands a query string into structural types. Supported
95
103
  # types are Arrays, Hashes and basic value types. It is possible to supply
96
104
  # query strings with parameters of conflicting types, in this case a
@@ -99,17 +107,11 @@ module Rack
99
107
  def parse_nested_query(qs, separator = nil)
100
108
  params = make_params
101
109
 
102
- unless qs.nil? || qs.empty?
103
- check_query_string(qs, separator).split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
104
- k, v = p.split('=', 2).map! { |s| unescape(s) }
105
-
106
- _normalize_params(params, k, v, 0)
107
- end
110
+ each_query_pair(qs, separator) do |k, v|
111
+ _normalize_params(params, k, v, 0)
108
112
  end
109
113
 
110
114
  return params.to_h
111
- rescue ArgumentError => e
112
- raise InvalidParameterError, e.message, e.backtrace
113
115
  end
114
116
 
115
117
  # normalize_params recursively expands parameters into structural types. If
@@ -215,20 +217,35 @@ module Rack
215
217
  true
216
218
  end
217
219
 
218
- def check_query_string(qs, sep)
219
- if qs
220
- if qs.bytesize > @bytesize_limit
221
- raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
222
- end
220
+ def each_query_pair(qs, separator, unescaper = nil)
221
+ return if !qs || qs.empty?
223
222
 
224
- if (param_count = qs.count(sep.is_a?(String) ? sep : '&')) >= @params_limit
225
- raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
226
- end
223
+ if qs.bytesize > @bytesize_limit
224
+ raise QueryLimitError, "total query size (#{qs.bytesize}) exceeds limit (#{@bytesize_limit})"
225
+ end
226
+
227
+ pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
228
+
229
+ if pairs.size > @params_limit
230
+ param_count = pairs.size + pairs.last.count(separator || "&")
231
+ raise QueryLimitError, "total number of query parameters (#{param_count}) exceeds limit (#{@params_limit})"
232
+ end
227
233
 
228
- qs
234
+ if unescaper
235
+ pairs.each do |p|
236
+ next if p.empty?
237
+ k, v = p.split('=', 2).map!(&unescaper)
238
+ yield k, v
239
+ end
229
240
  else
230
- ''
241
+ pairs.each do |p|
242
+ next if p.empty?
243
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
244
+ yield k, v
245
+ end
231
246
  end
247
+ rescue ArgumentError => e
248
+ raise InvalidParameterError, e.message, e.backtrace
232
249
  end
233
250
 
234
251
  def unescape(string, encoding = Encoding::UTF_8)
data/lib/rack/request.rb CHANGED
@@ -61,9 +61,14 @@ module Rack
61
61
 
62
62
  def initialize(env)
63
63
  @env = env
64
+ @ip = nil
64
65
  @params = nil
65
66
  end
66
67
 
68
+ def ip
69
+ @ip ||= super
70
+ end
71
+
67
72
  def params
68
73
  @params ||= super
69
74
  end
@@ -398,8 +403,8 @@ module Rack
398
403
  return forwarded.last
399
404
  end
400
405
  when :x_forwarded
401
- if value = get_header(HTTP_X_FORWARDED_HOST)
402
- return wrap_ipv6(split_header(value).last)
406
+ if (value = get_header(HTTP_X_FORWARDED_HOST)) && (x_forwarded_host = split_header(value).last)
407
+ return wrap_ipv6(x_forwarded_host)
403
408
  end
404
409
  end
405
410
  end
@@ -413,10 +418,9 @@ module Rack
413
418
 
414
419
  def ip
415
420
  remote_addresses = split_header(get_header('REMOTE_ADDR'))
416
- external_addresses = reject_trusted_ip_addresses(remote_addresses)
417
421
 
418
- unless external_addresses.empty?
419
- return external_addresses.last
422
+ remote_addresses.reverse_each do |ip|
423
+ return ip unless trusted_proxy?(ip)
420
424
  end
421
425
 
422
426
  if (forwarded_for = self.forwarded_for) && !forwarded_for.empty?
@@ -424,7 +428,10 @@ module Rack
424
428
  # So we reject all the trusted addresses (proxy*) and return the
425
429
  # last client. Or if we trust everyone, we just return the first
426
430
  # address.
427
- return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
431
+ forwarded_for.reverse_each do |ip|
432
+ return ip unless trusted_proxy?(ip)
433
+ end
434
+ return forwarded_for.first
428
435
  end
429
436
 
430
437
  # If all the addresses are trusted, and we aren't forwarded, just return
@@ -482,51 +489,29 @@ module Rack
482
489
 
483
490
  # Returns the data received in the query string.
484
491
  def GET
485
- rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
486
- query_string = self.query_string
487
- if rr_query_string == query_string
488
- get_header(RACK_REQUEST_QUERY_HASH)
489
- else
490
- if rr_query_string
491
- warn "query string used for GET parsing different from current query string. Starting in Rack 3.2, Rack will used the cached GET value instead of parsing the current query string.", uplevel: 1
492
- end
493
- query_hash = parse_query(query_string, '&')
494
- set_header(RACK_REQUEST_QUERY_STRING, query_string)
495
- set_header(RACK_REQUEST_QUERY_HASH, query_hash)
496
- end
492
+ get_header(RACK_REQUEST_QUERY_HASH) || set_header(RACK_REQUEST_QUERY_HASH, parse_query(query_string, '&'))
497
493
  end
498
494
 
499
- # Returns the data received in the request body.
495
+ # Returns the form data pairs received in the request body.
500
496
  #
501
497
  # This method support both application/x-www-form-urlencoded and
502
498
  # multipart/form-data.
503
- def POST
504
- if error = get_header(RACK_REQUEST_FORM_ERROR)
499
+ def form_pairs
500
+ if pairs = get_header(RACK_REQUEST_FORM_PAIRS)
501
+ return pairs
502
+ elsif error = get_header(RACK_REQUEST_FORM_ERROR)
505
503
  raise error.class, error.message, cause: error.cause
506
504
  end
507
505
 
508
506
  begin
509
507
  rack_input = get_header(RACK_INPUT)
510
508
 
511
- # If the form hash was already memoized:
512
- if form_hash = get_header(RACK_REQUEST_FORM_HASH)
513
- form_input = get_header(RACK_REQUEST_FORM_INPUT)
514
- # And it was memoized from the same input:
515
- if form_input.equal?(rack_input)
516
- return form_hash
517
- elsif form_input
518
- warn "input stream used for POST parsing different from current input stream. Starting in Rack 3.2, Rack will used the cached POST value instead of parsing the current input stream.", uplevel: 1
519
- end
520
- end
521
-
522
509
  # Otherwise, figure out how to parse the input:
523
510
  if rack_input.nil?
524
- set_header RACK_REQUEST_FORM_INPUT, nil
525
- set_header(RACK_REQUEST_FORM_HASH, {})
511
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
526
512
  elsif form_data? || parseable_data?
527
513
  if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
528
514
  set_header RACK_REQUEST_FORM_PAIRS, pairs
529
- set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
530
515
  else
531
516
  form_vars = get_header(RACK_INPUT).read
532
517
 
@@ -535,14 +520,11 @@ module Rack
535
520
  form_vars.slice!(-1) if form_vars.end_with?("\0")
536
521
 
537
522
  set_header RACK_REQUEST_FORM_VARS, form_vars
538
- set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
523
+ pairs = query_parser.parse_query_pairs(form_vars, '&')
524
+ set_header(RACK_REQUEST_FORM_PAIRS, pairs)
539
525
  end
540
-
541
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
542
- get_header RACK_REQUEST_FORM_HASH
543
526
  else
544
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
545
- set_header(RACK_REQUEST_FORM_HASH, {})
527
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
546
528
  end
547
529
  rescue => error
548
530
  set_header(RACK_REQUEST_FORM_ERROR, error)
@@ -550,6 +532,21 @@ module Rack
550
532
  end
551
533
  end
552
534
 
535
+ # Returns the data received in the request body.
536
+ #
537
+ # This method support both application/x-www-form-urlencoded and
538
+ # multipart/form-data.
539
+ def POST
540
+ if form_hash = get_header(RACK_REQUEST_FORM_HASH)
541
+ return form_hash
542
+ elsif error = get_header(RACK_REQUEST_FORM_ERROR)
543
+ raise error.class, error.message, cause: error.cause
544
+ end
545
+
546
+ pairs = form_pairs
547
+ set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
548
+ end
549
+
553
550
  # The union of GET and POST data.
554
551
  #
555
552
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
@@ -557,6 +554,10 @@ module Rack
557
554
  self.GET.merge(self.POST)
558
555
  end
559
556
 
557
+ # Allow overriding the query parser that the receiver will use.
558
+ # By default Rack::Utils.default_query_parser is used.
559
+ attr_writer :query_parser
560
+
560
561
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
561
562
  #
562
563
  # The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
@@ -616,13 +617,6 @@ module Rack
616
617
  Rack::Request.ip_filter.call(ip)
617
618
  end
618
619
 
619
- # like Hash#values_at
620
- def values_at(*keys)
621
- warn("Request#values_at is deprecated and will be removed in a future version of Rack. Please use request.params.values_at instead", uplevel: 1)
622
-
623
- keys.map { |key| params[key] }
624
- end
625
-
626
620
  private
627
621
 
628
622
  def default_session; {}; end
@@ -670,7 +664,7 @@ module Rack
670
664
  end
671
665
 
672
666
  def query_parser
673
- Utils.default_query_parser
667
+ @query_parser || Utils.default_query_parser
674
668
  end
675
669
 
676
670
  def parse_query(qs, d = '&')
@@ -678,6 +672,7 @@ module Rack
678
672
  end
679
673
 
680
674
  def parse_multipart
675
+ warn "Rack::Request#parse_multipart is deprecated and will be removed in a future version of Rack.", uplevel: 1
681
676
  Rack::Multipart.extract_multipart(self, query_parser)
682
677
  end
683
678
 
@@ -692,7 +687,7 @@ module Rack
692
687
  end
693
688
 
694
689
  def split_header(value)
695
- value ? value.strip.split(/[,\s]+/) : []
690
+ value ? value.strip.split(/[, \t]+/) : []
696
691
  end
697
692
 
698
693
  # ipv6 extracted from resolv stdlib, simplified
@@ -740,10 +735,6 @@ module Rack
740
735
  return match[:host], match[:address], match[:port]&.to_i
741
736
  end
742
737
 
743
- def reject_trusted_ip_addresses(ip_addresses)
744
- ip_addresses.reject { |ip| trusted_proxy?(ip) }
745
- end
746
-
747
738
  FORWARDED_SCHEME_HEADERS = {
748
739
  proto: HTTP_X_FORWARDED_PROTO,
749
740
  scheme: HTTP_X_FORWARDED_SCHEME
@@ -21,7 +21,10 @@ module Rack
21
21
  end
22
22
 
23
23
  def call(env)
24
- env[RACK_INPUT] = RewindableInput.new(env[RACK_INPUT])
24
+ if (input = env[RACK_INPUT])
25
+ env[RACK_INPUT] = RewindableInput.new(input)
26
+ end
27
+
25
28
  @app.call(env)
26
29
  end
27
30
  end
@@ -65,8 +65,12 @@ module Rack
65
65
  def dump_exception(exception)
66
66
  if exception.respond_to?(:detailed_message)
67
67
  message = exception.detailed_message(highlight: false)
68
+ # :nocov:
69
+ # Ruby 3.2 added Exception#detailed_message, so the else
70
+ # branch cannot be hit on the current Ruby version.
68
71
  else
69
72
  message = exception.message
73
+ # :nocov:
70
74
  end
71
75
  string = "#{exception.class}: #{message}\n".dup
72
76
  string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
@@ -401,7 +405,5 @@ module Rack
401
405
  </body>
402
406
  </html>
403
407
  HTML
404
-
405
- # :startdoc:
406
408
  end
407
409
  end
@@ -117,7 +117,5 @@ TEMPLATE = <<'HTML'
117
117
  </body>
118
118
  </html>
119
119
  HTML
120
-
121
- # :startdoc:
122
120
  end
123
121
  end
data/lib/rack/utils.rb CHANGED
@@ -181,12 +181,16 @@ module Rack
181
181
  # doesn't get monkey-patched by rails
182
182
  if defined?(ERB::Escape) && ERB::Escape.instance_method(:html_escape)
183
183
  define_method(:escape_html, ERB::Escape.instance_method(:html_escape))
184
+ # :nocov:
185
+ # Ruby 3.2/ERB 4.0 added ERB::Escape#html_escape, so the else
186
+ # branch cannot be hit on the current Ruby version.
184
187
  else
185
188
  require 'cgi/escape'
186
189
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
187
190
  def escape_html(string)
188
191
  CGI.escapeHTML(string.to_s)
189
192
  end
193
+ # :nocov:
190
194
  end
191
195
 
192
196
  def select_best_encoding(available_encodings, accept_encoding)
@@ -254,26 +258,18 @@ module Rack
254
258
  parse_cookies_header env[HTTP_COOKIE]
255
259
  end
256
260
 
257
- # A valid cookie key according to RFC2616.
261
+ # A valid cookie key according to RFC6265 and RFC2616.
258
262
  # A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ " / [ ] ? = { }.
259
263
  VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
260
264
  private_constant :VALID_COOKIE_KEY
261
265
 
262
- private def escape_cookie_key(key)
263
- if key =~ VALID_COOKIE_KEY
264
- key
265
- else
266
- warn "Cookie key #{key.inspect} is not valid according to RFC2616; it will be escaped. This behaviour is deprecated and will be removed in a future version of Rack.", uplevel: 2
267
- escape(key)
268
- end
269
- end
270
-
271
266
  # :call-seq:
272
267
  # set_cookie_header(key, value) -> encoded string
273
268
  #
274
269
  # Generate an encoded string using the provided +key+ and +value+ suitable
275
270
  # for the +set-cookie+ header according to RFC6265. The +value+ may be an
276
- # instance of either +String+ or +Hash+.
271
+ # instance of either +String+ or +Hash+. If the cookie key is invalid (as
272
+ # defined by RFC6265), an +ArgumentError+ will be raised.
277
273
  #
278
274
  # If the cookie +value+ is an instance of +Hash+, it considers the following
279
275
  # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
@@ -281,10 +277,6 @@ module Rack
281
277
  # details about the interpretation of these fields, consult
282
278
  # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
283
279
  #
284
- # An extra cookie attribute +escape_key+ can be provided to control whether
285
- # or not the cookie key is URL encoded. If explicitly set to +false+, the
286
- # cookie key name will not be url encoded (escaped). The default is +true+.
287
- #
288
280
  # set_cookie_header("myname", "myvalue")
289
281
  # # => "myname=myvalue"
290
282
  #
@@ -292,9 +284,12 @@ module Rack
292
284
  # # => "myname=myvalue; max-age=10"
293
285
  #
294
286
  def set_cookie_header(key, value)
287
+ unless key =~ VALID_COOKIE_KEY
288
+ raise ArgumentError, "invalid cookie key: #{key.inspect}"
289
+ end
290
+
295
291
  case value
296
292
  when Hash
297
- key = escape_cookie_key(key) unless value[:escape_key] == false
298
293
  domain = "; domain=#{value[:domain]}" if value[:domain]
299
294
  path = "; path=#{value[:path]}" if value[:path]
300
295
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
@@ -316,8 +311,6 @@ module Rack
316
311
  end
317
312
  partitioned = "; partitioned" if value[:partitioned]
318
313
  value = value[:value]
319
- else
320
- key = escape_cookie_key(key)
321
314
  end
322
315
 
323
316
  value = [value] unless Array === value
@@ -416,7 +409,7 @@ module Rack
416
409
  return nil if size.zero?
417
410
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
418
411
  ranges = []
419
- $1.split(/,\s*/).each do |range_spec|
412
+ $1.split(/,[ \t]*/).each do |range_spec|
420
413
  return nil unless range_spec.include?('-')
421
414
  range = range_spec.split('-')
422
415
  r0, r1 = range[0], range[1]
@@ -592,11 +585,9 @@ module Rack
592
585
  fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
593
586
  message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
594
587
  if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
595
- # message = "#{message} Please use #{canonical_symbol.inspect} instead."
596
- # For now, let's not emit any warning when there is a mapping.
597
- else
598
- warn message, uplevel: 3
588
+ message = "#{message} Please use #{canonical_symbol.inspect} instead."
599
589
  end
590
+ warn message, uplevel: 3
600
591
  fallback_code
601
592
  end
602
593
  else