rack 3.1.20 → 3.1.21
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -1
- data/lib/rack/directory.rb +1 -1
- data/lib/rack/files.rb +1 -1
- data/lib/rack/multipart/parser.rb +35 -2
- data/lib/rack/request.rb +2 -2
- data/lib/rack/sendfile.rb +2 -2
- data/lib/rack/static.rb +7 -3
- data/lib/rack/utils.rb +107 -15
- data/lib/rack/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7aaf1007eae50de14eec6eaa6a4d586f1fe15b614bfe77bf68bea65c6ad59423
|
|
4
|
+
data.tar.gz: 56ce4db8b9c1aeffaf750836ea16bf264841909aeb4befb81719e45189742ac3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3917f67c856cf670e2f3eaf4432b74366f8b5ee09a78ae2acad9075b10cca355fc718b5025fed92091b56187e4e614fc5ab24066c26298a3af4352ec064fe490
|
|
7
|
+
data.tar.gz: 6a741930a02797a64a50bf7fb8e11957e4bf0c5771a26e0491c131c3ad71d0e120f5a70ceec80fe12c0d866daaa822eb0cdcb9f55a9a2d509ef01659fde08231
|
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## [3.1.21] - 2026-04-01
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- [CVE-2026-34763](https://github.com/advisories/GHSA-7mqq-6cf9-v2qp) Root directory disclosure via unescaped regex interpolation in `Rack::Directory`.
|
|
10
|
+
- [CVE-2026-34230](https://github.com/advisories/GHSA-v569-hp3g-36wr) Avoid O(n^2) algorithm in `Rack::Utils.select_best_encoding` which could lead to denial of service.
|
|
11
|
+
- [CVE-2026-32762](https://github.com/advisories/GHSA-qfgr-crr9-7r49) Forwarded header semicolon injection enables Host and Scheme spoofing.
|
|
12
|
+
- [CVE-2026-26961](https://github.com/advisories/GHSA-vgpv-f759-9wx3) Raise error for multipart requests with multiple boundary parameters.
|
|
13
|
+
- [CVE-2026-34786](https://github.com/advisories/GHSA-q4qf-9j86-f5mh) `Rack::Static` `header_rules` bypass via URL-encoded path mismatch.
|
|
14
|
+
- [CVE-2026-34831](https://github.com/advisories/GHSA-q2ww-5357-x388) `Content-Length` mismatch in `Rack::Files` error responses.
|
|
15
|
+
- [CVE-2026-34826](https://github.com/advisories/GHSA-x8cg-fq8g-mxfx) Multipart byte range processing allows denial of service via excessive overlapping ranges.
|
|
16
|
+
- [CVE-2026-34835](https://github.com/advisories/GHSA-g2pf-xv49-m2h5) `Rack::Request` accepts invalid Host characters, enabling host allowlist bypass.
|
|
17
|
+
- [CVE-2026-34830](https://github.com/advisories/GHSA-qv7j-4883-hwh7) `Rack::Sendfile` header-based `X-Accel-Mapping` regex injection enables unauthorized `X-Accel-Redirect`.
|
|
18
|
+
- [CVE-2026-34785](https://github.com/advisories/GHSA-h2jq-g4cq-5ppq) `Rack::Static` prefix matching can expose unintended files under the static root.
|
|
19
|
+
- [CVE-2026-34829](https://github.com/advisories/GHSA-8vqr-qjwx-82mw) Multipart parsing without `Content-Length` header allows unbounded chunked file uploads.
|
|
20
|
+
- [CVE-2026-34827](https://github.com/advisories/GHSA-v6x5-cg8r-vv6x) Quadratic-time multipart header parsing allows denial of service via escape-heavy quoted parameters.
|
|
21
|
+
|
|
22
|
+
## [3.1.20] - 2026-02-16
|
|
6
23
|
|
|
7
24
|
### Security
|
|
8
25
|
|
|
@@ -393,6 +410,13 @@ Rack v3.1 is primarily a maintenance release that removes features deprecated in
|
|
|
393
410
|
- Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm))
|
|
394
411
|
- `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst))
|
|
395
412
|
|
|
413
|
+
## [2.2.22] - 2026-02-16
|
|
414
|
+
|
|
415
|
+
### Security
|
|
416
|
+
|
|
417
|
+
- [CVE-2026-25500](https://github.com/advisories/GHSA-whrj-4476-wvmp) XSS injection via malicious filename in `Rack::Directory`.
|
|
418
|
+
- [CVE-2026-22860](https://github.com/advisories/GHSA-mxw3-3hh2-x2mh) Directory traversal via root prefix bypass in `Rack::Directory`.
|
|
419
|
+
|
|
396
420
|
## [2.2.21] - 2025-11-03
|
|
397
421
|
|
|
398
422
|
### Fixed
|
data/lib/rack/directory.rb
CHANGED
|
@@ -51,7 +51,7 @@ table { width:100%%; }
|
|
|
51
51
|
class DirectoryBody < Struct.new(:root, :path, :files)
|
|
52
52
|
# Yield strings for each part of the directory entry
|
|
53
53
|
def each
|
|
54
|
-
show_path = Utils.escape_html(path.sub(
|
|
54
|
+
show_path = Utils.escape_html(path.sub(/\A#{Regexp.escape(root)}/, ''))
|
|
55
55
|
yield(DIR_PAGE_HEADER % [ show_path, show_path ])
|
|
56
56
|
|
|
57
57
|
unless path.chomp('/') == root
|
data/lib/rack/files.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Rack
|
|
|
33
33
|
EOL = "\r\n"
|
|
34
34
|
FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
|
|
35
35
|
HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
|
|
36
|
-
MULTIPART = %r|\Amultipart
|
|
36
|
+
MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
|
|
37
37
|
MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
|
|
38
38
|
MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
|
|
39
39
|
MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
|
|
@@ -68,6 +68,13 @@ module Rack
|
|
|
68
68
|
BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
|
|
69
69
|
private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
|
|
70
70
|
|
|
71
|
+
bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
|
|
72
|
+
PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
|
|
73
|
+
private_constant :PARSER_BYTESIZE_LIMIT
|
|
74
|
+
|
|
75
|
+
CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
|
|
76
|
+
private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
|
|
77
|
+
|
|
71
78
|
class BoundedIO # :nodoc:
|
|
72
79
|
def initialize(io, content_length)
|
|
73
80
|
@io = io
|
|
@@ -104,7 +111,15 @@ module Rack
|
|
|
104
111
|
return unless content_type
|
|
105
112
|
data = content_type.match(MULTIPART)
|
|
106
113
|
return unless data
|
|
107
|
-
|
|
114
|
+
|
|
115
|
+
unless data[1].empty?
|
|
116
|
+
raise Error, "whitespace between boundary parameter name and equal sign"
|
|
117
|
+
end
|
|
118
|
+
if data.post_match.match?(/boundary\s*=/i)
|
|
119
|
+
raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
data[2]
|
|
108
123
|
end
|
|
109
124
|
|
|
110
125
|
def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
|
|
@@ -113,6 +128,10 @@ module Rack
|
|
|
113
128
|
boundary = parse_boundary content_type
|
|
114
129
|
return EMPTY unless boundary
|
|
115
130
|
|
|
131
|
+
if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
|
|
132
|
+
raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
|
|
133
|
+
end
|
|
134
|
+
|
|
116
135
|
if boundary.length > 70
|
|
117
136
|
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
|
|
118
137
|
# Most clients use no more than 55 characters.
|
|
@@ -229,6 +248,8 @@ module Rack
|
|
|
229
248
|
@mime_index = 0
|
|
230
249
|
@body_retained = nil
|
|
231
250
|
@retained_size = 0
|
|
251
|
+
@total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
|
|
252
|
+
@content_disposition_quoted_escapes = 0
|
|
232
253
|
@collector = Collector.new tempfile
|
|
233
254
|
|
|
234
255
|
@sbuf = StringScanner.new("".dup)
|
|
@@ -240,6 +261,7 @@ module Rack
|
|
|
240
261
|
end
|
|
241
262
|
|
|
242
263
|
def parse(io)
|
|
264
|
+
@total_bytes_read &&= nil if io.is_a?(BoundedIO)
|
|
243
265
|
outbuf = String.new
|
|
244
266
|
read_data(io, outbuf)
|
|
245
267
|
|
|
@@ -283,6 +305,12 @@ module Rack
|
|
|
283
305
|
def read_data(io, outbuf)
|
|
284
306
|
content = io.read(@bufsize, outbuf)
|
|
285
307
|
handle_empty_content!(content)
|
|
308
|
+
if @total_bytes_read
|
|
309
|
+
@total_bytes_read += content.bytesize
|
|
310
|
+
if @total_bytes_read > PARSER_BYTESIZE_LIMIT
|
|
311
|
+
raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
|
|
312
|
+
end
|
|
313
|
+
end
|
|
286
314
|
@sbuf.concat(content)
|
|
287
315
|
end
|
|
288
316
|
|
|
@@ -376,6 +404,11 @@ module Rack
|
|
|
376
404
|
# stop parsing parameter value if found ending quote
|
|
377
405
|
break if c == '"'
|
|
378
406
|
|
|
407
|
+
@content_disposition_quoted_escapes += 1
|
|
408
|
+
if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
|
|
409
|
+
raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
|
|
410
|
+
end
|
|
411
|
+
|
|
379
412
|
escaped_char = disposition.slice!(0, 1)
|
|
380
413
|
if param == 'filename' && escaped_char != '"'
|
|
381
414
|
# Possible IE uploaded filename, append both escape backslash and value
|
data/lib/rack/request.rb
CHANGED
|
@@ -728,8 +728,8 @@ module Rack
|
|
|
728
728
|
# Match IPv6 as a string of hex digits and colons in square brackets
|
|
729
729
|
\[(?<address>#{ipv6})\]
|
|
730
730
|
|
|
|
731
|
-
# Match
|
|
732
|
-
(?<address>[
|
|
731
|
+
# Match characters allowed by RFC 3986 Section 3.2.2
|
|
732
|
+
(?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
|
|
733
733
|
)
|
|
734
734
|
(:(?<port>\d+))?
|
|
735
735
|
\z
|
data/lib/rack/sendfile.rb
CHANGED
|
@@ -51,7 +51,7 @@ module Rack
|
|
|
51
51
|
#
|
|
52
52
|
# The `x-accel-mapping` header should specify the location on the file system,
|
|
53
53
|
# followed by an equals sign (=), followed name of the private URL pattern
|
|
54
|
-
# that it maps to. The middleware performs a
|
|
54
|
+
# that it maps to. The middleware performs a case-insensitive substitution on the
|
|
55
55
|
# resulting path.
|
|
56
56
|
#
|
|
57
57
|
# To enable `x-accel-redirect`, you must configure the middleware explicitly:
|
|
@@ -186,7 +186,7 @@ module Rack
|
|
|
186
186
|
# Safe to use header: explicit config + no app mappings:
|
|
187
187
|
mapping.split(',').map(&:strip).each do |m|
|
|
188
188
|
internal, external = m.split('=', 2).map(&:strip)
|
|
189
|
-
new_path = path.sub(/\A#{internal}/i, external)
|
|
189
|
+
new_path = path.sub(/\A#{Regexp.escape(internal)}/i, external)
|
|
190
190
|
return new_path unless path == new_path
|
|
191
191
|
end
|
|
192
192
|
|
data/lib/rack/static.rb
CHANGED
|
@@ -93,6 +93,9 @@ 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
|
|
96
99
|
@index = options[:index]
|
|
97
100
|
@gzip = options[:gzip]
|
|
98
101
|
@cascade = options[:cascade]
|
|
@@ -115,7 +118,7 @@ module Rack
|
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
def route_file(path)
|
|
118
|
-
@urls.kind_of?(Array) && @urls.any? { |url| path.
|
|
121
|
+
@urls.kind_of?(Array) && @urls.any? { |url, url_slash| path == url || path.start_with?(url_slash) }
|
|
119
122
|
end
|
|
120
123
|
|
|
121
124
|
def can_serve(path)
|
|
@@ -165,6 +168,8 @@ module Rack
|
|
|
165
168
|
|
|
166
169
|
# Convert HTTP header rules to HTTP headers
|
|
167
170
|
def applicable_rules(path)
|
|
171
|
+
path = ::Rack::Utils.unescape_path(path)
|
|
172
|
+
|
|
168
173
|
@header_rules.find_all do |rule, new_headers|
|
|
169
174
|
case rule
|
|
170
175
|
when :all
|
|
@@ -172,10 +177,9 @@ module Rack
|
|
|
172
177
|
when :fonts
|
|
173
178
|
/\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
|
|
174
179
|
when String
|
|
175
|
-
path = ::Rack::Utils.unescape(path)
|
|
176
180
|
path.start_with?(rule) || path.start_with?('/' + rule)
|
|
177
181
|
when Array
|
|
178
|
-
|
|
182
|
+
/\.#{Regexp.union(rule)}\z/.match?(path)
|
|
179
183
|
when Regexp
|
|
180
184
|
rule.match?(path)
|
|
181
185
|
else
|
data/lib/rack/utils.rb
CHANGED
|
@@ -146,17 +146,77 @@ module Rack
|
|
|
146
146
|
end
|
|
147
147
|
end
|
|
148
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
|
+
|
|
149
152
|
def forwarded_values(forwarded_header)
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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!
|
|
158
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?
|
|
159
217
|
end
|
|
218
|
+
|
|
219
|
+
params
|
|
160
220
|
end
|
|
161
221
|
module_function :forwarded_values
|
|
162
222
|
|
|
@@ -189,17 +249,41 @@ module Rack
|
|
|
189
249
|
end
|
|
190
250
|
end
|
|
191
251
|
|
|
252
|
+
# Given an array of available encoding strings, and an array of
|
|
253
|
+
# acceptable encodings for a request, where each element of the
|
|
254
|
+
# acceptable encodings array is an array where the first element
|
|
255
|
+
# is an encoding name and the second element is the numeric
|
|
256
|
+
# priority for the encoding, return the available encoding with
|
|
257
|
+
# the highest priority.
|
|
258
|
+
#
|
|
259
|
+
# The accept_encoding argument is typically generated by calling
|
|
260
|
+
# Request#accept_encoding.
|
|
261
|
+
#
|
|
262
|
+
# Example:
|
|
263
|
+
#
|
|
264
|
+
# select_best_encoding(%w(compress gzip identity),
|
|
265
|
+
# [["compress", 0.5], ["gzip", 1.0]])
|
|
266
|
+
# # => "gzip"
|
|
267
|
+
#
|
|
268
|
+
# To reduce denial of service potential, only the first 16
|
|
269
|
+
# acceptable encodings are considered.
|
|
192
270
|
def select_best_encoding(available_encodings, accept_encoding)
|
|
193
271
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
|
194
272
|
|
|
273
|
+
# Only process the first 16 encodings
|
|
274
|
+
accept_encoding = accept_encoding[0...16]
|
|
195
275
|
expanded_accept_encoding = []
|
|
276
|
+
wildcard_seen = false
|
|
196
277
|
|
|
197
278
|
accept_encoding.each do |m, q|
|
|
198
279
|
preference = available_encodings.index(m) || available_encodings.size
|
|
199
280
|
|
|
200
281
|
if m == "*"
|
|
201
|
-
|
|
202
|
-
|
|
282
|
+
unless wildcard_seen
|
|
283
|
+
(available_encodings - accept_encoding.map(&:first)).each do |m2|
|
|
284
|
+
expanded_accept_encoding << [m2, q, preference]
|
|
285
|
+
end
|
|
286
|
+
wildcard_seen = true
|
|
203
287
|
end
|
|
204
288
|
else
|
|
205
289
|
expanded_accept_encoding << [m, q, preference]
|
|
@@ -207,7 +291,13 @@ module Rack
|
|
|
207
291
|
end
|
|
208
292
|
|
|
209
293
|
encoding_candidates = expanded_accept_encoding
|
|
210
|
-
.
|
|
294
|
+
.sort do |(_, q1, p1), (_, q2, p2)|
|
|
295
|
+
if r = (q1 <=> q2).nonzero?
|
|
296
|
+
-r
|
|
297
|
+
else
|
|
298
|
+
(p1 <=> p2).nonzero? || 0
|
|
299
|
+
end
|
|
300
|
+
end
|
|
211
301
|
.map!(&:first)
|
|
212
302
|
|
|
213
303
|
unless encoding_candidates.include?("identity")
|
|
@@ -406,17 +496,19 @@ module Rack
|
|
|
406
496
|
# Parses the "Range:" header, if present, into an array of Range objects.
|
|
407
497
|
# Returns nil if the header is missing or syntactically invalid.
|
|
408
498
|
# Returns an empty array if none of the ranges are satisfiable.
|
|
409
|
-
def byte_ranges(env, size)
|
|
410
|
-
get_byte_ranges env['HTTP_RANGE'], size
|
|
499
|
+
def byte_ranges(env, size, max_ranges: 100)
|
|
500
|
+
get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges
|
|
411
501
|
end
|
|
412
502
|
|
|
413
|
-
def get_byte_ranges(http_range, size)
|
|
503
|
+
def get_byte_ranges(http_range, size, max_ranges: 100)
|
|
414
504
|
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
|
|
415
505
|
# Ignore Range when file size is 0 to avoid a 416 error.
|
|
416
506
|
return nil if size.zero?
|
|
417
507
|
return nil unless http_range && http_range =~ /bytes=([^;]+)/
|
|
508
|
+
byte_range = $1
|
|
509
|
+
return nil if byte_range.count(',') >= max_ranges
|
|
418
510
|
ranges = []
|
|
419
|
-
|
|
511
|
+
byte_range.split(/,[ \t]*/).each do |range_spec|
|
|
420
512
|
return nil unless range_spec.include?('-')
|
|
421
513
|
range = range_spec.split('-')
|
|
422
514
|
r0, r1 = range[0], range[1]
|
data/lib/rack/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.1.
|
|
4
|
+
version: 3.1.21
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leah Neukirchen
|
|
@@ -156,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
156
156
|
- !ruby/object:Gem::Version
|
|
157
157
|
version: '0'
|
|
158
158
|
requirements: []
|
|
159
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.6
|
|
160
160
|
specification_version: 4
|
|
161
161
|
summary: A modular Ruby webserver interface.
|
|
162
162
|
test_files: []
|