diff --git a/.github/workflows/test-external.yaml b/.github/workflows/test-external.yaml
index 59c25ef4f..bcf7a6c72 100644
--- a/.github/workflows/test-external.yaml
+++ b/.github/workflows/test-external.yaml
@@ -11,24 +11,18 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
- ruby: ['2.7', '3.0', '3.1']
+ ruby: ['3.0', '3.1', '3.2', '3.3']
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v3
- - uses: ruby/setup-ruby@v1
+ - uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: ${{matrix.ruby}}
bundler-cache: true
-
- - name: Installing packages (ubuntu)
- if: matrix.os == 'ubuntu-latest'
- run: sudo apt-get install libfcgi-dev libmemcached-dev
-
- - name: Installing packages (macos)
- if: matrix.os == 'macos-latest'
- run: brew install fcgi libmemcached
+ apt-get: _update_ libfcgi-dev libmemcached-dev
+ brew: fcgi libmemcached
- run: bundle exec bake test:external
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index a224f6771..697b3cd27 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -10,15 +10,16 @@ jobs:
strategy:
fail-fast: false
matrix:
- os:
+ os:
- ubuntu-latest
- ruby:
+ ruby:
- '2.4'
- '2.5'
- '2.6'
- '2.7'
- '3.0'
- '3.1'
+ - '3.2'
- jruby
- truffleruby-head
include:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c9a133fd..d3d7fa370 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,72 @@
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/).
+## Unreleased
+
+## [3.0.10] - 2024-03-21
+
+- Backport #2104 to 3-0-stable: Return empty when parsing a multi-part POST with only one end delimiter. ([#2164](https://github.com/rack/rack/pull/2164), [@JoeDupuis](https://github.com/JoeDupuis))
+
+## [3.0.9] - 2024-01-31
+
+- Fix incorrect content-length header that was emitted when `Rack::Response#write` was used in some situations. ([#2150](https://github.com/rack/rack/pull/2150), [@mattbrictson](https://github.com/mattbrictson))
+
+## [3.0.8] - 2023-06-14
+
+- Fix some unused variable verbose warnings. ([#2084](https://github.com/rack/rack/pull/2084), [@jeremyevans], [@skipkayhil](https://github.com/skipkayhil))
+
+## [3.0.7] - 2023-03-16
+
+- Make query parameters without `=` have `nil` values. ([#2059](https://github.com/rack/rack/pull/2059), [@jeremyevans])
+
+## [3.0.6.1] - 2023-03-13
+
+- [CVE-2023-27539] Avoid ReDoS in header parsing
+
+## [3.0.6] - 2023-03-13
+
+- Add `QueryParser#missing_value` for handling missing values + tests. ([#2052](https://github.com/rack/rack/pull/2052), [@ioquatix])
+
+## [3.0.5] - 2023-03-13
+
+- Split form/query parsing into two steps. ([#2038](https://github.com/rack/rack/pull/2038), [@matthewd](https://github.com/matthewd))
+
+## [3.0.4.1] - 2023-03-02
+
+- [CVE-2023-27530] Introduce multipart_total_part_limit to limit total parts
+
+## [3.0.4.1] - 2023-01-17
+
+- [CVE-2022-44571] Fix ReDoS vulnerability in multipart parser
+- [CVE-2022-44570] Fix ReDoS in Rack::Utils.get_byte_ranges
+- [CVE-2022-44572] Forbid control characters in attributes (also ReDoS)
+
+## [3.0.4] - 2023-01-17
+
+- `Rack::Request#POST` should consistently raise errors. Cache errors that occur when invoking `Rack::Request#POST` so they can be raised again later. ([#2010](https://github.com/rack/rack/pull/2010), [@ioquatix])
+- Fix `Rack::Lint` error message for `HTTP_CONTENT_TYPE` and `HTTP_CONTENT_LENGTH`. ([#2007](https://github.com/rack/rack/pull/2007), [@byroot](https://github.com/byroot))
+- Extend `Rack::MethodOverride` to handle `QueryParser::ParamsTooDeepError` error. ([#2006](https://github.com/rack/rack/pull/2006), [@byroot](https://github.com/byroot))
+
+## [3.0.3] - 2022-12-27
+
+### Fixed
+
+- `Rack::URLMap` uses non-deprecated form of `Regexp.new`. ([#1998](https://github.com/rack/rack/pull/1998), [@weizheheng](https://github.com/weizheheng))
+
+## [3.0.2] - 2022-12-05
+
+### Fixed
+
+- `Utils.build_nested_query` URL-encodes nested field names including the square brackets.
+- Allow `Rack::Response` to pass through streaming bodies. ([#1993](https://github.com/rack/rack/pull/1993), [@ioquatix])
+
+## [3.0.1] - 2022-11-18
+
+### Fixed
+
+- `MethodOverride` does not look for an override if a request does not include form/parseable data.
+- `Rack::Lint::Wrapper` correctly handles `respond_to?` with `to_ary`, `each`, `call` and `to_path`, forwarding to the body. ([#1981](https://github.com/rack/rack/pull/1981), [@ioquatix])
+
## [3.0.0] - 2022-09-06
- No changes
@@ -34,7 +100,7 @@ All notable changes to this project will be documented in this file. For info on
- `SERVER_PROTOCOL` is now a required environment key, matching the HTTP protocol used in the request.
- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional.
- `rack.hijack_io` has been removed completely.
-- `rack.response_finished` is an optional environment key which contains an array of callable objects that must accept `#call(env, status, headers, error)` and are invoked after the response is finished (either successfully or unsucessfully).
+- `rack.response_finished` is an optional environment key which contains an array of callable objects that must accept `#call(env, status, headers, error)` and are invoked after the response is finished (either successfully or unsuccessfully).
- It is okay to call `#close` on `rack.input` to indicate that you no longer need or care about the input.
- The stream argument supplied to the streaming body and hijack must support `#<<` for writing output.
diff --git a/README.md b/README.md
index 3f060ca30..2d26ab44f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# 
-> **_NOTE:_** Rack v3.0.0.beta1 was recently released. Please check the [Upgrade
+> **_NOTE:_** Rack v3.0.0 was recently released. Please check the [Upgrade
> Guide](UPGRADE-GUIDE.md) for more details about migrating your existing
> servers, middlewares and applications. For detailed information on specific
> changes, check the [Change Log](CHANGELOG.md).
@@ -23,7 +23,7 @@ by a [supported web framework](#supported-web-frameworks):
$ gem install rack --pre
# or, add it to your current application gemfile:
-$ bundle add rack --version 3.0.0.beta1
+$ bundle add rack --version 3.0.0
```
If you need features from `Rack::Session` or `bin/rackup` please add those gems separately.
@@ -186,19 +186,35 @@ but this query string would not be allowed:
Limiting the depth prevents a possible stack overflow when parsing parameters.
-### `multipart_part_limit`
+### `multipart_file_limit`
```ruby
-Rack::Utils.multipart_part_limit = 128 # default
+Rack::Utils.multipart_file_limit = 128 # default
```
-The maximum number of parts a request can contain. Accepting too many parts can
-lead to the server running out of file handles.
+The maximum number of parts with a filename a request can contain. Accepting
+too many parts can lead to the server running out of file handles.
The default is 128, which means that a single request can't upload more than 128
files at once. Set to 0 for no limit.
-Can also be set via the `RACK_MULTIPART_PART_LIMIT` environment variable.
+Can also be set via the `RACK_MULTIPART_FILE_LIMIT` environment variable.
+
+(This is also aliased as `multipart_part_limit` and `RACK_MULTIPART_PART_LIMIT` for compatibility)
+
+
+### `multipart_total_part_limit`
+
+The maximum total number of parts a request can contain of any type, including
+both file and non-file form fields.
+
+The default is 4096, which means that a single request can't contain more than
+4096 parts.
+
+Set to 0 for no limit.
+
+Can also be set via the `RACK_MULTIPART_TOTAL_PART_LIMIT` environment variable.
+
## Changelog
diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md
index f845e5de3..290fac24d 100644
--- a/UPGRADE-GUIDE.md
+++ b/UPGRADE-GUIDE.md
@@ -150,7 +150,7 @@ dropped entirely.
### `rack.input` is no longer required to be rewindable
-Previosuly, `rack.input` was required to be rewindable, i.e. `io.seek(0)` but
+Previously, `rack.input` was required to be rewindable, i.e. `io.seek(0)` but
this was only generally possible with a file based backing, which prevented
efficient streaming of request bodies. Now, `rack.input` is not required to be
rewindable.
@@ -308,11 +308,11 @@ def call(env)
headers[ETAG_STRING] = %(W/"#{digest}") if digest
end
- return [status, headers, body]
+ return [status, headers, body]
end
```
-### Middleware should not directly modify the response body
+### Middleware should not directly modify the response body
Be aware that the response body might not respond to `#each` and you must now
check if the body responds to `#each` or not to determine if it is an enumerable
diff --git a/lib/rack.rb b/lib/rack.rb
index 5b87ea1bc..b37c00cde 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -41,6 +41,7 @@ module Rack
autoload :MethodOverride, "rack/method_override"
autoload :Mime, "rack/mime"
autoload :NullLogger, "rack/null_logger"
+ autoload :QueryParser, "rack/query_parser"
autoload :Recursive, "rack/recursive"
autoload :Reloader, "rack/reloader"
autoload :RewindableInput, "rack/rewindable_input"
diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb
index d5b4ea16d..019efde75 100644
--- a/lib/rack/auth/basic.rb
+++ b/lib/rack/auth/basic.rb
@@ -10,8 +10,6 @@ module Auth
#
# Initialize with the Rack application that you want protecting,
# and a block that checks if a username and password pair are valid.
- #
- # See also: example/protectedlobster.rb
class Basic < AbstractHandler
diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb
index 6d40b534a..0b9c3d24a 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -10,26 +10,23 @@ module Rack
#
# Example:
#
- # require 'rack/lobster'
- # app = Rack::Builder.new do
- # use Rack::CommonLogger
- # use Rack::ShowExceptions
- # map "/lobster" do
- # use Rack::Lint
- # run Rack::Lobster.new
- # end
- # end
+ # app = Rack::Builder.new do
+ # use Rack::CommonLogger
+ # map "/ok" do
+ # run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] }
+ # end
+ # end
#
- # run app
+ # run app
#
# Or
#
- # app = Rack::Builder.app do
- # use Rack::CommonLogger
- # run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
- # end
+ # app = Rack::Builder.app do
+ # use Rack::CommonLogger
+ # run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] }
+ # end
#
- # run app
+ # run app
#
# +use+ adds middleware to the stack, +run+ dispatches to an application.
# You can use +map+ to construct a Rack::URLMap in a convenient way.
@@ -180,15 +177,6 @@ def use(middleware, *args, &block)
#
# run Heartbeat.new
#
- # It could also be a module:
- #
- # module HelloWorld
- # def call(env)
- # [200, { "content-type" => "text/plain" }, ["Hello World"]]
- # end
- # end
- #
- # run HelloWorld
def run(app = nil, &block)
raise ArgumentError, "Both app and block given!" if app && block_given?
@@ -213,21 +201,35 @@ def warmup(prc = nil, &block)
# the Rack application specified by run inside the block. Other requests will be sent to the
# default application specified by run outside the block.
#
- # Rack::Builder.app do
+ # class App
+ # def call(env)
+ # [200, {'content-type' => 'text/plain'}, ["Hello World"]]
+ # end
+ # end
+ #
+ # class Heartbeat
+ # def call(env)
+ # [200, { "content-type" => "text/plain" }, ["OK"]]
+ # end
+ # end
+ #
+ # app = Rack::Builder.app do
# map '/heartbeat' do
- # run Heartbeat
+ # run Heartbeat.new
# end
- # run App
+ # run App.new
# end
#
+ # run app
+ #
# The +use+ method can also be used inside the block to specify middleware to run under a specific path:
#
- # Rack::Builder.app do
+ # app = Rack::Builder.app do
# map '/heartbeat' do
# use Middleware
- # run Heartbeat
+ # run Heartbeat.new
# end
- # run App
+ # run App.new
# end
#
# This example includes a piece of middleware which will run before +/heartbeat+ requests hit +Heartbeat+.
diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb
index d99b63673..13365935b 100644
--- a/lib/rack/constants.rb
+++ b/lib/rack/constants.rb
@@ -14,7 +14,7 @@ module Rack
SERVER_NAME = 'SERVER_NAME'
SERVER_PORT = 'SERVER_PORT'
HTTP_COOKIE = 'HTTP_COOKIE'
-
+
# Response Header Keys
CACHE_CONTROL = 'cache-control'
CONTENT_LENGTH = 'content-length'
@@ -55,6 +55,7 @@ module Rack
RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'
+ RACK_REQUEST_FORM_ERROR = 'rack.request.form_error'
RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'
RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'
RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
diff --git a/lib/rack/headers.rb b/lib/rack/headers.rb
index 153c1b223..ae1a89d12 100644
--- a/lib/rack/headers.rb
+++ b/lib/rack/headers.rb
@@ -31,7 +31,7 @@ def []=(key, value)
super(key.downcase.freeze, value)
end
alias store []=
-
+
def assoc(key)
super(downcase_key(key))
end
@@ -43,7 +43,7 @@ def compare_by_identity
def delete(key)
super(downcase_key(key))
end
-
+
def dig(key, *a)
super(downcase_key(key), *a)
end
@@ -52,7 +52,7 @@ def fetch(key, *default, &block)
key = downcase_key(key)
super
end
-
+
def fetch_values(*a)
super(*a.map!{|key| downcase_key(key)})
end
@@ -63,34 +63,34 @@ def has_key?(key)
alias include? has_key?
alias key? has_key?
alias member? has_key?
-
+
def invert
hash = self.class.new
each{|key, value| hash[value] = key}
hash
end
-
+
def merge(hash, &block)
dup.merge!(hash, &block)
end
-
+
def reject(&block)
hash = dup
hash.reject!(&block)
hash
end
-
+
def replace(hash)
clear
update(hash)
end
-
+
def select(&block)
hash = dup
hash.select!(&block)
hash
end
-
+
def to_proc
lambda{|x| self[x]}
end
@@ -100,10 +100,10 @@ def transform_values(&block)
end
def update(hash, &block)
- hash.each do |key, value|
+ hash.each do |key, value|
self[key] = if block_given? && include?(key)
block.call(key, self[key], value)
- else
+ else
value
end
end
@@ -114,7 +114,7 @@ def update(hash, &block)
def values_at(*keys)
keys.map{|key| self[key]}
end
-
+
# :nocov:
if RUBY_VERSION >= '2.5'
# :nocov:
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
index 42878879a..ee3ec7161 100755
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -303,7 +303,7 @@ def check_environment(env)
## (use the versions without HTTP_).
%w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
if env.include? header
- raise LintError, "env contains #{header}, must use #{header[5, -1]}"
+ raise LintError, "env contains #{header}, must use #{header[5..-1]}"
end
}
@@ -629,7 +629,7 @@ def check_headers(headers)
unless headers.kind_of?(Hash)
raise LintError, "headers object should be a hash, but isn't (got #{headers.class} as headers)"
end
-
+
if headers.frozen?
raise LintError, "headers object should not be frozen, but is"
end
@@ -817,8 +817,14 @@ def each
verify_to_path
end
+ BODY_METHODS = {to_ary: true, each: true, call: true, to_path: true}
+
+ def to_path
+ @body.to_path
+ end
+
def respond_to?(name, *)
- if name == :to_ary
+ if BODY_METHODS.key?(name)
@body.respond_to?(name)
else
super
@@ -883,7 +889,7 @@ class StreamWrapper
def initialize(stream)
@stream = stream
-
+
REQUIRED_METHODS.each do |method_name|
raise LintError, "Stream must respond to #{method_name}" unless stream.respond_to?(method_name)
end
diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb
index ff3145deb..7fc1e39db 100644
--- a/lib/rack/media_type.rb
+++ b/lib/rack/media_type.rb
@@ -4,7 +4,7 @@ module Rack
# Rack::MediaType parse media type and parameters out of content_type string
class MediaType
- SPLIT_PATTERN = %r{\s*[;,]\s*}
+ SPLIT_PATTERN = /[;,]/
class << self
# The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -15,7 +15,11 @@ class << self
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
def type(content_type)
return nil unless content_type
- content_type.split(SPLIT_PATTERN, 2).first.tap(&:downcase!)
+ if type = content_type.split(SPLIT_PATTERN, 2).first
+ type.rstrip!
+ type.downcase!
+ type
+ end
end
# The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -27,9 +31,10 @@ def params(content_type)
return {} if content_type.nil?
content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
+ s.strip!
k, v = s.split('=', 2)
-
- hsh[k.tap(&:downcase!)] = strip_doublequotes(v)
+ k.downcase!
+ hsh[k] = strip_doublequotes(v)
end
end
diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb
index cf7fcf75d..6125b1916 100644
--- a/lib/rack/method_override.rb
+++ b/lib/rack/method_override.rb
@@ -46,8 +46,8 @@ def allowed_methods
end
def method_override_param(req)
- req.POST[METHOD_OVERRIDE_PARAM_KEY]
- rescue Utils::InvalidParameterError, Utils::ParameterTypeError
+ req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
+ rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
rescue EOFError
req.get_header(RACK_ERRORS).puts "Bad request content body"
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index 480badaf2..345cc258b 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -8,6 +8,8 @@ module Rack
module Multipart
class MultipartPartLimitError < Errno::EMFILE; end
+ class MultipartTotalPartLimitError < StandardError; end
+
# Use specific error class when parsing multipart request
# that ends early.
class EmptyContentError < ::EOFError; end
@@ -23,10 +25,10 @@ class Error < StandardError; end
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
- MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
# Updated definitions from RFC 2231
- ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
+ ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
SECTION = /\*[0-9]+/
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
@@ -166,7 +168,7 @@ def on_mime_head(mime_index, head, filename, content_type, name)
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
- check_open_files
+ check_part_limits
end
def on_mime_body(mime_index, content)
@@ -178,13 +180,23 @@ def on_mime_finish(mime_index)
private
- def check_open_files
- if Utils.multipart_part_limit > 0
- if @open_files >= Utils.multipart_part_limit
+ def check_part_limits
+ file_limit = Utils.multipart_file_limit
+ part_limit = Utils.multipart_total_part_limit
+
+ if file_limit && file_limit > 0
+ if @open_files >= file_limit
@mime_parts.each(&:close)
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
end
end
+
+ if part_limit && part_limit > 0
+ if @mime_parts.size >= part_limit
+ @mime_parts.each(&:close)
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
+ end
+ end
end
end
@@ -201,6 +213,7 @@ def initialize(boundary, tempfile, bufsize, query_parser)
@sbuf = StringScanner.new("".dup)
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
@head_regex = /(.*?#{EOL})#{EOL}/m
end
@@ -267,7 +280,14 @@ def handle_fast_forward
@state = :MIME_HEAD
return
when :END_BOUNDARY
- # invalid multipart upload, but retry for opening boundary
+ # invalid multipart upload
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
+ # stop parsing a buffer if a buffer is only an end boundary.
+ @state = :DONE
+ return
+ end
+
+ # retry for opening boundary
else
# no boundary found, keep reading data
return :want_read
diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb
index 3077fb1fd..9e8434cf3 100644
--- a/lib/rack/query_parser.rb
+++ b/lib/rack/query_parser.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'uri'
+
module Rack
class QueryParser
DEFAULT_SEP = /[&] */n
@@ -128,8 +130,6 @@ def normalize_params(params, name, v, _depth=nil)
return if k.empty?
- v ||= String.new
-
if after == ''
if k == '[]' && depth != 0
return [v]
@@ -190,8 +190,8 @@ def params_hash_has_key?(hash, key)
true
end
- def unescape(s)
- Utils.unescape(s)
+ def unescape(string, encoding = Encoding::UTF_8)
+ URI.decode_www_form_component(string, encoding)
end
class Params
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 452140149..99a33aa1e 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -44,7 +44,7 @@ class << self
@x_forwarded_proto_priority = [:proto, :scheme]
valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/
-
+
trusted_proxies = Regexp.union(
/\A127#{valid_ipv4_octet}{3}\z/, # localhost IPv4 range 127.x.x.x, per RFC-3330
/\A::1\z/, # localhost IPv6 ::1
@@ -54,7 +54,7 @@ class << self
/\A192\.168#{valid_ipv4_octet}{2}\z/, # private IPv4 range 192.168.x.x
/\Alocalhost\z|\Aunix(\z|:)/i, # localhost hostname, and unix domain sockets
)
-
+
self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) }
ALLOWED_SCHEMES = %w(https http wss ws).freeze
@@ -496,26 +496,46 @@ def GET
# This method support both application/x-www-form-urlencoded and
# multipart/form-data.
def POST
- if get_header(RACK_INPUT).nil?
- raise "Missing rack.input"
- elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
- get_header(RACK_REQUEST_FORM_HASH)
- elsif form_data? || parseable_data?
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
- form_vars = get_header(RACK_INPUT).read
-
- # Fix for Safari Ajax postings that always append \0
- # form_vars.sub!(/\0\z/, '') # performance replacement:
- form_vars.slice!(-1) if form_vars.end_with?("\0")
-
- set_header RACK_REQUEST_FORM_VARS, form_vars
- set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
+ if error = get_header(RACK_REQUEST_FORM_ERROR)
+ raise error.class, error.message, cause: error.cause
+ end
+
+ begin
+ rack_input = get_header(RACK_INPUT)
+
+ # If the form hash was already memoized:
+ if form_hash = get_header(RACK_REQUEST_FORM_HASH)
+ # And it was memoized from the same input:
+ if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
+ return form_hash
+ end
end
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
- get_header RACK_REQUEST_FORM_HASH
- else
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
- set_header(RACK_REQUEST_FORM_HASH, {})
+
+ # Otherwise, figure out how to parse the input:
+ if rack_input.nil?
+ set_header RACK_REQUEST_FORM_INPUT, nil
+ set_header(RACK_REQUEST_FORM_HASH, {})
+ elsif form_data? || parseable_data?
+ unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
+ form_vars = get_header(RACK_INPUT).read
+
+ # Fix for Safari Ajax postings that always append \0
+ # form_vars.sub!(/\0\z/, '') # performance replacement:
+ form_vars.slice!(-1) if form_vars.end_with?("\0")
+
+ set_header RACK_REQUEST_FORM_VARS, form_vars
+ set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
+ end
+
+ set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
+ get_header RACK_REQUEST_FORM_HASH
+ else
+ set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
+ set_header(RACK_REQUEST_FORM_HASH, {})
+ end
+ rescue => error
+ set_header(RACK_REQUEST_FORM_ERROR, error)
+ raise
end
end
@@ -625,8 +645,8 @@ def wrap_ipv6(host)
end
def parse_http_accept_header(header)
- header.to_s.split(/\s*,\s*/).map do |part|
- attribute, parameters = part.split(/\s*;\s*/, 2)
+ header.to_s.split(",").each(&:strip!).map do |part|
+ attribute, parameters = part.split(";", 2).each(&:strip!)
quality = 1.0
if parameters and /\Aq=([\d.]+)/ =~ parameters
quality = $1.to_f
diff --git a/lib/rack/response.rb b/lib/rack/response.rb
index 2b7057a2f..b9b02c272 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -43,7 +43,7 @@ def header
#
# If the +body+ is +nil+, construct an empty response object with internal
# buffering.
- #
+ #
# If the +body+ responds to +to_str+, assume it's a string-like object and
# construct a buffered response object containing using that string as the
# initial contents of the buffer.
@@ -102,11 +102,16 @@ def chunked?
CHUNKED == get_header(TRANSFER_ENCODING)
end
+ def no_entity_body?
+ # The response body is an enumerable body and it is not allowed to have an entity body.
+ @body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
+ end
+
# Generate a response array consistent with the requirements of the SPEC.
# @return [Array] a 3-tuple suitable of `[status, headers, body]`
# which is suitable to be returned from the middleware `#call(env)` method.
def finish(&block)
- if STATUS_WITH_NO_ENTITY_BODY[@status]
+ if no_entity_body?
delete_header CONTENT_TYPE
delete_header CONTENT_LENGTH
close
@@ -323,6 +328,8 @@ def buffered_body!
@body.each do |part|
@length += part.to_s.bytesize
end
+
+ @buffered = true
elsif @body.respond_to?(:each)
# Turn the user supplied body into a buffered array:
body = @body
@@ -333,7 +340,7 @@ def buffered_body!
end
body.close if body.respond_to?(:close)
-
+
@buffered = true
else
@buffered = false
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index 45f0a8c36..9c6e0c42f 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -111,7 +111,7 @@ def initialize(app, variation = nil, mappings = [])
end
def call(env)
- status, headers, body = response = @app.call(env)
+ _, headers, body = response = @app.call(env)
if body.respond_to?(:to_path)
case type = variation(env)
diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb
index afb97eea4..99c4d8236 100644
--- a/lib/rack/urlmap.rb
+++ b/lib/rack/urlmap.rb
@@ -37,7 +37,7 @@ def remap(map)
end
location = location.chomp('/')
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
[host, location, match, app]
}.sort_by do |(host, location, _, _)|
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 648b70590..f91838b37 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -58,13 +58,24 @@ def unescape(s, encoding = Encoding::UTF_8)
end
class << self
- attr_accessor :multipart_part_limit
+ attr_accessor :multipart_total_part_limit
+
+ attr_accessor :multipart_file_limit
+
+ # multipart_part_limit is the original name of multipart_file_limit, but
+ # the limit only counts parts with filenames.
+ alias multipart_part_limit multipart_file_limit
+ alias multipart_part_limit= multipart_file_limit=
end
- # The maximum number of parts a request can contain. Accepting too many part
- # can lead to the server running out of file handles.
+ # The maximum number of file parts a request can contain. Accepting too
+ # many parts can lead to the server running out of file handles.
# Set to `0` for no limit.
- self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
+ self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
+
+ # The maximum total number of parts a request can contain. Accepting too
+ # many can lead to excessive memory use and parsing time.
+ self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
def self.param_depth_limit
default_query_parser.param_depth_limit
@@ -121,19 +132,19 @@ def build_nested_query(value, prefix = nil)
}.join("&")
when Hash
value.map { |k, v|
- build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
}.delete_if(&:empty?).join('&')
when nil
- prefix
+ escape(prefix)
else
raise ArgumentError, "value must be a Hash" if prefix.nil?
- "#{prefix}=#{escape(value)}"
+ "#{escape(prefix)}=#{escape(value)}"
end
end
def q_values(q_value_header)
- q_value_header.to_s.split(/\s*,\s*/).map do |part|
- value, parameters = part.split(/\s*;\s*/, 2)
+ q_value_header.to_s.split(',').map do |part|
+ value, parameters = part.split(';', 2).map(&:strip)
quality = 1.0
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
quality = md[1].to_f
@@ -146,9 +157,10 @@ def forwarded_values(forwarded_header)
return nil unless forwarded_header
forwarded_header = forwarded_header.to_s.gsub("\n", ";")
- forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
- field.split(/\s*,\s*/).each do |pair|
- return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
+ forwarded_header.split(';').each_with_object({}) do |field, values|
+ field.split(',').each do |pair|
+ pair = pair.split('=').map(&:strip).join('=')
+ return nil unless pair =~ /\A(by|for|host|proto)="?([^"]+)"?\Z/i
(values[$1.downcase.to_sym] ||= []) << $2
end
end
@@ -278,7 +290,7 @@ def parse_cookies(env)
# If the cookie +value+ is an instance of +Hash+, it considers the following
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
- # details about the interpretation of these fields, consult
+ # details about the interpretation of these fields, consult
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
#
# An extra cookie attribute +escape_key+ can be provided to control whether
@@ -426,17 +438,18 @@ def get_byte_ranges(http_range, size)
return nil unless http_range && http_range =~ /bytes=([^;]+)/
ranges = []
$1.split(/,\s*/).each do |range_spec|
- return nil unless range_spec =~ /(\d*)-(\d*)/
- r0, r1 = $1, $2
- if r0.empty?
- return nil if r1.empty?
+ return nil unless range_spec.include?('-')
+ range = range_spec.split('-')
+ r0, r1 = range[0], range[1]
+ if r0.nil? || r0.empty?
+ return nil if r1.nil?
# suffix-byte-range-spec, represents trailing suffix of file
r0 = size - r1.to_i
r0 = 0 if r0 < 0
r1 = size - 1
else
r0 = r0.to_i
- if r1.empty?
+ if r1.nil?
r1 = size - 1
else
r1 = r1.to_i
@@ -446,6 +459,9 @@ def get_byte_ranges(http_range, size)
end
ranges << (r0..r1) if r0 <= r1
end
+
+ return [] if ranges.map(&:size).sum > size
+
ranges
end
diff --git a/lib/rack/version.rb b/lib/rack/version.rb
index 782eb9811..884d21e2e 100644
--- a/lib/rack/version.rb
+++ b/lib/rack/version.rb
@@ -25,7 +25,7 @@ def self.version
VERSION
end
- RELEASE = "3.0.0"
+ RELEASE = "3.0.10"
# Return the Rack release as a dotted string.
def self.release
diff --git a/test/cgi/rackup_stub.rb b/test/cgi/rackup_stub.rb
old mode 100755
new mode 100644
diff --git a/test/cgi/sample_rackup.ru b/test/cgi/sample_rackup.ru
old mode 100755
new mode 100644
diff --git a/test/cgi/test b/test/cgi/test
old mode 100755
new mode 100644
diff --git a/test/cgi/test.ru b/test/cgi/test.ru
old mode 100755
new mode 100644
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index 6ca284776..2cc7732c7 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -277,7 +277,7 @@ def config_file(name)
it "strips leading unicode byte order mark when present" do
enc = Encoding.default_external
begin
- verbose, $VERBOSE = $VERBOSE, nil
+ verbose, $VERBOSE = $VERBOSE, nil
Encoding.default_external = 'UTF-8'
app, _ = Rack::Builder.parse_file config_file('bom.ru')
Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index 87c3c4f0b..9d6e81f50 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -275,7 +275,7 @@ class << app_body; def each; yield('foo'); yield('bar'); end; end
'PATH_INFO' => '/foo/bar'
}
}
-
+
app_body3 = [app_body]
closed = false
app_body3.define_singleton_method(:close){closed = true}
diff --git a/test/spec_headers.rb b/test/spec_headers.rb
old mode 100755
new mode 100644
index dcd6296bf..f22680b77
--- a/test/spec_headers.rb
+++ b/test/spec_headers.rb
@@ -18,7 +18,7 @@ def test_public_interface
assert_empty(headers_methods - hash_methods)
assert_empty(hash_methods - headers_methods)
end
-
+
def test_class_aref
assert_equal Hash[], Rack::Headers[]
assert_equal Hash['a'=>'2'], Rack::Headers['A'=>'2']
@@ -27,7 +27,7 @@ def test_class_aref
assert_raises(ArgumentError){Rack::Headers['A']}
assert_raises(ArgumentError){Rack::Headers['A',2,'B']}
end
-
+
def test_default_values
h, ch = Hash.new, Rack::Headers.new
assert_equal h, ch
@@ -48,29 +48,29 @@ def test_default_values
assert_equal '3', Rack::Headers.new('3').default
assert_nil Rack::Headers.new('3').default_proc
assert_equal '3', Rack::Headers.new('3')['1']
-
+
@fh.default = '4'
assert_equal '4', @fh.default
assert_nil @fh.default_proc
assert_equal '4', @fh['55']
-
+
h = Rack::Headers.new('5')
assert_equal '5', h.default
assert_nil h.default_proc
assert_equal '5', h['55']
-
+
h = Rack::Headers.new{|hash, key| '1234'}
assert_nil h.default
refute_equal nil, h.default_proc
assert_equal '1234', h['55']
-
+
h = Rack::Headers.new{|hash, key| hash[key] = '1234'; nil}
assert_nil h.default
refute_equal nil, h.default_proc
assert_nil h['Ac']
assert_equal '1234', h['aC']
end
-
+
def test_store_and_retrieve
assert_nil @h['a']
@h['A'] = '2'
@@ -88,14 +88,14 @@ def test_store_and_retrieve
assert_equal '8', @h['c']
assert_equal '8', @h['C']
end
-
+
def test_clear
assert_equal 3, @fh.length
@fh.clear
assert_equal Hash[], @fh
assert_equal 0, @fh.length
end
-
+
def test_delete
assert_equal 3, @fh.length
assert_equal '1', @fh.delete('aB')
@@ -103,7 +103,7 @@ def test_delete
assert_nil @fh.delete('Ab')
assert_equal 2, @fh.length
end
-
+
def test_delete_if_and_reject
assert_equal 3, @fh.length
hash = @fh.reject{|key, value| key == 'ab' || key == 'cd'}
@@ -135,49 +135,49 @@ def @h.foo; 1; end
assert_equal '2', h2['a']
assert_equal '3', h3['b']
end
-
+
def test_each
i = 0
@h.each{i+=1}
assert_equal 0, i
items = [['ab','1'], ['cd','2'], ['3','4']]
- @fh.each do |k,v|
+ @fh.each do |k,v|
assert items.include?([k,v])
items -= [[k,v]]
end
assert_equal [], items
end
-
+
def test_each_key
i = 0
@h.each{i+=1}
assert_equal 0, i
keys = ['ab', 'cd', '3']
- @fh.each_key do |k|
+ @fh.each_key do |k|
assert keys.include?(k)
assert k.frozen?
keys -= [k]
end
assert_equal [], keys
end
-
+
def test_each_value
i = 0
@h.each{i+=1}
assert_equal 0, i
values = ['1', '2', '4']
- @fh.each_value do |v|
+ @fh.each_value do |v|
assert values.include?(v)
values -= [v]
end
assert_equal [], values
end
-
+
def test_empty
assert @h.empty?
assert !@fh.empty?
end
-
+
def test_fetch
assert_raises(ArgumentError){@h.fetch(1,2,3)}
assert_raises(ArgumentError){@h.fetch(1,2,3){4}}
@@ -194,7 +194,7 @@ def test_fetch
assert_equal '4', @fh.fetch("3"){|k| k*3}
assert_raises(IndexError){Rack::Headers.new{34}.fetch(1)}
end
-
+
def test_has_key
%i'include? has_key? key? member?'.each do |meth|
assert !@h.send(meth,1)
@@ -207,7 +207,7 @@ def test_has_key
assert !@fh.send(meth,1)
end
end
-
+
def test_has_value
%i'value? has_value?'.each do |meth|
assert !@h.send(meth,'1')
@@ -217,14 +217,14 @@ def test_has_value
assert !@fh.send(meth,'3')
end
end
-
+
def test_inspect
%i'inspect to_s'.each do |meth|
assert_equal '{}', @h.send(meth)
assert_equal '{"ab"=>"1", "cd"=>"2", "3"=>"4"}', @fh.send(meth)
end
end
-
+
def test_invert
assert_kind_of(Rack::Headers, @h.invert)
assert_equal({}, @h.invert)
@@ -232,19 +232,19 @@ def test_invert
assert_equal({'cd'=>'ab'}, Rack::Headers['AB'=>'CD'].invert)
assert_equal({'cd'=>'xy'}, Rack::Headers['AB'=>'Cd', 'xY'=>'cD'].invert)
end
-
+
def test_keys
assert_equal [], @h.keys
assert_equal %w'ab cd 3', @fh.keys
end
-
+
def test_length
%i'length size'.each do |meth|
assert_equal 0, @h.send(meth)
assert_equal 3, @fh.send(meth)
end
end
-
+
def test_merge_and_update
assert_equal @h, @h.merge({})
assert_equal @fh, @fh.merge({})
@@ -263,7 +263,7 @@ def test_merge_and_update
assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh
end
-
+
def test_replace
h = @h.dup
fh = @fh.dup
@@ -278,7 +278,7 @@ def test_replace
assert_equal @h, fh.replace({})
assert_equal @fh, h.replace('AB'=>'1', 'cd'=>'2', '3'=>'4')
end
-
+
def test_select
assert_equal({}, @h.select{true})
assert_equal({}, @h.select{false})
@@ -287,7 +287,7 @@ def test_select
assert_equal({'cd' => '2'}, @fh.select{|k,v| k.start_with?('c')})
assert_equal({'3' => '4'}, @fh.select{|k,v| v == '4'})
end
-
+
def test_shift
assert_nil @h.shift
array = @fh.to_a
@@ -307,29 +307,29 @@ def test_shift
assert_equal [], array
assert_equal 0, i
end
-
+
def test_sort
assert_equal [], @h.sort
assert_equal [], @h.sort{|a,b| a.to_s<=>b.to_s}
assert_equal [['ab', '1'], ['cd', '4'], ['ef', '2']], Rack::Headers['CD','4','AB','1','EF','2'].sort
assert_equal [['3', '4'], ['ab', '1'], ['cd', '2']], @fh.sort{|(ak,av),(bk,bv)| ak.to_s<=>bk.to_s}
end
-
+
def test_to_a
assert_equal [], @h.to_a
assert_equal [['ab', '1'], ['cd', '2'], ['3', '4']], @fh.to_a
end
-
+
def test_to_hash
assert_equal Hash[], @h.to_hash
assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_hash
end
-
+
def test_values
assert_equal [], @h.values
assert_equal ['f', 'c'], Rack::Headers['aB','f','1','c'].values
end
-
+
def test_values_at
assert_equal [], @h.values_at()
assert_equal [nil], @h.values_at(1)
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
index 054f9fded..398c7719c 100755
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -33,7 +33,6 @@ def env(*args)
lambda { Rack::Lint.new(nil).call({}.freeze) }.must_raise(Rack::Lint::LintError).
message.must_match(/env should not be frozen, but is/)
-
lambda {
e = env
e.delete("REQUEST_METHOD")
@@ -473,6 +472,17 @@ def obj.each; end
message.must_match(/content-length header was 1, but should be 0/)
end
+ it "responds to to_path" do
+ body = Object.new
+ def body.each; end
+ def body.to_path; __FILE__ end
+ app = lambda { |env| [200, {}, body] }
+
+ body = Rack::Lint.new(app).call(env({}))[2]
+ body.must_respond_to(:to_path)
+ body.to_path.must_equal __FILE__
+ end
+
it "notice body errors" do
lambda {
body = Rack::Lint.new(lambda { |env|
diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb
index d0538a70d..f3b8ad729 100644
--- a/test/spec_method_override.rb
+++ b/test/spec_method_override.rb
@@ -106,10 +106,28 @@ def app
env[Rack::RACK_ERRORS].read.must_include 'Bad request content body'
end
+ it "not modify REQUEST_METHOD for POST requests when the params are unparseable because too deep" do
+ env = Rack::MockRequest.env_for("/", method: "POST", input: ("[a]" * 36) + "=1")
+ app.call env
+
+ env["REQUEST_METHOD"].must_equal "POST"
+ end
+
it "not modify REQUEST_METHOD for POST requests when the params are unparseable" do
env = Rack::MockRequest.env_for("/", method: "POST", input: "(%bad-params%)")
app.call env
env["REQUEST_METHOD"].must_equal "POST"
end
+
+ it "not set form input when the content type is JSON" do
+ env = Rack::MockRequest.env_for("/",
+ "CONTENT_TYPE" => "application/json",
+ method: "POST",
+ input: '{"_method":"options"}')
+ app.call env
+
+ env["REQUEST_METHOD"].must_equal "POST"
+ env["rack.request.form_input"].must_be_nil
+ end
end
diff --git a/test/spec_mock_request.rb b/test/spec_mock_request.rb
index 889c8a9f1..b2a16fded 100644
--- a/test/spec_mock_request.rb
+++ b/test/spec_mock_request.rb
@@ -26,8 +26,8 @@
req.GET["status"] || 200,
"content-type" => "text/yaml"
)
- response.set_cookie("session_test", { value: "session_test", domain: ".test.com", path: "/" })
- response.set_cookie("secure_test", { value: "secure_test", domain: ".test.com", path: "/", secure: true })
+ response.set_cookie("session_test", { value: "session_test", domain: "test.com", path: "/" })
+ response.set_cookie("secure_test", { value: "secure_test", domain: "test.com", path: "/", secure: true })
response.set_cookie("persistent_test", { value: "persistent_test", max_age: 15552000, path: "/" })
response.set_cookie("persistent_with_expires_test", { value: "persistent_with_expires_test", expires: Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT"), path: "/" })
response.set_cookie("expires_and_max-age_test", { value: "expires_and_max-age_test", expires: Time.now + 15552000 * 2, max_age: 15552000, path: "/" })
@@ -192,17 +192,17 @@
env = YAML.unsafe_load(res.body)
env["REQUEST_METHOD"].must_equal "GET"
env["QUERY_STRING"].must_include "baz=2"
- env["QUERY_STRING"].must_include "foo[bar]=1"
+ env["QUERY_STRING"].must_include "foo%5Bbar%5D=1"
env["PATH_INFO"].must_equal "/foo"
env["mock.postdata"].must_equal ""
end
it "accept raw input in params for GET requests" do
- res = Rack::MockRequest.new(app).get("/foo?baz=2", params: "foo[bar]=1")
+ res = Rack::MockRequest.new(app).get("/foo?baz=2", params: "foo%5Bbar%5D=1")
env = YAML.unsafe_load(res.body)
env["REQUEST_METHOD"].must_equal "GET"
env["QUERY_STRING"].must_include "baz=2"
- env["QUERY_STRING"].must_include "foo[bar]=1"
+ env["QUERY_STRING"].must_include "foo%5Bbar%5D=1"
env["PATH_INFO"].must_equal "/foo"
env["mock.postdata"].must_equal ""
end
@@ -214,17 +214,17 @@
env["QUERY_STRING"].must_equal ""
env["PATH_INFO"].must_equal "/foo"
env["CONTENT_TYPE"].must_equal "application/x-www-form-urlencoded"
- env["mock.postdata"].must_equal "foo[bar]=1"
+ env["mock.postdata"].must_equal "foo%5Bbar%5D=1"
end
it "accept raw input in params for POST requests" do
- res = Rack::MockRequest.new(app).post("/foo", params: "foo[bar]=1")
+ res = Rack::MockRequest.new(app).post("/foo", params: "foo%5Bbar%5D=1")
env = YAML.unsafe_load(res.body)
env["REQUEST_METHOD"].must_equal "POST"
env["QUERY_STRING"].must_equal ""
env["PATH_INFO"].must_equal "/foo"
env["CONTENT_TYPE"].must_equal "application/x-www-form-urlencoded"
- env["mock.postdata"].must_equal "foo[bar]=1"
+ env["mock.postdata"].must_equal "foo%5Bbar%5D=1"
end
it "accept params and build multipart encoded params for POST requests" do
diff --git a/test/spec_mock_response.rb b/test/spec_mock_response.rb
index 2813ecd6f..83fba9287 100644
--- a/test/spec_mock_response.rb
+++ b/test/spec_mock_response.rb
@@ -26,8 +26,8 @@
req.GET["status"] || 200,
"content-type" => "text/yaml"
)
- response.set_cookie("session_test", { value: "session_test", domain: ".test.com", path: "/" })
- response.set_cookie("secure_test", { value: "secure_test", domain: ".test.com", path: "/", secure: true })
+ response.set_cookie("session_test", { value: "session_test", domain: "test.com", path: "/" })
+ response.set_cookie("secure_test", { value: "secure_test", domain: "test.com", path: "/", secure: true })
response.set_cookie("persistent_test", { value: "persistent_test", max_age: 15552000, path: "/" })
response.set_cookie("persistent_with_expires_test", { value: "persistent_with_expires_test", expires: Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT"), path: "/" })
response.set_cookie("expires_and_max-age_test", { value: "expires_and_max-age_test", expires: Time.now + 15552000 * 2, max_age: 15552000, path: "/" })
@@ -82,7 +82,7 @@
res = Rack::MockRequest.new(app).get("")
session_cookie = res.cookie("session_test")
session_cookie.value[0].must_equal "session_test"
- session_cookie.domain.must_equal ".test.com"
+ session_cookie.domain.must_equal "test.com"
session_cookie.path.must_equal "/"
session_cookie.secure.must_equal false
session_cookie.expires.must_be_nil
@@ -122,7 +122,7 @@
res = Rack::MockRequest.new(app).get("")
secure_cookie = res.cookie("secure_test")
secure_cookie.value[0].must_equal "secure_test"
- secure_cookie.domain.must_equal ".test.com"
+ secure_cookie.domain.must_equal "test.com"
secure_cookie.path.must_equal "/"
secure_cookie.secure.must_equal true
secure_cookie.expires.must_be_nil
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 43bb90b2d..c415056b6 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -691,7 +691,7 @@ def initialize(*)
end
end
- it "reach a multipart limit" do
+ it "reach a multipart file limit" do
begin
previous_limit = Rack::Utils.multipart_part_limit
Rack::Utils.multipart_part_limit = 3
@@ -703,6 +703,18 @@ def initialize(*)
end
end
+ it "reach a multipart total limit" do
+ begin
+ previous_limit = Rack::Utils.multipart_total_part_limit
+ Rack::Utils.multipart_total_part_limit = 5
+
+ env = Rack::MockRequest.env_for '/', multipart_fixture(:three_files_three_fields)
+ lambda { Rack::Multipart.parse_multipart(env) }.must_raise Rack::Multipart::MultipartTotalPartLimitError
+ ensure
+ Rack::Utils.multipart_total_part_limit = previous_limit
+ end
+ end
+
it "return nil if no UploadedFiles were used" do
data = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => "contents" }])
data.must_be_nil
diff --git a/test/spec_query_parser.rb b/test/spec_query_parser.rb
new file mode 100644
index 000000000..dbb8b14ed
--- /dev/null
+++ b/test/spec_query_parser.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+
+separate_testing do
+ require_relative '../lib/rack/query_parser'
+end
+
+describe Rack::QueryParser do
+ query_parser ||= Rack::QueryParser.make_default(8)
+
+ it "can normalize values with missing values" do
+ query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
+ query_parser.parse_nested_query("a=").must_equal({"a" => ""})
+ query_parser.parse_nested_query("a").must_equal({"a" => nil})
+ end
+end
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 750639deb..c22619063 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -572,11 +572,11 @@ def self.req(headers)
end
it "parse the query string" do
- req = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=bla"))
- req.query_string.must_equal "foo=bar&quux=bla"
- req.GET.must_equal "foo" => "bar", "quux" => "bla"
- req.POST.must_be :empty?
- req.params.must_equal "foo" => "bar", "quux" => "bla"
+ request = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=bla¬hing&empty="))
+ request.query_string.must_equal "foo=bar&quux=bla¬hing&empty="
+ request.GET.must_equal "foo" => "bar", "quux" => "bla", "nothing" => nil, "empty" => ""
+ request.POST.must_be :empty?
+ request.params.must_equal "foo" => "bar", "quux" => "bla", "nothing" => nil, "empty" => ""
end
it "not truncate query strings containing semi-colons #543 only in POST" do
@@ -696,11 +696,6 @@ def initialize(*)
message.must_equal "invalid %-encoding (a%)"
end
- it "raise if rack.input is missing" do
- req = make_request({})
- lambda { req.POST }.must_raise RuntimeError
- end
-
it "parse POST data when method is POST and no content-type given" do
req = make_request \
Rack::MockRequest.env_for("/?foo=quux",
@@ -1218,6 +1213,22 @@ def initialize(*)
req.media_type_params['weird'].must_equal 'lol"'
end
+ it "returns the same error for invalid post inputs" do
+ env = {
+ 'REQUEST_METHOD' => 'POST',
+ 'PATH_INFO' => '/foo',
+ 'rack.input' => StringIO.new('invalid=bar&invalid[foo]=bar'),
+ 'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded",
+ }
+
+ 2.times do
+ # The actual exception type here is unimportant - just that it fails.
+ assert_raises(Rack::Utils::ParameterTypeError) do
+ Rack::Request.new(env).POST
+ end
+ end
+ end
+
it "parse with junk before boundary" do
# Adapted from RFC 1867.
input = < "multipart/form-data, boundary=AaB03x",
+ "CONTENT_LENGTH" => input.size,
+ :input => input
+ )
+
+ req = make_request mr
+ req.query_string.must_equal ""
+ req.GET.must_be :empty?
+ req.POST.must_be :empty?
+ req.params.must_equal({})
+ end
+
+ it "MultipartPartLimitError when request has too many multipart file parts if limit set" do
begin
data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
data += "--AaB03x--\r"
@@ -1338,6 +1367,22 @@ def initialize(*)
end
end
+ it "MultipartPartLimitError when request has too many multipart total parts if limit set" do
+ begin
+ data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
+ data += "--AaB03x--\r"
+
+ options = {
+ "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
+ "CONTENT_LENGTH" => data.length.to_s,
+ :input => StringIO.new(data)
+ }
+
+ request = make_request Rack::MockRequest.env_for("/", options)
+ lambda { request.POST }.must_raise Rack::Multipart::MultipartTotalPartLimitError
+ end
+ end
+
it 'closes tempfiles it created in the case of too many created' do
begin
data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
@@ -1507,12 +1552,16 @@ def initialize(*)
rack_input.write(input)
rack_input.rewind
- req = make_request Rack::MockRequest.env_for("/",
- "rack.request.form_hash" => { 'foo' => 'bar' },
- "rack.request.form_input" => rack_input,
- :input => rack_input)
+ form_hash = {}
+
+ req = make_request Rack::MockRequest.env_for(
+ "/",
+ "rack.request.form_hash" => form_hash,
+ "rack.request.form_input" => rack_input,
+ :input => rack_input
+ )
- req.POST.must_equal req.env['rack.request.form_hash']
+ req.POST.must_be_same_as form_hash
end
it "conform to the Rack spec" do
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 459aaa8fb..11e64d61f 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -295,14 +295,14 @@
"foo=bar; domain=example.com.example.com",
"foo=bar; domain=example.com"
]
-
+
response.delete_cookie "foo", { domain: "example.com" }
response["set-cookie"].must_equal [
"foo=bar; domain=example.com.example.com",
"foo=bar; domain=example.com",
"foo=; domain=example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
]
-
+
response.delete_cookie "foo", { domain: "example.com.example.com" }
response["set-cookie"].must_equal [
"foo=bar; domain=example.com.example.com",
@@ -412,7 +412,7 @@ def object_with_each.each
status.must_equal 404
end
- it "correctly updates content-type when writing when not initialized with body" do
+ it "correctly updates content-length when writing when initialized without body" do
r = Rack::Response.new
r.write('foo')
r.write('bar')
@@ -423,20 +423,39 @@ def object_with_each.each
header['content-length'].must_equal '9'
end
- it "correctly updates content-type when writing when initialized with body" do
+ it "correctly updates content-length when writing when initialized with Array body" do
+ r = Rack::Response.new(["foo"])
+ r.write('bar')
+ r.write('baz')
+ _, header, body = r.finish
+ str = "".dup; body.each { |part| str << part }
+ str.must_equal "foobarbaz"
+ header['content-length'].must_equal '9'
+ end
+
+ it "correctly updates content-length when writing when initialized with String body" do
+ r = Rack::Response.new("foo")
+ r.write('bar')
+ r.write('baz')
+ _, header, body = r.finish
+ str = "".dup; body.each { |part| str << part }
+ str.must_equal "foobarbaz"
+ header['content-length'].must_equal '9'
+ end
+
+ it "correctly updates content-length when writing when initialized with object body that responds to #each" do
obj = Object.new
def obj.each
yield 'foo'
yield 'bar'
end
- ["foobar", ["foo", "bar"], obj].each do
- r = Rack::Response.new(["foo", "bar"])
- r.write('baz')
- _, header, body = r.finish
- str = "".dup; body.each { |part| str << part }
- str.must_equal "foobarbaz"
- header['content-length'].must_equal '9'
- end
+ r = Rack::Response.new(obj)
+ r.write('baz')
+ r.write('baz')
+ _, header, body = r.finish
+ str = "".dup; body.each { |part| str << part }
+ str.must_equal "foobarbazbaz"
+ header['content-length'].must_equal '12'
end
it "doesn't return invalid responses" do
@@ -642,6 +661,16 @@ def obj.each
res.body.wont_be :closed?
end
+ it "doesn't clear #body when 101 and streaming" do
+ res = Rack::Response.new
+
+ streaming_body = proc{|stream| stream.close}
+ res.body = streaming_body
+ res.status = 101
+ res.finish
+ res.body.must_equal streaming_body
+ end
+
it "flatten doesn't cause infinite loop" do
# https://github.com/rack/rack/issues/419
res = Rack::Response.new("Hello World")
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 52d295024..2165c5a6a 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -159,7 +159,7 @@ def assert_nested_query(exp, act)
it "parse nested query strings correctly" do
Rack::Utils.parse_nested_query("foo").
- must_equal "foo" => ""
+ must_equal "foo" => nil
Rack::Utils.parse_nested_query("foo=").
must_equal "foo" => ""
Rack::Utils.parse_nested_query("foo=bar").
@@ -176,7 +176,7 @@ def assert_nested_query(exp, act)
Rack::Utils.parse_nested_query("&foo=1&&bar=2").
must_equal "foo" => "1", "bar" => "2"
Rack::Utils.parse_nested_query("foo&bar=").
- must_equal "foo" => "", "bar" => ""
+ must_equal "foo" => nil, "bar" => ""
Rack::Utils.parse_nested_query("foo=bar&baz=").
must_equal "foo" => "bar", "baz" => ""
Rack::Utils.parse_nested_query("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F").
@@ -186,19 +186,19 @@ def assert_nested_query(exp, act)
must_equal "pid=1234" => "1023", "a" => "b"
Rack::Utils.parse_nested_query("foo[]").
- must_equal "foo" => [""]
+ must_equal "foo" => [nil]
Rack::Utils.parse_nested_query("foo[]=").
must_equal "foo" => [""]
Rack::Utils.parse_nested_query("foo[]=bar").
must_equal "foo" => ["bar"]
Rack::Utils.parse_nested_query("foo[]=bar&foo").
- must_equal "foo" => ""
+ must_equal "foo" => nil
Rack::Utils.parse_nested_query("foo[]=bar&foo[").
- must_equal "foo" => ["bar"], "foo[" => ""
+ must_equal "foo" => ["bar"], "foo[" => nil
Rack::Utils.parse_nested_query("foo[]=bar&foo[=baz").
must_equal "foo" => ["bar"], "foo[" => "baz"
Rack::Utils.parse_nested_query("foo[]=bar&foo[]").
- must_equal "foo" => ["bar", ""]
+ must_equal "foo" => ["bar", nil]
Rack::Utils.parse_nested_query("foo[]=bar&foo[]=").
must_equal "foo" => ["bar", ""]
@@ -210,7 +210,7 @@ def assert_nested_query(exp, act)
must_equal "foo" => ["bar"], "baz" => ["1", "2", "3"]
Rack::Utils.parse_nested_query("x[y][z]").
- must_equal "x" => { "y" => { "z" => "" } }
+ must_equal "x" => { "y" => { "z" => nil } }
Rack::Utils.parse_nested_query("x[y][z]=1").
must_equal "x" => { "y" => { "z" => "1" } }
Rack::Utils.parse_nested_query("x[y][z][]=1").
@@ -328,9 +328,9 @@ def initialize(*)
assert_nested_query("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F",
"my weird field" => "q1!2\"'w$5&7/z8)?")
- Rack::Utils.build_nested_query("foo" => [nil]).must_equal "foo[]"
- Rack::Utils.build_nested_query("foo" => [""]).must_equal "foo[]="
- Rack::Utils.build_nested_query("foo" => ["bar"]).must_equal "foo[]=bar"
+ Rack::Utils.build_nested_query("foo" => [nil]).must_equal "foo%5B%5D"
+ Rack::Utils.build_nested_query("foo" => [""]).must_equal "foo%5B%5D="
+ Rack::Utils.build_nested_query("foo" => ["bar"]).must_equal "foo%5B%5D=bar"
Rack::Utils.build_nested_query('foo' => []).must_equal ''
Rack::Utils.build_nested_query('foo' => {}).must_equal ''
Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => []).must_equal 'foo=bar'
@@ -341,35 +341,39 @@ def initialize(*)
Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => '').
must_equal 'foo=bar&baz='
Rack::Utils.build_nested_query('foo' => ['1', '2']).
- must_equal 'foo[]=1&foo[]=2'
+ must_equal 'foo%5B%5D=1&foo%5B%5D=2'
Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => ['1', '2', '3']).
- must_equal 'foo=bar&baz[]=1&baz[]=2&baz[]=3'
+ must_equal 'foo=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3'
Rack::Utils.build_nested_query('foo' => ['bar'], 'baz' => ['1', '2', '3']).
- must_equal 'foo[]=bar&baz[]=1&baz[]=2&baz[]=3'
+ must_equal 'foo%5B%5D=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3'
Rack::Utils.build_nested_query('foo' => ['bar'], 'baz' => ['1', '2', '3']).
- must_equal 'foo[]=bar&baz[]=1&baz[]=2&baz[]=3'
+ must_equal 'foo%5B%5D=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3'
Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => '1' } }).
- must_equal 'x[y][z]=1'
+ must_equal 'x%5By%5D%5Bz%5D=1'
Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => ['1'] } }).
- must_equal 'x[y][z][]=1'
+ must_equal 'x%5By%5D%5Bz%5D%5B%5D=1'
Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => ['1', '2'] } }).
- must_equal 'x[y][z][]=1&x[y][z][]=2'
+ must_equal 'x%5By%5D%5Bz%5D%5B%5D=1&x%5By%5D%5Bz%5D%5B%5D=2'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1' }] }).
- must_equal 'x[y][][z]=1'
+ must_equal 'x%5By%5D%5B%5D%5Bz%5D=1'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => ['1'] }] }).
- must_equal 'x[y][][z][]=1'
+ must_equal 'x%5By%5D%5B%5D%5Bz%5D%5B%5D=1'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => '2' }] }).
- must_equal 'x[y][][z]=1&x[y][][w]=2'
+ must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bw%5D=2'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'v' => { 'w' => '1' } }] }).
- must_equal 'x[y][][v][w]=1'
+ must_equal 'x%5By%5D%5B%5D%5Bv%5D%5Bw%5D=1'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'v' => { 'w' => '2' } }] }).
- must_equal 'x[y][][z]=1&x[y][][v][w]=2'
+ must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bv%5D%5Bw%5D=2'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1' }, { 'z' => '2' }] }).
- must_equal 'x[y][][z]=1&x[y][][z]=2'
+ must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bz%5D=2'
Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => 'a' }, { 'z' => '2', 'w' => '3' }] }).
- must_equal 'x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3'
+ must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bw%5D=a&x%5By%5D%5B%5D%5Bz%5D=2&x%5By%5D%5B%5D%5Bw%5D=3'
Rack::Utils.build_nested_query({ "foo" => ["1", ["2"]] }).
- must_equal 'foo[]=1&foo[][]=2'
+ must_equal 'foo%5B%5D=1&foo%5B%5D%5B%5D=2'
+
+ # A nested hash is the same as string keys with brackets.
+ Rack::Utils.build_nested_query('foo' => { 'bar' => 'baz' }).
+ must_equal Rack::Utils.build_nested_query('foo[bar]' => 'baz')
lambda { Rack::Utils.build_nested_query("foo=bar") }.
must_raise(ArgumentError).
@@ -712,6 +716,10 @@ def initialize(*)
end
describe Rack::Utils, "get_byte_ranges" do
+ it "returns an empty list if the sum of the ranges is too large" do
+ assert_equal [], Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-20,0-500" }, 500)
+ end
+
deprecated "pase simple byte ranges from env" do
Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 500).must_equal [(123..456)]
end