rack 3.0.18 → 3.1.18

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.
data/lib/rack/request.rb CHANGED
@@ -482,9 +482,14 @@ module Rack
482
482
 
483
483
  # Returns the data received in the query string.
484
484
  def GET
485
- if get_header(RACK_REQUEST_QUERY_STRING) == query_string
485
+ rr_query_string = get_header(RACK_REQUEST_QUERY_STRING)
486
+ query_string = self.query_string
487
+ if rr_query_string == query_string
486
488
  get_header(RACK_REQUEST_QUERY_HASH)
487
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
488
493
  query_hash = parse_query(query_string, '&')
489
494
  set_header(RACK_REQUEST_QUERY_STRING, query_string)
490
495
  set_header(RACK_REQUEST_QUERY_HASH, query_hash)
@@ -505,9 +510,12 @@ module Rack
505
510
 
506
511
  # If the form hash was already memoized:
507
512
  if form_hash = get_header(RACK_REQUEST_FORM_HASH)
513
+ form_input = get_header(RACK_REQUEST_FORM_INPUT)
508
514
  # And it was memoized from the same input:
509
- if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
515
+ if form_input.equal?(rack_input)
510
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
511
519
  end
512
520
  end
513
521
 
@@ -516,8 +524,14 @@ module Rack
516
524
  set_header RACK_REQUEST_FORM_INPUT, nil
517
525
  set_header(RACK_REQUEST_FORM_HASH, {})
518
526
  elsif form_data? || parseable_data?
519
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
520
- form_vars = get_header(RACK_INPUT).read
527
+ if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
528
+ set_header RACK_REQUEST_FORM_PAIRS, pairs
529
+ set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
530
+ 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) || ''
521
535
 
522
536
  # Fix for Safari Ajax postings that always append \0
523
537
  # form_vars.sub!(/\0\z/, '') # performance replacement:
@@ -605,24 +619,10 @@ module Rack
605
619
  Rack::Request.ip_filter.call(ip)
606
620
  end
607
621
 
608
- # shortcut for <tt>request.params[key]</tt>
609
- def [](key)
610
- warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
611
-
612
- params[key.to_s]
613
- end
614
-
615
- # shortcut for <tt>request.params[key] = value</tt>
616
- #
617
- # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
618
- def []=(key, value)
619
- warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
620
-
621
- params[key.to_s] = value
622
- end
623
-
624
622
  # like Hash#values_at
625
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
626
  keys.map { |key| params[key] }
627
627
  end
628
628
 
@@ -645,14 +645,26 @@ module Rack
645
645
  end
646
646
 
647
647
  def parse_http_accept_header(header)
648
- header.to_s.split(",").each(&:strip!).map do |part|
649
- attribute, parameters = part.split(";", 2).each(&:strip!)
648
+ # It would be nice to use filter_map here, but it's Ruby 2.7+
649
+ parts = header.to_s.split(',')
650
+
651
+ parts.map! do |part|
652
+ part.strip!
653
+ next if part.empty?
654
+
655
+ attribute, parameters = part.split(';', 2)
656
+ attribute.strip!
657
+ parameters&.strip!
650
658
  quality = 1.0
651
659
  if parameters and /\Aq=([\d.]+)/ =~ parameters
652
660
  quality = $1.to_f
653
661
  end
654
662
  [attribute, quality]
655
663
  end
664
+
665
+ parts.compact!
666
+
667
+ parts
656
668
  end
657
669
 
658
670
  # Get an array of values set in the RFC 7239 `Forwarded` request header.
@@ -672,6 +684,16 @@ module Rack
672
684
  Rack::Multipart.extract_multipart(self, query_parser)
673
685
  end
674
686
 
687
+ def expand_param_pairs(pairs, query_parser = query_parser())
688
+ params = query_parser.make_params
689
+
690
+ pairs.each do |k, v|
691
+ query_parser.normalize_params(params, k, v)
692
+ end
693
+
694
+ params.to_params_hash
695
+ end
696
+
675
697
  def split_header(value)
676
698
  value ? value.strip.split(/[,\s]+/) : []
677
699
  end
data/lib/rack/response.rb CHANGED
@@ -31,13 +31,6 @@ module Rack
31
31
  attr_accessor :length, :status, :body
32
32
  attr_reader :headers
33
33
 
34
- # Deprecated, use headers instead.
35
- def header
36
- warn 'Rack::Response#header is deprecated and will be removed in Rack 3.1, use #headers instead', uplevel: 1
37
-
38
- headers
39
- end
40
-
41
34
  # Initialize the response object with the specified +body+, +status+
42
35
  # and +headers+.
43
36
  #
@@ -62,7 +55,7 @@ module Rack
62
55
  @status = status.to_i
63
56
 
64
57
  unless headers.is_a?(Hash)
65
- warn "Providing non-hash headers to Rack::Response is deprecated and will be removed in Rack 3.1", uplevel: 1
58
+ raise ArgumentError, "Headers must be a Hash!"
66
59
  end
67
60
 
68
61
  @headers = Headers.new
@@ -79,7 +72,8 @@ module Rack
79
72
  if body.nil?
80
73
  @body = []
81
74
  @buffered = true
82
- @length = 0
75
+ # Body is unspecified - it may be a buffered response, or it may be a HEAD response.
76
+ @length = nil
83
77
  elsif body.respond_to?(:to_str)
84
78
  @body = [body]
85
79
  @buffered = true
@@ -87,7 +81,7 @@ module Rack
87
81
  else
88
82
  @body = body
89
83
  @buffered = nil # undetermined as of yet.
90
- @length = 0
84
+ @length = nil
91
85
  end
92
86
 
93
87
  yield self if block_given?
@@ -106,7 +100,7 @@ module Rack
106
100
  # The response body is an enumerable body and it is not allowed to have an entity body.
107
101
  @body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
108
102
  end
109
-
103
+
110
104
  # Generate a response array consistent with the requirements of the SPEC.
111
105
  # @return [Array] a 3-tuple suitable of `[status, headers, body]`
112
106
  # which is suitable to be returned from the middleware `#call(env)` method.
@@ -118,9 +112,14 @@ module Rack
118
112
  return [@status, @headers, []]
119
113
  else
120
114
  if block_given?
115
+ # We don't add the content-length here as the user has provided a block that can #write additional chunks to the body.
121
116
  @block = block
122
117
  return [@status, @headers, self]
123
118
  else
119
+ # If we know the length of the body, set the content-length header... except if we are chunked? which is a legacy special case where the body might already be encoded and thus the actual encoded body length and the content-length are likely to be different.
120
+ if @length && !chunked?
121
+ @headers[CONTENT_LENGTH] = @length.to_s
122
+ end
124
123
  return [@status, @headers, @body]
125
124
  end
126
125
  end
@@ -138,7 +137,9 @@ module Rack
138
137
  end
139
138
  end
140
139
 
141
- # Append to body and update content-length.
140
+ # Append a chunk to the response body.
141
+ #
142
+ # Converts the response into a buffered response if it wasn't already.
142
143
  #
143
144
  # NOTE: Do not mix #write and direct #body access!
144
145
  #
@@ -320,29 +321,34 @@ module Rack
320
321
 
321
322
  protected
322
323
 
324
+ # Convert the body of this response into an internally buffered Array if possible.
325
+ #
326
+ # `@buffered` is a ternary value which indicates whether the body is buffered. It can be:
327
+ # * `nil` - The body has not been buffered yet.
328
+ # * `true` - The body is buffered as an Array instance.
329
+ # * `false` - The body is not buffered and cannot be buffered.
330
+ #
331
+ # @return [Boolean] whether the body is buffered as an Array instance.
323
332
  def buffered_body!
324
333
  if @buffered.nil?
325
334
  if @body.is_a?(Array)
326
335
  # The user supplied body was an array:
327
336
  @body = @body.compact
328
- @body.each do |part|
329
- @length += part.to_s.bytesize
330
- end
331
-
337
+ @length = @body.sum{|part| part.bytesize}
332
338
  @buffered = true
333
339
  elsif @body.respond_to?(:each)
334
340
  # Turn the user supplied body into a buffered array:
335
341
  body = @body
336
342
  @body = Array.new
343
+ @buffered = true
337
344
 
338
345
  body.each do |part|
339
346
  @writer.call(part.to_s)
340
347
  end
341
348
 
342
349
  body.close if body.respond_to?(:close)
343
-
344
- @buffered = true
345
350
  else
351
+ # We don't know how to buffer the user-supplied body:
346
352
  @buffered = false
347
353
  end
348
354
  end
@@ -351,11 +357,13 @@ module Rack
351
357
  end
352
358
 
353
359
  def append(chunk)
360
+ chunk = chunk.dup unless chunk.frozen?
354
361
  @body << chunk
355
362
 
356
- unless chunked?
363
+ if @length
357
364
  @length += chunk.bytesize
358
- set_header(CONTENT_LENGTH, @length.to_s)
365
+ elsif @buffered
366
+ @length = chunk.bytesize
359
367
  end
360
368
 
361
369
  return chunk
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-sendfile-type and x-accel-mapping headers to the backend:
32
+ # a private "/files/" area, enable `x-accel-redirect`, and pass the special
33
+ # `x-accel-mapping` header to the backend:
34
34
  #
35
35
  # location ~ /files/(.*) {
36
36
  # internal;
@@ -44,24 +44,29 @@ 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;
48
47
  # proxy_set_header x-accel-mapping /var/www/=/files/;
49
48
  #
50
49
  # proxy_pass http://127.0.0.1:8080/;
51
50
  # }
52
51
  #
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,
52
+ # The `x-accel-mapping` header should specify the location on the file system,
55
53
  # followed by an equals sign (=), followed name of the private URL pattern
56
54
  # that it maps to. The middleware performs a simple substitution on the
57
55
  # resulting path.
58
56
  #
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
+ #
59
64
  # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
60
65
  #
61
66
  # === lighttpd
62
67
  #
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
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
65
70
  # configuration.
66
71
  #
67
72
  # $HTTP["host"] == "example.com" {
@@ -83,7 +88,7 @@ module Rack
83
88
  #
84
89
  # === Apache
85
90
  #
86
- # x-sendfile is supported under Apache 2.x using a separate module:
91
+ # `x-sendfile` is supported under Apache 2.x using a separate module:
87
92
  #
88
93
  # https://tn123.org/mod_xsendfile/
89
94
  #
@@ -97,16 +102,28 @@ module Rack
97
102
  # === Mapping parameter
98
103
  #
99
104
  # The third parameter allows for an overriding extension of the
100
- # x-accel-mapping header. Mappings should be provided in tuples of internal to
105
+ # `x-accel-mapping` header. Mappings should be provided in tuples of internal to
101
106
  # external. The internal values may contain regular expression syntax, they
102
107
  # 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.
103
120
 
104
121
  class Sendfile
105
122
  def initialize(app, variation = nil, mappings = [])
106
123
  @app = app
107
124
  @variation = variation
108
125
  @mappings = mappings.map do |internal, external|
109
- [/^#{internal}/i, external]
126
+ [/\A#{internal}/i, external]
110
127
  end
111
128
  end
112
129
 
@@ -145,22 +162,35 @@ module Rack
145
162
  end
146
163
 
147
164
  private
165
+
148
166
  def variation(env)
149
- @variation ||
150
- env['sendfile.type'] ||
151
- env['HTTP_X_SENDFILE_TYPE']
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']
152
180
  end
153
181
 
154
182
  def map_accel_path(env, path)
155
183
  if mapping = @mappings.find { |internal, _| internal =~ path }
156
- path.sub(*mapping)
157
- elsif mapping = env['HTTP_X_ACCEL_MAPPING']
184
+ return path.sub(*mapping)
185
+ elsif mapping = x_accel_mapping(env)
186
+ # Safe to use header: explicit config + no app mappings:
158
187
  mapping.split(',').map(&:strip).each do |m|
159
188
  internal, external = m.split('=', 2).map(&:strip)
160
- new_path = path.sub(/^#{internal}/i, external)
189
+ new_path = path.sub(/\A#{internal}/i, external)
161
190
  return new_path unless path == new_path
162
191
  end
163
- path
192
+
193
+ return path
164
194
  end
165
195
  end
166
196
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
3
  require 'erb'
5
4
 
6
5
  require_relative 'constants'
@@ -19,6 +18,11 @@ module Rack
19
18
  class ShowExceptions
20
19
  CONTEXT = 7
21
20
 
21
+ Frame = Struct.new(:filename, :lineno, :function,
22
+ :pre_context_lineno, :pre_context,
23
+ :context_line, :post_context_lineno,
24
+ :post_context)
25
+
22
26
  def initialize(app)
23
27
  @app = app
24
28
  end
@@ -79,7 +83,7 @@ module Rack
79
83
  # This double assignment is to prevent an "unused variable" warning.
80
84
  # Yes, it is dumb, but I don't like Ruby yelling at me.
81
85
  frames = frames = exception.backtrace.map { |line|
82
- frame = OpenStruct.new
86
+ frame = Frame.new
83
87
  if line =~ /(.*?):(\d+)(:in `(.*)')?/
84
88
  frame.filename = $1
85
89
  frame.lineno = $2.to_i