rack 3.1.23 → 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.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

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
@@ -320,10 +325,8 @@ module Rack
320
325
  end
321
326
 
322
327
  def host_with_port(authority = self.authority)
323
- parts = split_authority(authority)
324
- return nil if parts.empty?
328
+ host, _, port = split_authority(authority)
325
329
 
326
- host, _, port = parts
327
330
  if port == DEFAULT_PORTS[self.scheme]
328
331
  host
329
332
  else
@@ -400,8 +403,8 @@ module Rack
400
403
  return forwarded.last
401
404
  end
402
405
  when :x_forwarded
403
- if value = get_header(HTTP_X_FORWARDED_HOST)
404
- 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)
405
408
  end
406
409
  end
407
410
  end
@@ -415,10 +418,9 @@ module Rack
415
418
 
416
419
  def ip
417
420
  remote_addresses = split_header(get_header('REMOTE_ADDR'))
418
- external_addresses = reject_trusted_ip_addresses(remote_addresses)
419
421
 
420
- unless external_addresses.empty?
421
- return external_addresses.last
422
+ remote_addresses.reverse_each do |ip|
423
+ return ip unless trusted_proxy?(ip)
422
424
  end
423
425
 
424
426
  if (forwarded_for = self.forwarded_for) && !forwarded_for.empty?
@@ -426,7 +428,10 @@ module Rack
426
428
  # So we reject all the trusted addresses (proxy*) and return the
427
429
  # last client. Or if we trust everyone, we just return the first
428
430
  # address.
429
- 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
430
435
  end
431
436
 
432
437
  # If all the addresses are trusted, and we aren't forwarded, just return
@@ -484,70 +489,42 @@ module Rack
484
489
 
485
490
  # Returns the data received in the query string.
486
491
  def GET
487
- rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
488
- query_string = self.query_string
489
- if rr_query_string == query_string
490
- get_header(RACK_REQUEST_QUERY_HASH)
491
- else
492
- if rr_query_string
493
- 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
494
- end
495
- query_hash = parse_query(query_string, '&')
496
- set_header(RACK_REQUEST_QUERY_STRING, query_string)
497
- set_header(RACK_REQUEST_QUERY_HASH, query_hash)
498
- end
492
+ get_header(RACK_REQUEST_QUERY_HASH) || set_header(RACK_REQUEST_QUERY_HASH, parse_query(query_string, '&'))
499
493
  end
500
494
 
501
- # Returns the data received in the request body.
495
+ # Returns the form data pairs received in the request body.
502
496
  #
503
497
  # This method support both application/x-www-form-urlencoded and
504
498
  # multipart/form-data.
505
- def POST
506
- 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)
507
503
  raise error.class, error.message, cause: error.cause
508
504
  end
509
505
 
510
506
  begin
511
507
  rack_input = get_header(RACK_INPUT)
512
508
 
513
- # If the form hash was already memoized:
514
- if form_hash = get_header(RACK_REQUEST_FORM_HASH)
515
- form_input = get_header(RACK_REQUEST_FORM_INPUT)
516
- # And it was memoized from the same input:
517
- if form_input.equal?(rack_input)
518
- return form_hash
519
- elsif form_input
520
- 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
521
- end
522
- end
523
-
524
509
  # Otherwise, figure out how to parse the input:
525
510
  if rack_input.nil?
526
- set_header RACK_REQUEST_FORM_INPUT, nil
527
- set_header(RACK_REQUEST_FORM_HASH, {})
511
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
528
512
  elsif form_data? || parseable_data?
529
513
  if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
530
514
  set_header RACK_REQUEST_FORM_PAIRS, pairs
531
- set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
532
515
  else
533
- # Add 2 bytes. One to check whether it is over the limit, and a second
534
- # in case the slice! call below removes the last byte
535
- # If read returns nil, use the empty string
536
- form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
516
+ form_vars = get_header(RACK_INPUT).read
537
517
 
538
518
  # Fix for Safari Ajax postings that always append \0
539
519
  # form_vars.sub!(/\0\z/, '') # performance replacement:
540
520
  form_vars.slice!(-1) if form_vars.end_with?("\0")
541
521
 
542
522
  set_header RACK_REQUEST_FORM_VARS, form_vars
543
- 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)
544
525
  end
545
-
546
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
547
- get_header RACK_REQUEST_FORM_HASH
548
526
  else
549
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
550
- set_header(RACK_REQUEST_FORM_HASH, {})
527
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
551
528
  end
552
529
  rescue => error
553
530
  set_header(RACK_REQUEST_FORM_ERROR, error)
@@ -555,6 +532,21 @@ module Rack
555
532
  end
556
533
  end
557
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
+
558
550
  # The union of GET and POST data.
559
551
  #
560
552
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
@@ -562,6 +554,10 @@ module Rack
562
554
  self.GET.merge(self.POST)
563
555
  end
564
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
+
565
561
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
566
562
  #
567
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.
@@ -593,8 +589,7 @@ module Rack
593
589
  end
594
590
 
595
591
  def base_url
596
- hostport = host_with_port || host_with_port(server_authority)
597
- "#{scheme}://#{hostport}"
592
+ "#{scheme}://#{host_with_port}"
598
593
  end
599
594
 
600
595
  # Tries to return a remake of the original request URL as a string.
@@ -622,13 +617,6 @@ module Rack
622
617
  Rack::Request.ip_filter.call(ip)
623
618
  end
624
619
 
625
- # like Hash#values_at
626
- def values_at(*keys)
627
- 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)
628
-
629
- keys.map { |key| params[key] }
630
- end
631
-
632
620
  private
633
621
 
634
622
  def default_session; {}; end
@@ -676,7 +664,7 @@ module Rack
676
664
  end
677
665
 
678
666
  def query_parser
679
- Utils.default_query_parser
667
+ @query_parser || Utils.default_query_parser
680
668
  end
681
669
 
682
670
  def parse_query(qs, d = '&')
@@ -684,6 +672,7 @@ module Rack
684
672
  end
685
673
 
686
674
  def parse_multipart
675
+ warn "Rack::Request#parse_multipart is deprecated and will be removed in a future version of Rack.", uplevel: 1
687
676
  Rack::Multipart.extract_multipart(self, query_parser)
688
677
  end
689
678
 
@@ -698,7 +687,7 @@ module Rack
698
687
  end
699
688
 
700
689
  def split_header(value)
701
- value ? value.strip.split(/[,\s]+/) : []
690
+ value ? value.strip.split(/[, \t]+/) : []
702
691
  end
703
692
 
704
693
  # ipv6 extracted from resolv stdlib, simplified
@@ -731,8 +720,8 @@ module Rack
731
720
  # Match IPv6 as a string of hex digits and colons in square brackets
732
721
  \[(?<address>#{ipv6})\]
733
722
  |
734
- # Match characters allowed by RFC 3986 Section 3.2.2
735
- (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
723
+ # Match any other printable string (except square brackets) as a hostname
724
+ (?<address>[[[:graph:]&&[^\[\]]]]*?)
736
725
  )
737
726
  (:(?<port>\d+))?
738
727
  \z
@@ -746,10 +735,6 @@ module Rack
746
735
  return match[:host], match[:address], match[:port]&.to_i
747
736
  end
748
737
 
749
- def reject_trusted_ip_addresses(ip_addresses)
750
- ip_addresses.reject { |ip| trusted_proxy?(ip) }
751
- end
752
-
753
738
  FORWARDED_SCHEME_HEADERS = {
754
739
  proto: HTTP_X_FORWARDED_PROTO,
755
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
data/lib/rack/sendfile.rb CHANGED
@@ -16,21 +16,21 @@ module Rack
16
16
  # delivery code.
17
17
  #
18
18
  # In order to take advantage of this middleware, the response body must
19
- # respond to +to_path+ and the request must include an `x-sendfile-type`
19
+ # respond to +to_path+ and the request must include an x-sendfile-type
20
20
  # header. Rack::Files and other components implement +to_path+ so there's
21
- # rarely anything you need to do in your application. The `x-sendfile-type`
21
+ # rarely anything you need to do in your application. The x-sendfile-type
22
22
  # header is typically set in your web servers configuration. The following
23
23
  # sections attempt to document
24
24
  #
25
25
  # === Nginx
26
26
  #
27
- # Nginx supports the `x-accel-redirect` header. This is similar to `x-sendfile`
27
+ # Nginx supports the x-accel-redirect header. This is similar to x-sendfile
28
28
  # but requires parts of the filesystem to be mapped into a private URL
29
29
  # hierarchy.
30
30
  #
31
31
  # The following example shows the Nginx configuration required to create
32
- # a private "/files/" area, enable `x-accel-redirect`, and pass the special
33
- # `x-accel-mapping` header to the backend:
32
+ # a private "/files/" area, enable x-accel-redirect, and pass the special
33
+ # x-sendfile-type and x-accel-mapping headers to the backend:
34
34
  #
35
35
  # location ~ /files/(.*) {
36
36
  # internal;
@@ -44,29 +44,24 @@ module Rack
44
44
  # proxy_set_header X-Real-IP $remote_addr;
45
45
  # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
46
46
  #
47
+ # proxy_set_header x-sendfile-type x-accel-redirect;
47
48
  # proxy_set_header x-accel-mapping /var/www/=/files/;
48
49
  #
49
50
  # proxy_pass http://127.0.0.1:8080/;
50
51
  # }
51
52
  #
52
- # The `x-accel-mapping` header should specify the location on the file system,
53
+ # Note that the x-sendfile-type header must be set exactly as shown above.
54
+ # The x-accel-mapping header should specify the location on the file system,
53
55
  # followed by an equals sign (=), followed name of the private URL pattern
54
- # that it maps to. The middleware performs a case-insensitive substitution on the
56
+ # that it maps to. The middleware performs a simple substitution on the
55
57
  # resulting path.
56
58
  #
57
- # To enable `x-accel-redirect`, you must configure the middleware explicitly:
58
- #
59
- # use Rack::Sendfile, "x-accel-redirect"
60
- #
61
- # For security reasons, the `x-sendfile-type` header from requests is ignored.
62
- # The sendfile variation must be set via the middleware constructor.
63
- #
64
59
  # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
65
60
  #
66
61
  # === lighttpd
67
62
  #
68
- # Lighttpd has supported some variation of the `x-sendfile` header for some
69
- # time, although only recent version support `x-sendfile` in a reverse proxy
63
+ # Lighttpd has supported some variation of the x-sendfile header for some
64
+ # time, although only recent version support x-sendfile in a reverse proxy
70
65
  # configuration.
71
66
  #
72
67
  # $HTTP["host"] == "example.com" {
@@ -88,7 +83,7 @@ module Rack
88
83
  #
89
84
  # === Apache
90
85
  #
91
- # `x-sendfile` is supported under Apache 2.x using a separate module:
86
+ # x-sendfile is supported under Apache 2.x using a separate module:
92
87
  #
93
88
  # https://tn123.org/mod_xsendfile/
94
89
  #
@@ -102,28 +97,16 @@ module Rack
102
97
  # === Mapping parameter
103
98
  #
104
99
  # The third parameter allows for an overriding extension of the
105
- # `x-accel-mapping` header. Mappings should be provided in tuples of internal to
100
+ # x-accel-mapping header. Mappings should be provided in tuples of internal to
106
101
  # external. The internal values may contain regular expression syntax, they
107
102
  # will be matched with case indifference.
108
- #
109
- # When `x-accel-redirect` is explicitly enabled via the variation parameter,
110
- # and no application-level mappings are provided, the middleware will read
111
- # the `x-accel-mapping` header from the proxy. This allows nginx to control
112
- # the path mapping without requiring application-level configuration.
113
- #
114
- # === Security
115
- #
116
- # For security reasons, the `x-sendfile-type` header from HTTP requests is
117
- # ignored. The sendfile variation must be explicitly configured via the
118
- # middleware constructor to prevent information disclosure vulnerabilities
119
- # where attackers could bypass proxy restrictions.
120
103
 
121
104
  class Sendfile
122
105
  def initialize(app, variation = nil, mappings = [])
123
106
  @app = app
124
107
  @variation = variation
125
108
  @mappings = mappings.map do |internal, external|
126
- [/\A#{internal}/i, external]
109
+ [/^#{internal}/i, external]
127
110
  end
128
111
  end
129
112
 
@@ -162,35 +145,22 @@ module Rack
162
145
  end
163
146
 
164
147
  private
165
-
166
148
  def variation(env)
167
- # Note: HTTP_X_SENDFILE_TYPE is intentionally NOT read for security reasons.
168
- # Attackers could use this header to enable x-accel-redirect and bypass proxy restrictions.
169
- @variation || env['sendfile.type']
170
- end
171
-
172
- def x_accel_mapping(env)
173
- # Only allow header when:
174
- # 1. `x-accel-redirect` is explicitly enabled via constructor.
175
- # 2. No application-level mappings are configured.
176
- return nil unless @variation =~ /x-accel-redirect/i
177
- return nil if @mappings.any?
178
-
179
- env['HTTP_X_ACCEL_MAPPING']
149
+ @variation ||
150
+ env['sendfile.type'] ||
151
+ env['HTTP_X_SENDFILE_TYPE']
180
152
  end
181
153
 
182
154
  def map_accel_path(env, path)
183
155
  if mapping = @mappings.find { |internal, _| internal =~ path }
184
- return path.sub(*mapping)
185
- elsif mapping = x_accel_mapping(env)
186
- # Safe to use header: explicit config + no app mappings:
156
+ path.sub(*mapping)
157
+ elsif mapping = env['HTTP_X_ACCEL_MAPPING']
187
158
  mapping.split(',').map(&:strip).each do |m|
188
159
  internal, external = m.split('=', 2).map(&:strip)
189
- new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
160
+ new_path = path.sub(/^#{internal}/i, external)
190
161
  return new_path unless path == new_path
191
162
  end
192
-
193
- return path
163
+ path
194
164
  end
195
165
  end
196
166
  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/static.rb CHANGED
@@ -93,9 +93,6 @@ module Rack
93
93
  def initialize(app, options = {})
94
94
  @app = app
95
95
  @urls = options[:urls] || ["/favicon.ico"]
96
- if @urls.kind_of?(Array)
97
- @urls = @urls.map { |url| [url, url.end_with?('/') ? url : "#{url}/".freeze].freeze }.freeze
98
- end
99
96
  @index = options[:index]
100
97
  @gzip = options[:gzip]
101
98
  @cascade = options[:cascade]
@@ -118,7 +115,7 @@ module Rack
118
115
  end
119
116
 
120
117
  def route_file(path)
121
- @urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
118
+ @urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
122
119
  end
123
120
 
124
121
  def can_serve(path)
@@ -168,8 +165,6 @@ module Rack
168
165
 
169
166
  # Convert HTTP header rules to HTTP headers
170
167
  def applicable_rules(path)
171
- path = ::Rack::Utils.clean_path_info(::Rack::Utils.unescape_path(path))
172
-
173
168
  @header_rules.find_all do |rule, new_headers|
174
169
  case rule
175
170
  when :all
@@ -177,9 +172,10 @@ module Rack
177
172
  when :fonts
178
173
  /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
179
174
  when String
175
+ path = ::Rack::Utils.unescape(path)
180
176
  path.start_with?(rule) || path.start_with?('/' + rule)
181
177
  when Array
182
- /\.#{Regexp.union(rule)}\z/.match?(path)
178
+ /\.(#{rule.join('|')})\z/.match?(path)
183
179
  when Regexp
184
180
  rule.match?(path)
185
181
  else