rack 3.1.21 → 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
@@ -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,70 +489,42 @@ 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
- # Add 2 bytes. One to check whether it is over the limit, and a second
532
- # in case the slice! call below removes the last byte
533
- # If read returns nil, use the empty string
534
- form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
516
+ form_vars = get_header(RACK_INPUT).read
535
517
 
536
518
  # Fix for Safari Ajax postings that always append \0
537
519
  # form_vars.sub!(/\0\z/, '') # performance replacement:
538
520
  form_vars.slice!(-1) if form_vars.end_with?("\0")
539
521
 
540
522
  set_header RACK_REQUEST_FORM_VARS, form_vars
541
- 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)
542
525
  end
543
-
544
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
545
- get_header RACK_REQUEST_FORM_HASH
546
526
  else
547
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
548
- set_header(RACK_REQUEST_FORM_HASH, {})
527
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
549
528
  end
550
529
  rescue => error
551
530
  set_header(RACK_REQUEST_FORM_ERROR, error)
@@ -553,6 +532,21 @@ module Rack
553
532
  end
554
533
  end
555
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
+
556
550
  # The union of GET and POST data.
557
551
  #
558
552
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
@@ -560,6 +554,10 @@ module Rack
560
554
  self.GET.merge(self.POST)
561
555
  end
562
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
+
563
561
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
564
562
  #
565
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.
@@ -619,13 +617,6 @@ module Rack
619
617
  Rack::Request.ip_filter.call(ip)
620
618
  end
621
619
 
622
- # like Hash#values_at
623
- def values_at(*keys)
624
- 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)
625
-
626
- keys.map { |key| params[key] }
627
- end
628
-
629
620
  private
630
621
 
631
622
  def default_session; {}; end
@@ -673,7 +664,7 @@ module Rack
673
664
  end
674
665
 
675
666
  def query_parser
676
- Utils.default_query_parser
667
+ @query_parser || Utils.default_query_parser
677
668
  end
678
669
 
679
670
  def parse_query(qs, d = '&')
@@ -681,6 +672,7 @@ module Rack
681
672
  end
682
673
 
683
674
  def parse_multipart
675
+ warn "Rack::Request#parse_multipart is deprecated and will be removed in a future version of Rack.", uplevel: 1
684
676
  Rack::Multipart.extract_multipart(self, query_parser)
685
677
  end
686
678
 
@@ -695,7 +687,7 @@ module Rack
695
687
  end
696
688
 
697
689
  def split_header(value)
698
- value ? value.strip.split(/[,\s]+/) : []
690
+ value ? value.strip.split(/[, \t]+/) : []
699
691
  end
700
692
 
701
693
  # ipv6 extracted from resolv stdlib, simplified
@@ -728,8 +720,8 @@ module Rack
728
720
  # Match IPv6 as a string of hex digits and colons in square brackets
729
721
  \[(?<address>#{ipv6})\]
730
722
  |
731
- # Match characters allowed by RFC 3986 Section 3.2.2
732
- (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
723
+ # Match any other printable string (except square brackets) as a hostname
724
+ (?<address>[[[:graph:]&&[^\[\]]]]*?)
733
725
  )
734
726
  (:(?<port>\d+))?
735
727
  \z
@@ -743,10 +735,6 @@ module Rack
743
735
  return match[:host], match[:address], match[:port]&.to_i
744
736
  end
745
737
 
746
- def reject_trusted_ip_addresses(ip_addresses)
747
- ip_addresses.reject { |ip| trusted_proxy?(ip) }
748
- end
749
-
750
738
  FORWARDED_SCHEME_HEADERS = {
751
739
  proto: HTTP_X_FORWARDED_PROTO,
752
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.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