From 787bba5e3f4c374fa83dc32f74768e99d4c5d1c3 Mon Sep 17 00:00:00 2001 From: James Silberbauer Date: Wed, 9 Nov 2022 23:47:55 +0200 Subject: [PATCH 01/24] Do not look for override method if there is no form/parseable data (#1980) --- CHANGELOG.md | 6 ++++++ lib/rack/method_override.rb | 2 +- test/spec_method_override.rb | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c9a133fd..d069038d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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] + +### Fixed + +- `MethodOverride` does not look for an override if a request does not include form/parseable data. + ## [3.0.0] - 2022-09-06 - No changes diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb index cf7fcf75d..61df3fc5e 100644 --- a/lib/rack/method_override.rb +++ b/lib/rack/method_override.rb @@ -46,7 +46,7 @@ def allowed_methods end def method_override_param(req) - req.POST[METHOD_OVERRIDE_PARAM_KEY] + req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data? rescue Utils::InvalidParameterError, Utils::ParameterTypeError req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params" rescue EOFError diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb index d0538a70d..d77690f44 100644 --- a/test/spec_method_override.rb +++ b/test/spec_method_override.rb @@ -112,4 +112,15 @@ def app 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 From 6c758e3153c2cbeda6307931149b1a0a3079dab6 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 12 Nov 2022 12:37:26 +1300 Subject: [PATCH 02/24] Fix `respond_to?(:each)` with `Rack::Lint` with streaming bodies. (#1981) * Fix handling of `to_path` and add spec. * Also forward `call`. --- CHANGELOG.md | 1 + lib/rack/lint.rb | 8 +++++++- test/spec_lint.rb | 12 +++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d069038d9..41991cf4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. For info on ### 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 diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index 42878879a..75eed2086 100755 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -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 diff --git a/test/spec_lint.rb b/test/spec_lint.rb index 054f9fded..081ff367b 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] } + + status, headers, body = Rack::Lint.new(app).call(env({})) + 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| From 316eff7f53ea84fc206079f0d7c142502a9c65b5 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 18 Nov 2022 12:34:59 -0800 Subject: [PATCH 03/24] Update `CHANGELOG.md`. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41991cf4e..301695c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ 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.1] - 2022-11-18 ### Fixed From 87984bf621be18c028a96f416bb222c68c8ae14c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 18 Nov 2022 12:33:09 -0800 Subject: [PATCH 04/24] Bump patch verison. --- lib/rack/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/version.rb b/lib/rack/version.rb index 782eb9811..cd73e2e1e 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.1" # Return the Rack release as a dotted string. def self.release From aa86b898ca6a1f13a653ccc4e8850a3e570e9952 Mon Sep 17 00:00:00 2001 From: Kenn Costales Date: Mon, 21 Nov 2022 02:11:57 +0800 Subject: [PATCH 05/24] Fix outdated Rack::Builder rdocs and remove Lobster references (#1986) --- lib/rack/auth/basic.rb | 2 -- lib/rack/builder.rb | 62 ++++++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 32 deletions(-) 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+. From 19225caa4ad065354f3afe5bd66b77d2ce47203c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 27 Nov 2022 16:14:33 -0800 Subject: [PATCH 06/24] Remove leading dot to fix compatibility with latest cgi gem. (#1988) --- test/spec_mock_request.rb | 4 ++-- test/spec_mock_response.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/spec_mock_request.rb b/test/spec_mock_request.rb index 889c8a9f1..d7ab76fda 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: "/" }) 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 From 3aa10e6e645043813b6688c2654dbeec876200bf Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 2 Dec 2022 21:37:01 -0800 Subject: [PATCH 07/24] Fix some typos (#1991) --- CHANGELOG.md | 2 +- UPGRADE-GUIDE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 301695c49..2b7077944 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,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/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md index f845e5de3..b3f205a7b 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. From 59c29a49fee6ae52ad04151a0b60c89acdf59ca8 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 2 Dec 2022 21:38:21 -0800 Subject: [PATCH 08/24] Trim trailing white space throughout the project (#1990) Many editors clean up trailing white space on save. By removing it all in one go, it helps keep future diffs cleaner and eases new contributions by avoiding spurious white space changes on unrelated lines. --- .github/workflows/test.yaml | 4 +-- UPGRADE-GUIDE.md | 4 +-- lib/rack/constants.rb | 2 +- lib/rack/headers.rb | 24 +++++++------- lib/rack/lint.rb | 4 +-- lib/rack/request.rb | 4 +-- lib/rack/response.rb | 4 +-- lib/rack/utils.rb | 2 +- test/spec_builder.rb | 2 +- test/spec_deflater.rb | 2 +- test/spec_headers.rb | 66 ++++++++++++++++++------------------- test/spec_response.rb | 4 +-- 12 files changed, 61 insertions(+), 61 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a224f6771..d36cf445d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,9 +10,9 @@ jobs: strategy: fail-fast: false matrix: - os: + os: - ubuntu-latest - ruby: + ruby: - '2.4' - '2.5' - '2.6' diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md index b3f205a7b..290fac24d 100644 --- a/UPGRADE-GUIDE.md +++ b/UPGRADE-GUIDE.md @@ -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/constants.rb b/lib/rack/constants.rb index d99b63673..6aa9281f6 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' 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 75eed2086..ed3c7f421 100755 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -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 @@ -889,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/request.rb b/lib/rack/request.rb index 452140149..6641b5fcd 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 diff --git a/lib/rack/response.rb b/lib/rack/response.rb index 2b7057a2f..c2046105d 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. @@ -333,7 +333,7 @@ def buffered_body! end body.close if body.respond_to?(:close) - + @buffered = true else @buffered = false diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 648b70590..0223d03cd 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -278,7 +278,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 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 index dcd6296bf..f22680b77 100755 --- 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_response.rb b/test/spec_response.rb index 459aaa8fb..144adb521 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", From ab1f1c1f912c5e434e0ac05b26140f5b8b2c0bff Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 2 Dec 2022 23:12:59 -0800 Subject: [PATCH 09/24] Fix Utils.build_nested_query to URL-encode all query string fields (#1989) In the formatted query string, the field should be fully URL-encoded. The previous implementation left the square brackets as un-escaped. It was noticed that if params were provided as a nested hash it produced a different result than a hash of string fields. For example, the params { 'q[s]' => 'name' } would result in the query string: q%5Bs%5D=name but { q: { s: 'name' } } would result in: q[s]=name Both forms now produce the same correct result with fully URL-encoded params. --- CHANGELOG.md | 1 + lib/rack/utils.rb | 6 +++--- test/spec_mock_request.rb | 12 ++++++------ test/spec_utils.rb | 40 +++++++++++++++++++++------------------ 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7077944..d77bbbeae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. For info on - `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]) +- `Utils.build_nested_query` URL-encodes nested field names including the square brackets. ## [3.0.0] - 2022-09-06 diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 0223d03cd..82597450d 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -121,13 +121,13 @@ 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 diff --git a/test/spec_mock_request.rb b/test/spec_mock_request.rb index d7ab76fda..b2a16fded 100644 --- a/test/spec_mock_request.rb +++ b/test/spec_mock_request.rb @@ -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_utils.rb b/test/spec_utils.rb index 52d295024..0293ccfdc 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -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). From c0bb5a5ebd8851f1689758f7f4ed262984acc1d9 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 3 Dec 2022 11:13:26 -0800 Subject: [PATCH 10/24] Remove unnecessary executable bit from test files (#1992) --- test/cgi/rackup_stub.rb | 0 test/cgi/sample_rackup.ru | 0 test/cgi/test | 0 test/cgi/test.ru | 0 test/spec_headers.rb | 0 5 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 test/cgi/rackup_stub.rb mode change 100755 => 100644 test/cgi/sample_rackup.ru mode change 100755 => 100644 test/cgi/test mode change 100755 => 100644 test/cgi/test.ru mode change 100755 => 100644 test/spec_headers.rb 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_headers.rb b/test/spec_headers.rb old mode 100755 new mode 100644 From 3e17592c088872e5b0dc87b61afdf56cf495ffe0 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 5 Dec 2022 17:59:21 +1300 Subject: [PATCH 11/24] Allow passing through streaming bodies. (#1993) --- lib/rack/response.rb | 7 ++++++- test/spec_response.rb | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/rack/response.rb b/lib/rack/response.rb index c2046105d..f24683bcb 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -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 diff --git a/test/spec_response.rb b/test/spec_response.rb index 144adb521..ef8aa481c 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -642,6 +642,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") From dcbda319d807baab594820da58cf7bbf44702d1b Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 5 Dec 2022 18:11:57 +1300 Subject: [PATCH 12/24] Bump patch version. --- CHANGELOG.md | 8 +++++++- lib/rack/version.rb | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d77bbbeae..e9f26dbf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,19 @@ 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/). +## [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]) -- `Utils.build_nested_query` URL-encodes nested field names including the square brackets. ## [3.0.0] - 2022-09-06 diff --git a/lib/rack/version.rb b/lib/rack/version.rb index cd73e2e1e..97bd47eb2 100644 --- a/lib/rack/version.rb +++ b/lib/rack/version.rb @@ -25,7 +25,7 @@ def self.version VERSION end - RELEASE = "3.0.1" + RELEASE = "3.0.2" # Return the Rack release as a dotted string. def self.release From 9dca4d3efe0b28bfdfaa4788bd86d2674866c948 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 26 Dec 2022 03:40:48 -0800 Subject: [PATCH 13/24] Update tests to work on latest Rubies. (#1999) * Don't update to latest rubygems. * Test on Ruby v3.2. --- .github/workflows/test.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d36cf445d..697b3cd27 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,6 +19,7 @@ jobs: - '2.7' - '3.0' - '3.1' + - '3.2' - jruby - truffleruby-head include: From d26971128c30b82f7372c8c33d083716e0b2f902 Mon Sep 17 00:00:00 2001 From: Wei Zhe Date: Mon, 26 Dec 2022 19:48:01 +0800 Subject: [PATCH 14/24] Fix Regexp deprecated third argument with Regexp::NOENCODING (#1998) --- lib/rack/urlmap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, _, _)| From 081ae02a1e3fbd925c1dc85d6c4d91d09ca29514 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 27 Dec 2022 08:59:10 +1300 Subject: [PATCH 15/24] Bump patch version. --- CHANGELOG.md | 8 +++++++- lib/rack/version.rb | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f26dbf3..469e4104a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,13 @@ 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/). -## [3.0.2] -2022-12-05 +## [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 diff --git a/lib/rack/version.rb b/lib/rack/version.rb index 97bd47eb2..27691d821 100644 --- a/lib/rack/version.rb +++ b/lib/rack/version.rb @@ -25,7 +25,7 @@ def self.version VERSION end - RELEASE = "3.0.2" + RELEASE = "3.0.3" # Return the Rack release as a dotted string. def self.release From 5dc8c7bc039a39a3f2e52afb4cc9bf100e65965b Mon Sep 17 00:00:00 2001 From: Jean byroot Boussier Date: Wed, 11 Jan 2023 11:56:00 +0100 Subject: [PATCH 16/24] Rack::MethodOverride handle QueryParser::ParamsTooDeepError (#2006) This middleware already handle two types of parsing issues but somehow not this one. Co-authored-by: Jean Boussier --- lib/rack/method_override.rb | 2 +- test/spec_method_override.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb index 61df3fc5e..6125b1916 100644 --- a/lib/rack/method_override.rb +++ b/lib/rack/method_override.rb @@ -47,7 +47,7 @@ def allowed_methods def method_override_param(req) req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data? - rescue Utils::InvalidParameterError, Utils::ParameterTypeError + 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/test/spec_method_override.rb b/test/spec_method_override.rb index d77690f44..f3b8ad729 100644 --- a/test/spec_method_override.rb +++ b/test/spec_method_override.rb @@ -106,6 +106,13 @@ 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 From af9cbb88a3d873c6dabe08554cbb78b9eeb2b8b1 Mon Sep 17 00:00:00 2001 From: Jean byroot Boussier Date: Wed, 11 Jan 2023 22:01:41 +0100 Subject: [PATCH 17/24] Fix Rack::Lint error message for HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH (#2007) Currently it's printing: ``` Rack::Lint::LintError: env contains HTTP_CONTENT_TYPE, must use ``` Which had me puzzled for quite a while. Co-authored-by: Jean Boussier --- lib/rack/lint.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index ed3c7f421..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 } From 401ca8248e642a9b3b11895f2957d12d448c5d55 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 16 Jan 2023 14:04:04 -0800 Subject: [PATCH 18/24] `Rack::Request#POST` should consistently raise errors. (#2010) Cache errors that occur when invoking `Rack::Request#POST` so they can be raised again later. * Don't throw exactly the same error - so we have the correct backtrace. --- lib/rack/constants.rb | 1 + lib/rack/request.rb | 47 ++++++++++++++++++++++++++----------------- test/spec_request.rb | 16 +++++++++++++++ 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb index 6aa9281f6..13365935b 100644 --- a/lib/rack/constants.rb +++ b/lib/rack/constants.rb @@ -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/request.rb b/lib/rack/request.rb index 6641b5fcd..40922a219 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -496,26 +496,35 @@ 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 + 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, '&') + 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 - 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, {}) + rescue => error + set_header(RACK_REQUEST_FORM_ERROR, error) + raise end end diff --git a/test/spec_request.rb b/test/spec_request.rb index 750639deb..9fed5029a 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -1218,6 +1218,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 = < Date: Tue, 17 Jan 2023 11:33:44 +1300 Subject: [PATCH 19/24] Bump patch version. --- CHANGELOG.md | 6 ++++++ lib/rack/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 469e4104a..2edd32c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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/). +## [3.0.4] - 2022-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 diff --git a/lib/rack/version.rb b/lib/rack/version.rb index 27691d821..f1b2710a6 100644 --- a/lib/rack/version.rb +++ b/lib/rack/version.rb @@ -25,7 +25,7 @@ def self.version VERSION end - RELEASE = "3.0.3" + RELEASE = "3.0.4" # Return the Rack release as a dotted string. def self.release From a493640cd89aec9c148bc9d22c5f938ca9ed0dfa Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 3 Aug 2022 00:19:56 -0700 Subject: [PATCH 20/24] Forbid control characters in attributes This commit restricts the characters accepted in ATTRIBUTE_CHAR, forbidding control characters and fixing a ReDOS vulnerability. This also now should fully follow the RFCs. RFC 2231, Section 7 specifies: attribute-char := RFC 2045, Appendix A specifies: tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> "/" / "[" / "]" / "?" / "=" RFC 822, Section 3.3 specifies: CTL = ; ( 177, 127.) SPACE = ; ( 40, 32.) [CVE-2022-44572] --- lib/rack/multipart/parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index 480badaf2..d624eba85 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -26,7 +26,7 @@ class Error < StandardError; end 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}?/ From 7a9d76a7850455a5ef9403203ea757ed110e7806 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 17 Jan 2023 12:04:37 -0800 Subject: [PATCH 21/24] Fix ReDoS in Rack::Utils.get_byte_ranges This commit fixes a ReDoS problem in `get_byte_ranges`. Thanks @ooooooo_q for the patch! [CVE-2022-44570] --- lib/rack/utils.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 82597450d..040bcef0f 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -426,17 +426,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 From b79bb5ac6e7478aa02f624bd9ef00b25c2502af5 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 17 Jan 2023 12:14:29 -0800 Subject: [PATCH 22/24] Fix ReDoS vulnerability in multipart parser This commit fixes a ReDoS vulnerability when parsing the Content-Disposition field in multipart attachments Thanks to @ooooooo_q for the patch! [CVE-2022-44571] --- lib/rack/multipart/parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index d624eba85..d5a3c8dd8 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -23,7 +23,7 @@ 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{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]} From 0f1e4234a449539c5b9ae8d314abd69d19e93c40 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 17 Jan 2023 12:36:48 -0800 Subject: [PATCH 23/24] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2edd32c0b..04c37c397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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/). +## [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] - 2022-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]) From d1b4c2d82ac5444228d30e66f38156f7046b4296 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 17 Jan 2023 12:47:10 -0800 Subject: [PATCH 24/24] bump version --- lib/rack/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rack/version.rb b/lib/rack/version.rb index f1b2710a6..6194b3496 100644 --- a/lib/rack/version.rb +++ b/lib/rack/version.rb @@ -25,7 +25,7 @@ def self.version VERSION end - RELEASE = "3.0.4" + RELEASE = "3.0.4.1" # Return the Rack release as a dotted string. def self.release