diff options
80 files changed, 704 insertions, 442 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1286d14a..1825ddc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,125 @@ # Changelog -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] +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 + +_Note: There are many unreleased changes in Rack (`master` is around 300 commits ahead of `2-0-stable`), and below is not an exhaustive list. If you would like to help out and document some of the unreleased changes, PRs are welcome._ + ### Added -- CHANGELOG.md using keep a changelog formatting by @twitnithegirl ### Changed -- `Rack::Utils.status_code` now raises an error when the status symbol is invalid instead of `500`. -- `Rack::Request::SCHEME_WHITELIST` has been renamed to `Rack::Request::ALLOWED_SCHEMES` -- `Rack::Multipart::Parser.get_filename` now accepts file that contains `+` in its name, avoiding the replacement of `+` to space character since filenames with `+` are valid. + +- Use `Time#httpdate` format for Expires, as proposed by RFC 7231. ([@nanaya](https://github.com/nanaya)) +- Make `Utils.status_code` raise an error when the status symbol is invalid instead of `500`. +- Rename `Request::SCHEME_WHITELIST` to `Request::ALLOWED_SCHEMES`. +- Make `Multipart::Parser.get_filename` accept files with `+` in their name. +- Add Falcon to the default handler fallbacks. ([@ioquatix](https://github.com/ioquatix)) +- Update codebase to avoid string mutations in preparation for `frozen_string_literals`. ([@pat](https://github.com/pat)) +- Change `MockRequest#env_for` to rely on the input optionally responding to `#size` instead of `#length`. ([@janko](https://github.com/janko)) +- Rename `Rack::File` -> `Rack::Files` and add deprecation notice. ([@postmodern](https://github.com/postmodern)). ### Removed -- HISTORY.md by @twitnithegirl -- NEWS.md by @twitnithegirl + +### Documentation + +- Update broken example in `Session::Abstract::ID` documentation. ([tonytonyjan](https://github.com/tonytonyjan)) +- Add Padrino to the list of frameworks implmenting Rack. ([@wikimatze](https://github.com/wikimatze)) +- Remove Mongrel from the suggested server options in the help output. ([@tricknotes](https://github.com/tricknotes)) +- Replace `HISTORY.md` and `NEWS.md` with `CHANGELOG.md`. ([@twitnithegirl](https://github.com/twitnithegirl)) +- Backfill `CHANGELOG.md` from 2.0.1 to 2.0.7 releases. ([@drenmi](https://github.com/Drenmi)) + +## [2.0.7] - 2019-04-02 + +### Fixed + +- Remove calls to `#eof?` on Rack input in `Multipart::Parser`, as this breaks the specification. ([@matthewd](https://github.com/matthewd)) +- Preserve forwarded IP addresses for trusted proxy chains. ([@SamSaffron](https://github.com/SamSaffron)) + +## [2.0.6] - 2018-11-05 + +### Fixed + +- [[CVE-2018-16470](https://nvd.nist.gov/vuln/detail/CVE-2018-16470)] Reduce buffer size of `Multipart::Parser` to avoid pathological parsing. ([@tenderlove](https://github.com/tenderlove)) +- Fix a call to a non-existing method `#accepts_html` in the `ShowExceptions` middleware. ([@tomelm](https://github.com/tomelm)) +- [[CVE-2018-16471](https://nvd.nist.gov/vuln/detail/CVE-2018-16471)] Whitelist HTTP and HTTPS schemes in `Request#scheme` to prevent a possible XSS attack. ([@PatrickTulskie](https://github.com/PatrickTulskie)) + +## [2.0.5] - 2018-04-23 + +### Fixed + +- Record errors originating from invalid UTF8 in `MethodOverride` middleware instead of breaking. ([@mclark](https://github.com/mclark)) + +## [2.0.4] - 2018-01-31 + +### Changed + +- Ensure the `Lock` middleware passes the original `env` object. ([@lugray](https://github.com/lugray)) +- Improve performance of `Multipart::Parser` when uploading large files. ([@tompng](https://github.com/tompng)) +- Increase buffer size in `Multipart::Parser` for better performance. ([@jkowens](https://github.com/jkowens)) +- Reduce memory usage of `Multipart::Parser` when uploading large files. ([@tompng](https://github.com/tompng)) +- Replace ConcurrentRuby dependency with native `Queue`. ([@devmchakan](https://github.com/devmchakan)) + +### Fixed + +- Require the correct digest algorithm in the `ETag` middleware. ([@matthewd](https://github.com/matthewd)) + +### Documentation + +- Update homepage links to use SSL. ([@hugoabonizio](https://github.com/hugoabonizio)) + +## [2.0.3] - 2017-05-15 + +### Changed + +- Ensure `env` values are ASCII 8-bit encoded. ([@eileencodes](https://github.com/eileencodes)) + +### Fixed + +- Prevent exceptions when a class with mixins inherits from `Session::Abstract::ID`. ([@jnraine](https://github.com/jnraine)) + +## [2.0.2] - 2017-05-08 + +### Added + +- Allow `Session::Abstract::SessionHash#fetch` to accept a block with a default value. ([@yannvanhalewyn](https://github.com/yannvanhalewyn)) +- Add `Builder#freeze_app` to freeze application and all middleware. ([@jeremyevans](https://github.com/jeremyevans)) + +### Changed + +- Freeze default session options to avoid accidental mutation. ([@kirs](https://github.com/kirs)) +- Detect partial hijack without hash headers. ([@devmchakan](https://github.com/devmchakan)) +- Update tests to use MiniTest 6 matchers. ([@tonytonyjan](https://github.com/tonytonyjan)) +- Allow 205 Reset Content responses to set a Content-Length, as RFC 7231 proposes setting this to 0. ([@devmchakan](https://github.com/devmchakan)) + +### Fixed + +- Handle `NULL` bytes in multipart filenames. ([@casperisfine](https://github.com/casperisfine)) +- Remove warnings due to miscapitalized global. ([@ioquatix](https://github.com/ioquatix)) +- Prevent exceptions caused by a race condition on multi-threaded servers. ([@sophiedeziel](https://github.com/sophiedeziel)) +- Add RDoc as an explicit depencency for `doc` group. ([@tonytonyjan](https://github.com/tonytonyjan)) +- Record errors originating from `Multipart::Parser` in the `MethodOverride` middleware instead of letting them bubble up. ([@carlzulauf](https://github.com/carlzulauf)) +- Remove remaining use of removed `Utils#bytesize` method from the `File` middleware. ([@brauliomartinezlm](https://github.com/brauliomartinezlm)) + +### Removed + +- Remove `deflate` encoding support to reduce caching overhead. ([@devmchakan](https://github.com/devmchakan)) + +### Documentation + +- Update broken example in `Deflater` documentation. ([@mwpastore](https://github.com/mwpastore)) + +## [2.0.1] - 2016-06-30 + +### Changed + +- Remove JSON as an explicit dependency. ([@mperham](https://github.com/mperham)) -# -# # History/News Archive Items below this line are from the previously maintained HISTORY.md and NEWS.md files. -# -## [2.0.0] +## [2.0.0.rc1] 2016-05-06 - Rack::Session::Abstract::ID is deprecated. Please change to use Rack::Session::Abstract::Persisted ## [2.0.0.alpha] 2015-12-04 @@ -119,7 +217,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md - Rack::Auth::AbstractRequest#scheme now yields strings, not symbols - Rack::Utils cookie functions now format expires in RFC 2822 format - Rack::File now has a default mime type - - rackup -b 'run Rack::File.new(".")', option provides command line configs + - rackup -b 'run Rack::Files.new(".")', option provides command line configs - Rack::Deflater will no longer double encode bodies - Rack::Mime#match? provides convenience for Accept header matching - Rack::Utils#q_values provides splitting for Accept headers @@ -12,7 +12,7 @@ c_platforms = Bundler::Dsl::VALID_PLATFORMS.dup.delete_if do |platform| platform =~ /jruby/ end -gem "rubocop", require: false +gem "rubocop", "0.68.1", require: false # Alternative solution that might work, but it has bad interactions with # Gemfile.lock if that gets committed/reused: diff --git a/README.rdoc b/README.rdoc index 4b66e779..89bb475a 100644 --- a/README.rdoc +++ b/README.rdoc @@ -32,12 +32,12 @@ These web servers include \Rack handlers in their distributions: * Ebb * Fuzed * Glassfish v3 +* NGINX Unit * Phusion Passenger (which is mod_rack for Apache and for nginx) * Puma * Reel * unixrack * uWSGI -* yahns Any valid \Rack app will run the same on all these handlers, without changing anything. @@ -76,7 +76,7 @@ applications needs using middleware, for example: * Rack::CommonLogger, for creating Apache-style logfiles. * Rack::ShowException, for catching unhandled exceptions and presenting them in a nice and helpful way with clickable backtrace. -* Rack::File, for serving static files. +* Rack::Files, for serving static files. * ...many others! All these components use the same interface, which is described in diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb index dfe2ce96..d334939c 100644 --- a/lib/rack/auth/basic.rb +++ b/lib/rack/auth/basic.rb @@ -2,6 +2,7 @@ require 'rack/auth/abstract/handler' require 'rack/auth/abstract/request' +require 'base64' module Rack module Auth @@ -47,7 +48,7 @@ module Rack end def credentials - @credentials ||= params.unpack("m*").first.split(/:/, 2) + @credentials ||= Base64.decode64(params).split(':', 2) end def username diff --git a/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb index ec6d8748..62bff984 100644 --- a/lib/rack/auth/digest/md5.rb +++ b/lib/rack/auth/digest/md5.rb @@ -108,21 +108,21 @@ module Rack alias :H :md5 def KD(secret, data) - H([secret, data] * ':') + H "#{secret}:#{data}" end def A1(auth, password) - [ auth.username, auth.realm, password ] * ':' + "#{auth.username}:#{auth.realm}:#{password}" end def A2(auth) - [ auth.method, auth.uri ] * ':' + "#{auth.method}:#{auth.uri}" end def digest(auth, password) password_hash = passwords_hashed? ? password : H(A1(auth, password)) - KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':') + KD password_hash, "#{auth.nonce}:#{auth.nc}:#{auth.cnonce}:#{QOP}:#{H A2(auth)}" end end diff --git a/lib/rack/auth/digest/nonce.rb b/lib/rack/auth/digest/nonce.rb index 6c1f28a3..3216d973 100644 --- a/lib/rack/auth/digest/nonce.rb +++ b/lib/rack/auth/digest/nonce.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'digest/md5' +require 'base64' module Rack module Auth @@ -20,7 +21,7 @@ module Rack end def self.parse(string) - new(*string.unpack("m*").first.split(' ', 2)) + new(*Base64.decode64(string).split(' ', 2)) end def initialize(timestamp = Time.now, given_digest = nil) @@ -28,11 +29,11 @@ module Rack end def to_s - [([ @timestamp, digest ] * ' ')].pack("m*").strip + Base64.encode64("#{@timestamp} #{digest}").strip end def digest - ::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':') + ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}") end def valid? diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index 92af0e58..dcd40c76 100644 --- a/lib/rack/builder.rb +++ b/lib/rack/builder.rb @@ -31,10 +31,12 @@ module Rack # You can use +map+ to construct a Rack::URLMap in a convenient way. class Builder + + # https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom UTF_8_BOM = '\xef\xbb\xbf' def self.parse_file(config, opts = Server::Options.new) - if config =~ /\.ru$/ + if config.end_with?('.ru') return self.load_file(config, opts) else require config @@ -95,7 +97,7 @@ module Rack def use(middleware, *args, &block) if @map mapping, @map = @map, nil - @use << proc { |app| generate_map app, mapping } + @use << proc { |app| generate_map(app, mapping) } end @use << proc { |app| middleware.new(app, *args, &block) } end diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb index 76bc9a1a..1ed7ffa9 100644 --- a/lib/rack/cascade.rb +++ b/lib/rack/cascade.rb @@ -11,7 +11,7 @@ module Rack attr_reader :apps def initialize(apps, catch = [404, 405]) - @apps = []; @has_app = {} + @apps = [] apps.each { |app| add app } @catch = {} @@ -41,12 +41,11 @@ module Rack end def add(app) - @has_app[app] = true @apps << app end def include?(app) - @has_app.include? app + @apps.include?(app) end alias_method :<<, :add diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb index 71e35394..692b2070 100644 --- a/lib/rack/common_logger.rb +++ b/lib/rack/common_logger.rb @@ -66,8 +66,8 @@ module Rack end def extract_content_length(headers) - value = headers[CONTENT_LENGTH] or return '-' - value.to_s == '0' ? '-' : value + value = headers[CONTENT_LENGTH] + !value || value.to_s == '0' ? '-' : value end end end diff --git a/lib/rack/core_ext/regexp.rb b/lib/rack/core_ext/regexp.rb new file mode 100644 index 00000000..a32fcdf6 --- /dev/null +++ b/lib/rack/core_ext/regexp.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Regexp has `match?` since Ruby 2.4 +# so to support Ruby < 2.4 we need to define this method + +module Rack + module RegexpExtensions + refine Regexp do + def match?(string, pos = 0) + !!match(string, pos) + end + end unless //.respond_to?(:match?) + end +end diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 67598ef2..939832ce 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -4,6 +4,8 @@ require "zlib" require "time" # for Time.httpdate require 'rack/utils' +require_relative 'core_ext/regexp' + module Rack # This middleware enables compression of http responses. # @@ -17,6 +19,8 @@ module Rack # directive of 'no-transform' is present, or when the response status # code is one that doesn't allow an entity body. class Deflater + using ::Rack::RegexpExtensions + ## # Creates Rack::Deflater middleware. # @@ -84,8 +88,9 @@ module Rack gzip = ::Zlib::GzipWriter.new(self) gzip.mtime = @mtime if @mtime @body.each { |part| - gzip.write(part) - gzip.flush if @sync + len = gzip.write(part) + # Flushing empty parts would raise Zlib::BufError. + gzip.flush if @sync && len > 0 } ensure gzip.close @@ -108,7 +113,7 @@ module Rack # Skip compressing empty entity body responses and responses with # no-transform set. if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) || - headers['Cache-Control'].to_s =~ /\bno-transform\b/ || + /\bno-transform\b/.match?(headers['Cache-Control'].to_s) || (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/) return false end diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index f0acc40d..b08f5949 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -3,6 +3,7 @@ require 'time' require 'rack/utils' require 'rack/mime' +require 'rack/files' module Rack # Rack::Directory serves entries below the +root+ given, according to the @@ -10,7 +11,7 @@ module Rack # will be presented in an html based index. If a file is found, the env will # be passed to the specified +app+. # - # If +app+ is not specified, a Rack::File of the same +root+ will be used. + # If +app+ is not specified, a Rack::Files of the same +root+ will be used. class Directory DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>" @@ -60,7 +61,7 @@ table { width:100%%; } def initialize(root, app = nil) @root = ::File.expand_path(root) - @app = app || Rack::File.new(@root) + @app = app || Rack::Files.new(@root) @head = Rack::Head.new(lambda { |env| get env }) end diff --git a/lib/rack/file.rb b/lib/rack/file.rb index 425c1d38..85d5be72 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -1,178 +1,8 @@ # frozen_string_literal: true -require 'time' -require 'rack/utils' -require 'rack/mime' -require 'rack/request' -require 'rack/head' +require 'rack/files' module Rack - # Rack::File serves files below the +root+ directory given, according to the - # path info of the Rack request. - # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file - # as http://localhost:9292/passwd - # - # Handlers can detect if bodies are a Rack::File, and use mechanisms - # like sendfile on the +path+. - - class File - ALLOWED_VERBS = %w[GET HEAD OPTIONS] - ALLOW_HEADER = ALLOWED_VERBS.join(', ') - - attr_reader :root - - def initialize(root, headers = {}, default_mime = 'text/plain') - @root = ::File.expand_path root - @headers = headers - @default_mime = default_mime - @head = Rack::Head.new(lambda { |env| get env }) - end - - def call(env) - # HEAD requests drop the response body, including 4xx error messages. - @head.call env - end - - def get(env) - request = Rack::Request.new env - unless ALLOWED_VERBS.include? request.request_method - return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER }) - end - - path_info = Utils.unescape_path request.path_info - return fail(400, "Bad Request") unless Utils.valid_path?(path_info) - - clean_path_info = Utils.clean_path_info(path_info) - path = ::File.join(@root, clean_path_info) - - available = begin - ::File.file?(path) && ::File.readable?(path) - rescue SystemCallError - false - end - - if available - serving(request, path) - else - fail(404, "File not found: #{path_info}") - end - end - - def serving(request, path) - if request.options? - return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []] - end - last_modified = ::File.mtime(path).httpdate - return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified - - headers = { "Last-Modified" => last_modified } - mime_type = mime_type path, @default_mime - headers[CONTENT_TYPE] = mime_type if mime_type - - # Set custom headers - @headers.each { |field, content| headers[field] = content } if @headers - - response = [ 200, headers ] - - size = filesize path - - range = nil - ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size) - if ranges.nil? || ranges.length > 1 - # No ranges, or multiple ranges (which we don't support): - # TODO: Support multiple byte-ranges - response[0] = 200 - range = 0..size - 1 - elsif ranges.empty? - # Unsatisfiable. Return error, and file size: - response = fail(416, "Byte range unsatisfiable") - response[1]["Content-Range"] = "bytes */#{size}" - return response - else - # Partial content: - range = ranges[0] - response[0] = 206 - response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" - size = range.end - range.begin + 1 - end - - response[2] = [response_body] unless response_body.nil? - - response[1][CONTENT_LENGTH] = size.to_s - response[2] = make_body request, path, range - response - end - - class Iterator - attr_reader :path, :range - alias :to_path :path - - def initialize path, range - @path = path - @range = range - end - - def each - ::File.open(path, "rb") do |file| - file.seek(range.begin) - remaining_len = range.end - range.begin + 1 - while remaining_len > 0 - part = file.read([8192, remaining_len].min) - break unless part - remaining_len -= part.length - - yield part - end - end - end - - def close; end - end - - private - - def make_body request, path, range - if request.head? - [] - else - Iterator.new path, range - end - end - - def fail(status, body, headers = {}) - body += "\n" - - [ - status, - { - CONTENT_TYPE => "text/plain", - CONTENT_LENGTH => body.size.to_s, - "X-Cascade" => "pass" - }.merge!(headers), - [body] - ] - end - - # The MIME type for the contents of the file located at @path - def mime_type path, default_mime - Mime.mime_type(::File.extname(path), default_mime) - end - - def filesize path - # If response_body is present, use its size. - return response_body.bytesize if response_body - - # We check via File::size? whether this file provides size info - # via stat (e.g. /proc files often don't), otherwise we have to - # figure it out by reading the whole file into memory. - ::File.size?(path) || ::File.read(path).bytesize - end - - # By default, the response body for file requests is nil. - # In this case, the response body will be generated later - # from the file at @path - def response_body - nil - end - end + warn "Rack::File is deprecated, please use Rack::Files instead." + File = Files end diff --git a/lib/rack/files.rb b/lib/rack/files.rb new file mode 100644 index 00000000..61658e5c --- /dev/null +++ b/lib/rack/files.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require 'time' +require 'rack/utils' +require 'rack/mime' +require 'rack/request' +require 'rack/head' + +module Rack + # Rack::File serves files below the +root+ directory given, according to the + # path info of the Rack request. + # e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file + # as http://localhost:9292/passwd + # + # Handlers can detect if bodies are a Rack::File, and use mechanisms + # like sendfile on the +path+. + + class Files + ALLOWED_VERBS = %w[GET HEAD OPTIONS] + ALLOW_HEADER = ALLOWED_VERBS.join(', ') + + attr_reader :root + + def initialize(root, headers = {}, default_mime = 'text/plain') + @root = ::File.expand_path root + @headers = headers + @default_mime = default_mime + @head = Rack::Head.new(lambda { |env| get env }) + end + + def call(env) + # HEAD requests drop the response body, including 4xx error messages. + @head.call env + end + + def get(env) + request = Rack::Request.new env + unless ALLOWED_VERBS.include? request.request_method + return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER }) + end + + path_info = Utils.unescape_path request.path_info + return fail(400, "Bad Request") unless Utils.valid_path?(path_info) + + clean_path_info = Utils.clean_path_info(path_info) + path = ::File.join(@root, clean_path_info) + + available = begin + ::File.file?(path) && ::File.readable?(path) + rescue SystemCallError + false + end + + if available + serving(request, path) + else + fail(404, "File not found: #{path_info}") + end + end + + def serving(request, path) + if request.options? + return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []] + end + last_modified = ::File.mtime(path).httpdate + return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified + + headers = { "Last-Modified" => last_modified } + mime_type = mime_type path, @default_mime + headers[CONTENT_TYPE] = mime_type if mime_type + + # Set custom headers + @headers.each { |field, content| headers[field] = content } if @headers + + response = [ 200, headers ] + + size = filesize path + + range = nil + ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size) + if ranges.nil? || ranges.length > 1 + # No ranges, or multiple ranges (which we don't support): + # TODO: Support multiple byte-ranges + response[0] = 200 + range = 0..size - 1 + elsif ranges.empty? + # Unsatisfiable. Return error, and file size: + response = fail(416, "Byte range unsatisfiable") + response[1]["Content-Range"] = "bytes */#{size}" + return response + else + # Partial content: + range = ranges[0] + response[0] = 206 + response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" + size = range.end - range.begin + 1 + end + + response[2] = [response_body] unless response_body.nil? + + response[1][CONTENT_LENGTH] = size.to_s + response[2] = make_body request, path, range + response + end + + class Iterator + attr_reader :path, :range + alias :to_path :path + + def initialize path, range + @path = path + @range = range + end + + def each + ::File.open(path, "rb") do |file| + file.seek(range.begin) + remaining_len = range.end - range.begin + 1 + while remaining_len > 0 + part = file.read([8192, remaining_len].min) + break unless part + remaining_len -= part.length + + yield part + end + end + end + + def close; end + end + + private + + def make_body request, path, range + if request.head? + [] + else + Iterator.new path, range + end + end + + def fail(status, body, headers = {}) + body += "\n" + + [ + status, + { + CONTENT_TYPE => "text/plain", + CONTENT_LENGTH => body.size.to_s, + "X-Cascade" => "pass" + }.merge!(headers), + [body] + ] + end + + # The MIME type for the contents of the file located at @path + def mime_type path, default_mime + Mime.mime_type(::File.extname(path), default_mime) + end + + def filesize path + # If response_body is present, use its size. + return response_body.bytesize if response_body + + # We check via File::size? whether this file provides size info + # via stat (e.g. /proc files often don't), otherwise we have to + # figure it out by reading the whole file into memory. + ::File.size?(path) || ::File.read(path).bytesize + end + + # By default, the response body for file requests is nil. + # In this case, the response body will be generated later + # from the file at @path + def response_body + nil + end + end +end diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb index bc0a3bf8..df17b238 100644 --- a/lib/rack/handler.rb +++ b/lib/rack/handler.rb @@ -19,7 +19,7 @@ module Rack end if klass = @handlers[server] - klass.split("::").inject(Object) { |o, x| o.const_get(x) } + const_get(klass) else const_get(server, false) end @@ -45,6 +45,9 @@ module Rack raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}." end + SERVER_NAMES = %w(puma thin falcon webrick).freeze + private_constant :SERVER_NAMES + def self.default # Guess. if ENV.include?("PHP_FCGI_CHILDREN") @@ -54,7 +57,7 @@ module Rack elsif ENV.include?("RACK_HANDLER") self.get(ENV["RACK_HANDLER"]) else - pick ['puma', 'thin', 'falcon', 'webrick'] + pick SERVER_NAMES end end diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb index 9eec0c8f..41937c99 100644 --- a/lib/rack/media_type.rb +++ b/lib/rack/media_type.rb @@ -15,7 +15,7 @@ module Rack # 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.downcase + content_type.split(SPLIT_PATTERN, 2).first.tap &:downcase! end # The media type parameters provided in CONTENT_TYPE as a Hash, or @@ -25,15 +25,18 @@ module Rack # { 'charset' => 'utf-8' } def params(content_type) return {} if content_type.nil? - Hash[*content_type.split(SPLIT_PATTERN)[1..-1]. - collect { |s| s.split('=', 2) }. - map { |k, v| [k.downcase, strip_doublequotes(v)] }.flatten] + + content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh| + k, v = s.split('=', 2) + + hsh[k.tap(&:downcase!)] = strip_doublequotes(v) + end end private def strip_doublequotes(str) - (str[0] == ?" && str[-1] == ?") ? str[1..-2] : str + (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str end end end diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index 1bce2809..7c38d5f3 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -2,12 +2,15 @@ require 'rack/utils' require 'strscan' +require 'rack/core_ext/regexp' module Rack module Multipart class MultipartPartLimitError < Errno::EMFILE; end class Parser + using ::Rack::RegexpExtensions + BUFSIZE = 1_048_576 TEXT_PLAIN = "text/plain" TEMPFILE_FACTORY = lambda { |filename, content_type| @@ -277,6 +280,7 @@ module Rack delta = @sbuf.rest_size - @rx_max_size @collector.on_mime_body @mime_index, @sbuf.peek(delta) @sbuf.pos += delta + @sbuf.string = @sbuf.rest end :want_read end @@ -311,7 +315,7 @@ module Rack return unless filename - if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ } + if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) } filename = Utils.unescape_path(filename) end @@ -348,7 +352,7 @@ module Rack k, v = param.split('=', 2) k.strip! v.strip! - v = v[1..-2] if v[0] == '"' && v[-1] == '"' + v = v[1..-2] if v.start_with?('"') && v.end_with?('"') encoding = Encoding.find v if k == CHARSET end end diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb index 6f69e0ed..2a4eb244 100644 --- a/lib/rack/query_parser.rb +++ b/lib/rack/query_parser.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true +require_relative 'core_ext/regexp' + module Rack class QueryParser + using ::Rack::RegexpExtensions + DEFAULT_SEP = /[&;] */n COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n } @@ -51,7 +55,7 @@ module Rack end end - return params.to_params_hash + return params.to_h end # parse_nested_query expands a query string into structural types. Supported @@ -69,7 +73,7 @@ module Rack normalize_params(params, k, v, param_depth_limit) end - return params.to_params_hash + return params.to_h rescue ArgumentError => e raise InvalidParameterError, e.message end @@ -137,7 +141,7 @@ module Rack end def params_hash_has_key?(hash, key) - return false if key =~ /\[\]/ + return false if /\[\]/.match?(key) key.split(/[\[\]]+/).inject(hash) do |h, part| next h if part == '' @@ -173,22 +177,42 @@ module Rack @params.key?(key) end - def to_params_hash - hash = @params - hash.keys.each do |key| - value = hash[key] - if value.kind_of?(self.class) - if value.object_id == self.object_id - hash[key] = hash - else - hash[key] = value.to_params_hash - end - elsif value.kind_of?(Array) - value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x} + # Recursively unwraps nested `Params` objects and constructs an object + # of the same shape, but using the objects' internal representations + # (Ruby hashes) in place of the objects. The result is a hash consisting + # purely of Ruby primitives. + # + # Mutation warning! + # + # 1. This method mutates the internal representation of the `Params` + # objects in order to save object allocations. + # + # 2. The value you get back is a reference to the internal hash + # representation, not a copy. + # + # 3. Because the `Params` object's internal representation is mutable + # through the `#[]=` method, it is not thread safe. The result of + # getting the hash representation while another thread is adding a + # key to it is non-deterministic. + # + def to_h + @params.each do |key, value| + case value + when self + # Handle circular references gracefully. + @params[key] = @params + when Params + @params[key] = value.to_h + when Array + value.map! { |v| v.kind_of?(Params) ? v.to_h : v } + else + # Ignore anything that is not a `Params` object or + # a collection that can contain one. end end - hash + @params end + alias_method :to_params_hash, :to_h end end end diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb index c97d8635..e23ed1fb 100644 --- a/lib/rack/reloader.rb +++ b/lib/rack/reloader.rb @@ -6,6 +6,8 @@ require 'pathname' +require_relative 'core_ext/regexp' + module Rack # High performant source reloader @@ -22,6 +24,8 @@ module Rack # It is performing a check/reload cycle at the start of every request, but # also respects a cool down time, during which nothing will be done. class Reloader + using ::Rack::RegexpExtensions + def initialize(app, cooldown = 10, backend = Stat) @app = app @cooldown = cooldown @@ -71,7 +75,7 @@ module Rack paths = ['./', *$LOAD_PATH].uniq files.map{|file| - next if file =~ /\.(so|bundle)$/ # cannot reload compiled files + next if /\.(so|bundle)$/.match?(file) # cannot reload compiled files found, stat = figure_path(file, paths) next unless found && stat && mtime = stat.mtime diff --git a/lib/rack/request.rb b/lib/rack/request.rb index ce5bc0cd..54ea86c4 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -3,6 +3,8 @@ require 'rack/utils' require 'rack/media_type' +require_relative 'core_ext/regexp' + module Rack # Rack::Request provides a convenient interface to a Rack # environment. It is stateless, the environment +env+ passed to the @@ -13,11 +15,13 @@ module Rack # req.params["data"] class Request + using ::Rack::RegexpExtensions + class << self attr_accessor :ip_filter end - self.ip_filter = lambda { |ip| ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i } + self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) } ALLOWED_SCHEMES = %w(https http).freeze SCHEME_WHITELIST = ALLOWED_SCHEMES if Object.respond_to?(:deprecate_constant) @@ -219,7 +223,7 @@ module Rack string = get_header HTTP_COOKIE return hash if string == get_header(RACK_REQUEST_COOKIE_STRING) - hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE + hash.replace Utils.parse_cookies_header string set_header(RACK_REQUEST_COOKIE_STRING, string) hash end @@ -243,18 +247,23 @@ module Rack def host # Remove port number. - host_with_port.to_s.sub(/:\d+\z/, '') + h = host_with_port + if colon_index = h.index(":") + h[0, colon_index] + else + h + end end def port - if port = host_with_port.split(/:/)[1] + if port = extract_port(host_with_port) port.to_i elsif port = get_header(HTTP_X_FORWARDED_PORT) port.to_i elsif has_header?(HTTP_X_FORWARDED_HOST) DEFAULT_PORTS[scheme] elsif has_header?(HTTP_X_FORWARDED_PROTO) - DEFAULT_PORTS[get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]] + DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))] else get_header(SERVER_PORT).to_i end @@ -349,7 +358,7 @@ module Rack # Fix for Safari Ajax postings that always append \0 # form_vars.sub!(/\0\z/, '') # performance replacement: - form_vars.slice!(-1) if form_vars[-1] == ?\0 + 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, '&') @@ -398,7 +407,8 @@ module Rack # # <tt>env['rack.input']</tt> is not touched. def delete_param(k) - [ self.POST.delete(k), self.GET.delete(k) ].compact.first + post_value, get_value = self.POST.delete(k), self.GET.delete(k) + post_value || get_value end def base_url @@ -491,11 +501,18 @@ module Rack def strip_port(ip_address) # IPv6 format with optional port: "[2001:db8:cafe::17]:47011" # returns: "2001:db8:cafe::17" - return ip_address.gsub(/(^\[|\]:\d+$)/, '') if ip_address.include?('[') + sep_start = ip_address.index('[') + sep_end = ip_address.index(']') + if (sep_start && sep_end) + return ip_address[sep_start + 1, sep_end - 1] + end # IPv4 format with optional port: "192.0.2.43:47011" # returns: "192.0.2.43" - return ip_address.gsub(/:\d+$/, '') if ip_address.count(':') == 1 + sep = ip_address.index(':') + if (sep && ip_address.count(':') == 1) + return ip_address[0, sep] + end ip_address end @@ -505,16 +522,28 @@ module Rack end def forwarded_scheme - scheme_headers = [ - get_header(HTTP_X_FORWARDED_SCHEME), - get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0] - ] + allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) || + allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))) + end + + def allowed_scheme(header) + header if ALLOWED_SCHEMES.include?(header) + end - scheme_headers.each do |header| - return header if ALLOWED_SCHEMES.include?(header) + def extract_proto_header(header) + if header + if (comma_index = header.index(',')) + header[0, comma_index] + else + header + end end + end - nil + def extract_port(uri) + if (colon_index = uri.index(':')) + uri[colon_index + 1, uri.length] + end end end diff --git a/lib/rack/response.rb b/lib/rack/response.rb index db856f8a..58f9e5d6 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -3,7 +3,6 @@ require 'rack/request' require 'rack/utils' require 'rack/body_proxy' -require 'rack/simple_body_proxy' require 'rack/media_type' require 'time' @@ -34,7 +33,7 @@ module Rack def initialize(body = [], status = 200, header = {}) @status = status.to_i - @header = Utils::HeaderHash.new.merge(header) + @header = Utils::HeaderHash.new(header) @writer = lambda { |x| @body << x } @block = nil @@ -73,11 +72,7 @@ module Rack close [status.to_i, header, []] else - if @block.nil? - [status.to_i, header, SimpleBodyProxy.new(@body)] - else - [status.to_i, header, BodyProxy.new(self){}] - end + [status.to_i, header, BodyProxy.new(self){}] end end alias to_a finish # For *response diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb index 6113f858..3774b260 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rack/file' +require 'rack/files' require 'rack/body_proxy' module Rack @@ -16,7 +16,7 @@ module Rack # # In order to take advantage of this middleware, the response body must # respond to +to_path+ and the request must include an X-Sendfile-Type - # header. Rack::File and other components implement +to_path+ so there's + # header. Rack::Files and other components implement +to_path+ so there's # rarely anything you need to do in your application. The X-Sendfile-Type # header is typically set in your web servers configuration. The following # sections attempt to document @@ -117,7 +117,8 @@ module Rack path = ::File.expand_path(body.to_path) if url = map_accel_path(env, path) headers[CONTENT_LENGTH] = '0' - headers[type] = url + # '?' must be percent-encoded because it is not query string but a part of path + headers[type] = ::Rack::Utils.escape_path(url).gsub('?', '%3F') obody = body body = Rack::BodyProxy.new([]) do obody.close if obody.respond_to?(:close) diff --git a/lib/rack/server.rb b/lib/rack/server.rb index 2a3095b3..f0bc1500 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -3,10 +3,12 @@ require 'optparse' require 'fileutils' +require_relative 'core_ext/regexp' module Rack class Server + using ::Rack::RegexpExtensions class Options def parse!(args) @@ -134,7 +136,7 @@ module Rack has_options = false server.valid_options.each do |name, description| - next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own. + next if /^(Host|Port)[^a-zA-Z]/.match?(name.to_s) # ignore handler's host and port options, we do our own. info << " -O %-21s %s" % [name, description] has_options = true end @@ -252,7 +254,7 @@ module Rack class << self def logging_middleware lambda { |server| - server.server.name =~ /CGI/ || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr] + /CGI/.match?(server.server.name) || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr] } end diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index c9f9f458..b15ee3b8 100644 --- a/lib/rack/session/abstract/id.rb +++ b/lib/rack/session/abstract/id.rb @@ -17,6 +17,26 @@ module Rack # SessionHash is responsible to lazily load the session from store. class SessionHash + using Module.new { + refine Hash do + def transform_keys(&block) + hash = {} + each do |key, value| + hash[block.call(key)] = value + end + hash + end + end + } unless {}.respond_to?(:transform_keys) + + def transform_keys(&block) + hash = dup + each do |key, value| + hash[block.call(key)] = value + end + hash + end + include Enumerable attr_writer :id @@ -162,11 +182,7 @@ module Rack end def stringify_keys(other) - hash = {} - other.each do |key, value| - hash[key.to_s] = value - end - hash + other.transform_keys(&:to_s) end end diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index 618b1a0f..70ddadd6 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -6,6 +6,7 @@ require 'rack/request' require 'rack/response' require 'rack/session/abstract/id' require 'json' +require 'base64' module Rack @@ -51,11 +52,11 @@ module Rack # Encode session cookies as Base64 class Base64 def encode(str) - [str].pack('m') + ::Base64.encode64(str) end def decode(str) - str.unpack('m').first + ::Base64.decode64(str) end # Encode session cookies as Marshaled Base64 data @@ -139,9 +140,7 @@ module Rack session_data = request.cookies[@key] if @secrets.size > 0 && session_data - digest, session_data = session_data.reverse.split("--", 2) - digest.reverse! if digest - session_data.reverse! if session_data + session_data, _, digest = session_data.rpartition('--') session_data = nil unless digest_match?(session_data, digest) end diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb index a05ea0fb..dd587633 100644 --- a/lib/rack/session/memcache.rb +++ b/lib/rack/session/memcache.rb @@ -4,6 +4,7 @@ require 'rack/session/abstract/id' require 'memcache' +require 'rack/core_ext/regexp' module Rack module Session @@ -22,6 +23,8 @@ module Rack # a full description of behaviour, please see memcache's documentation. class Memcache < Abstract::ID + using ::Rack::RegexpExtensions + attr_reader :mutex, :pool DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \ @@ -52,7 +55,7 @@ module Rack with_lock(env) do unless sid and session = @pool.get(sid) sid, session = generate_sid, {} - unless /^STORED/ =~ @pool.add(sid, session) + unless /^STORED/.match?(@pool.add(sid, session)) raise "Session collision on '#{sid.inspect}'" end end diff --git a/lib/rack/simple_body_proxy.rb b/lib/rack/simple_body_proxy.rb deleted file mode 100644 index fe007c4c..00000000 --- a/lib/rack/simple_body_proxy.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Rack - class SimpleBodyProxy - def initialize(body) - @body = body - end - - def each(&blk) - @body.each(&blk) - end - end -end diff --git a/lib/rack/static.rb b/lib/rack/static.rb index 766ba0bf..9a0017db 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true -require "rack/file" +require "rack/files" require "rack/utils" +require_relative 'core_ext/regexp' + module Rack # The Rack::Static middleware intercepts requests for static files # (javascript files, images, stylesheets, etc) based on the url prefixes or - # route mappings passed in the options, and serves them using a Rack::File + # route mappings passed in the options, and serves them using a Rack::Files # object. This allows a Rack stack to serve both static and dynamic content. # # Examples: @@ -84,6 +86,7 @@ module Rack # ] # class Static + using ::Rack::RegexpExtensions def initialize(app, options = {}) @app = app @@ -97,11 +100,11 @@ module Rack # Allow for legacy :cache_control option while prioritizing global header_rules setting @header_rules.unshift([:all, { CACHE_CONTROL => options[:cache_control] }]) if options[:cache_control] - @file_server = Rack::File.new(root) + @file_server = Rack::Files.new(root) end def add_index_root?(path) - @index && route_file(path) && path =~ /\/$/ + @index && route_file(path) && path.end_with?('/') end def overwrite_file_path(path) @@ -122,7 +125,7 @@ module Rack if can_serve(path) if overwrite_file_path(path) env[PATH_INFO] = (add_index_root?(path) ? path + @index : @urls[path]) - elsif @gzip && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/ + elsif @gzip && env['HTTP_ACCEPT_ENCODING'] && /\bgzip\b/.match?(env['HTTP_ACCEPT_ENCODING']) path = env[PATH_INFO] env[PATH_INFO] += '.gz' response = @file_server.call(env) @@ -159,14 +162,14 @@ module Rack when :all true when :fonts - path =~ /\.(?:ttf|otf|eot|woff2|woff|svg)\z/ + /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path) when String path = ::Rack::Utils.unescape(path) path.start_with?(rule) || path.start_with?('/' + rule) when Array - path =~ /\.(#{rule.join('|')})\z/ + /\.(#{rule.join('|')})\z/.match?(path) when Regexp - path =~ rule + rule.match?(path) else false end diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 6772089e..38d37aae 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -8,11 +8,15 @@ require 'tempfile' require 'rack/query_parser' require 'time' +require_relative 'core_ext/regexp' + module Rack # Rack::Utils contains a grab-bag of useful methods for writing web # applications adopted from all kinds of Ruby libraries. module Utils + using ::Rack::RegexpExtensions + ParameterTypeError = QueryParser::ParameterTypeError InvalidParameterError = QueryParser::InvalidParameterError DEFAULT_SEP = QueryParser::DEFAULT_SEP @@ -120,7 +124,7 @@ module Rack when Hash value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) - }.reject(&:empty?).join('&') + }.delete_if(&:empty?).join('&') when nil prefix else @@ -134,7 +138,7 @@ module Rack q_value_header.to_s.split(/\s*,\s*/).map do |part| value, parameters = part.split(/\s*;\s*/, 2) quality = 1.0 - if md = /\Aq=([\d.]+)/.match(parameters) + if parameters && (md = /\Aq=([\d.]+)/.match(parameters)) quality = md[1].to_f end [value, quality] @@ -177,27 +181,26 @@ module Rack # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html expanded_accept_encoding = - accept_encoding.map { |m, q| + accept_encoding.each_with_object([]) do |(m, q), list| if m == "*" - (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] } + (available_encodings - accept_encoding.map(&:first)) + .each { |m2| list << [m2, q] } else - [[m, q]] + list << [m, q] end - }.inject([]) { |mem, list| - mem + list - } + end - encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m } + encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map!(&:first) unless encoding_candidates.include?("identity") encoding_candidates.push("identity") end - expanded_accept_encoding.each { |m, q| + expanded_accept_encoding.each do |m, q| encoding_candidates.delete(m) if q == 0.0 - } + end - return (encoding_candidates & available_encodings)[0] + (encoding_candidates & available_encodings)[0] end module_function :select_best_encoding @@ -230,6 +233,8 @@ module Rack case value[:same_site] when false, nil nil + when :none, 'None', :None + '; SameSite=None' when :lax, 'Lax', :Lax '; SameSite=Lax' when true, :strict, 'Strict', :Strict @@ -273,15 +278,15 @@ module Rack cookies = header end - cookies.reject! { |cookie| - if value[:domain] - cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/ - elsif value[:path] - cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/ - else - cookie =~ /\A#{escape(key)}=/ - end - } + regexp = if value[:domain] + /\A#{escape(key)}=.*domain=#{value[:domain]}/ + elsif value[:path] + /\A#{escape(key)}=.*path=#{value[:path]}/ + else + /\A#{escape(key)}=/ + end + + cookies.reject! { |cookie| regexp.match? cookie } cookies.join("\n") end @@ -410,11 +415,9 @@ module Rack # A case-insensitive Hash that preserves the original case of a # header when set. - class HeaderHash < Hash - def self.new(hash = {}) - HeaderHash === hash ? hash : super(hash) - end - + # + # @api private + class HeaderHash < Hash # :nodoc: def initialize(hash = {}) super() @names = {} diff --git a/rack.gemspec b/rack.gemspec index ba822629..f7b13b17 100644 --- a/rack.gemspec +++ b/rack.gemspec @@ -28,8 +28,17 @@ EOF s.email = 'leah@vuxu.org' s.homepage = 'https://rack.github.io/' s.required_ruby_version = '>= 2.2.2' + s.metadata = { + "bug_tracker_uri" => "https://github.com/rack/rack/issues", + "changelog_uri" => "https://github.com/rack/rack/blob/master/CHANGELOG.md", + "documentation_uri" => "https://rubydoc.info/github/rack/rack", + "homepage_uri" => "https://rack.github.io", + "mailing_list_uri" => "https://groups.google.com/forum/#!forum/rack-devel", + "source_code_uri" => "https://github.com/rack/rack" + } s.add_development_dependency 'minitest', "~> 5.0" s.add_development_dependency 'minitest-sprint' + s.add_development_dependency 'minitest-global_expectations' s.add_development_dependency 'rake' end diff --git a/test/builder/anything.rb b/test/builder/anything.rb deleted file mode 100644 index d8a65871..00000000 --- a/test/builder/anything.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class Anything - def self.call(env) - [200, { 'Content-Type' => 'text/plain' }, ['OK']] - end -end diff --git a/test/helper.rb b/test/helper.rb index 9a26e6ac..dff89558 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' module Rack class TestCase < Minitest::Test diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb index 994a79a7..3e479ace 100644 --- a/test/spec_auth_basic.rb +++ b/test/spec_auth_basic.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/auth/basic' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb index d60417eb..cc205aa9 100644 --- a/test/spec_auth_digest.rb +++ b/test/spec_auth_digest.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/auth/digest/md5' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb index 73b194d0..6be79f8b 100644 --- a/test/spec_body_proxy.rb +++ b/test/spec_body_proxy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/body_proxy' require 'stringio' diff --git a/test/spec_builder.rb b/test/spec_builder.rb index b1701735..853fb7b1 100644 --- a/test/spec_builder.rb +++ b/test/spec_builder.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/builder' require 'rack/lint' require 'rack/mock' @@ -240,13 +240,6 @@ describe Rack::Builder do env.must_equal({}) end - it "requires anything not ending in .ru" do - $: << File.dirname(__FILE__) - app, * = Rack::Builder.parse_file 'builder/anything' - Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK' - $:.pop - end - it 'requires an_underscore_app not ending in .ru' do $: << File.dirname(__FILE__) app, * = Rack::Builder.parse_file 'builder/an_underscore_app' diff --git a/test/spec_cascade.rb b/test/spec_cascade.rb index 8061a254..abb7b57f 100644 --- a/test/spec_cascade.rb +++ b/test/spec_cascade.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/cascade' -require 'rack/file' +require 'rack/files' require 'rack/lint' require 'rack/urlmap' require 'rack/mock' @@ -14,7 +14,7 @@ describe Rack::Cascade do end docroot = File.expand_path(File.dirname(__FILE__)) - app1 = Rack::File.new(docroot) + app1 = Rack::Files.new(docroot) app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" }) diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb index 26c5c37f..daa36cad 100644 --- a/test/spec_chunked.rb +++ b/test/spec_chunked.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/chunked' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb index 0aa2a048..330a6480 100644 --- a/test/spec_common_logger.rb +++ b/test/spec_common_logger.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/common_logger' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb index a6a33df1..8402f04e 100644 --- a/test/spec_conditional_get.rb +++ b/test/spec_conditional_get.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'time' require 'rack/conditional_get' require 'rack/mock' diff --git a/test/spec_config.rb b/test/spec_config.rb index d5f7ceca..d97107b6 100644 --- a/test/spec_config.rb +++ b/test/spec_config.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/builder' require 'rack/config' require 'rack/content_length' diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb index 8856e7d3..2e7a8581 100644 --- a/test/spec_content_length.rb +++ b/test/spec_content_length.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/content_length' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb index a46f95ea..53f1d172 100644 --- a/test/spec_content_type.rb +++ b/test/spec_content_type.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/content_type' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb index b0640a04..75244dcc 100644 --- a/test/spec_deflater.rb +++ b/test/spec_deflater.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'stringio' require 'time' # for Time#httpdate require 'rack/deflater' @@ -114,6 +114,19 @@ describe Rack::Deflater do end end + it 'be able to deflate bodies that respond to each and contain empty chunks' do + app_body = Object.new + class << app_body; def each; yield('foo'); yield(''); yield('bar'); end; end + + verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body| + headers.must_equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + end + end + it 'flush deflated chunks to the client as they become ready' do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end @@ -388,7 +401,7 @@ describe Rack::Deflater do app_body = Object.new class << app_body def each - (0..20).each { |i| yield "hello\n".freeze } + (0..20).each { |i| yield "hello\n" } end end diff --git a/test/spec_directory.rb b/test/spec_directory.rb index 1187471c..8635ec90 100644 --- a/test/spec_directory.rb +++ b/test/spec_directory.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/directory' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_etag.rb b/test/spec_etag.rb index 5e13d538..750ceaac 100644 --- a/test/spec_etag.rb +++ b/test/spec_etag.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/etag' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_file.rb b/test/spec_files.rb index 55b6eaad..ad57972d 100644 --- a/test/spec_file.rb +++ b/test/spec_files.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true -require 'minitest/autorun' -require 'rack/file' +require 'minitest/global_expectations/autorun' +require 'rack/files' require 'rack/lint' require 'rack/mock' -describe Rack::File do +describe Rack::Files do DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT - def file(*args) - Rack::Lint.new Rack::File.new(*args) + def files(*args) + Rack::Lint.new Rack::Files.new(*args) end it 'serves files with + in the file name' do Dir.mktmpdir do |dir| File.write File.join(dir, "you+me.txt"), "hello world" - app = file(dir) + app = files(dir) env = Rack::MockRequest.env_for("/you+me.txt") status, _, body = app.call env @@ -28,14 +28,14 @@ describe Rack::File do end it "serve files" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") res.must_be :ok? assert_match(res, /ruby/) end it "set Last-Modified header" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") path = File.join(DOCROOT, "/cgi/test") @@ -45,7 +45,7 @@ describe Rack::File do it "return 304 if file isn't modified since last serve" do path = File.join(DOCROOT, "/cgi/test") - res = Rack::MockRequest.new(file(DOCROOT)). + res = Rack::MockRequest.new(files(DOCROOT)). get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate) res.status.must_equal 304 @@ -54,14 +54,14 @@ describe Rack::File do it "return the file if it's modified since last serve" do path = File.join(DOCROOT, "/cgi/test") - res = Rack::MockRequest.new(file(DOCROOT)). + res = Rack::MockRequest.new(files(DOCROOT)). get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => (File.mtime(path) - 100).httpdate) res.must_be :ok? end it "serve files with URL encoded filenames" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test" + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test" res.must_be :ok? # res.must_match(/ruby/) # nope @@ -71,12 +71,12 @@ describe Rack::File do end it "serve uri with URL encoded null byte (%00) in filenames" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test%00") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test%00") res.must_be :bad_request? end it "allow safe directory traversal" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get('/cgi/../cgi/test') res.must_be :successful? @@ -89,7 +89,7 @@ describe Rack::File do end it "not allow unsafe directory traversal" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get("/../README.rdoc") res.must_be :client_error? @@ -104,7 +104,7 @@ describe Rack::File do end it "allow files with .. in their name" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get("/cgi/..test") res.must_be :not_found? @@ -116,33 +116,33 @@ describe Rack::File do end it "not allow unsafe directory traversal with encoded periods" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/%2E%2E/README") + res = Rack::MockRequest.new(files(DOCROOT)).get("/%2E%2E/README") res.must_be :client_error? res.must_be :not_found? end it "allow safe directory traversal with encoded periods" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%2E%2E/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%2E%2E/cgi/test") res.must_be :successful? end it "404 if it can't find the file" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/blubb") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/blubb") res.must_be :not_found? end it "detect SystemCallErrors" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi") res.must_be :not_found? end it "return bodies that respond to #to_path" do env = Rack::MockRequest.env_for("/cgi/test") - status, _, body = Rack::File.new(DOCROOT).call(env) + status, _, body = Rack::Files.new(DOCROOT).call(env) path = File.join(DOCROOT, "/cgi/test") @@ -154,7 +154,7 @@ describe Rack::File do it "return correct byte range in body" do env = Rack::MockRequest.env_for("/cgi/test") env["HTTP_RANGE"] = "bytes=22-33" - res = Rack::MockResponse.new(*file(DOCROOT).call(env)) + res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 206 res["Content-Length"].must_equal "12" @@ -165,7 +165,7 @@ describe Rack::File do it "return error for unsatisfiable byte range" do env = Rack::MockRequest.env_for("/cgi/test") env["HTTP_RANGE"] = "bytes=1234-5678" - res = Rack::MockResponse.new(*file(DOCROOT).call(env)) + res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 416 res["Content-Range"].must_equal "bytes */208" @@ -173,7 +173,7 @@ describe Rack::File do it "support custom http headers" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38', + status, heads, _ = files(DOCROOT, 'Cache-Control' => 'public, max-age=38', 'Access-Control-Allow-Origin' => '*').call(env) status.must_equal 200 @@ -183,7 +183,7 @@ describe Rack::File do it "support not add custom http headers if none are supplied" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = file(DOCROOT).call(env) + status, heads, _ = files(DOCROOT).call(env) status.must_equal 200 heads['Cache-Control'].must_be_nil @@ -191,7 +191,7 @@ describe Rack::File do end it "only support GET, HEAD, and OPTIONS requests" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) forbidden = %w[post put patch delete] forbidden.each do |method| @@ -209,7 +209,7 @@ describe Rack::File do end it "set Allow correctly for OPTIONS requests" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.options('/cgi/test') res.must_be :successful? res.headers['Allow'].wont_equal nil @@ -217,35 +217,35 @@ describe Rack::File do end it "set Content-Length correctly for HEAD requests" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.head "/cgi/test" res.must_be :successful? res['Content-Length'].must_equal "208" end it "default to a mime type of text/plain" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_equal "text/plain" end it "allow the default mime type to be set" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, 'application/octet-stream'))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, 'application/octet-stream'))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_equal "application/octet-stream" end it "not set Content-Type if the mime type is not set" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, nil))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_be_nil end it "return error when file not found for head request" do - res = Rack::MockRequest.new(file(DOCROOT)).head("/cgi/missing") + res = Rack::MockRequest.new(files(DOCROOT)).head("/cgi/missing") res.must_be :not_found? res.body.must_be :empty? end diff --git a/test/spec_handler.rb b/test/spec_handler.rb index c38cf4e9..5746dc22 100644 --- a/test/spec_handler.rb +++ b/test/spec_handler.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/handler' class Rack::Handler::Lobster; end diff --git a/test/spec_head.rb b/test/spec_head.rb index 1cf8b391..f6f41a5d 100644 --- a/test/spec_head.rb +++ b/test/spec_head.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/head' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_lint.rb b/test/spec_lint.rb index 07f7fe2b..192f260f 100644 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'stringio' require 'tempfile' require 'rack/lint' diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb index d4fcff47..9f3b9a89 100644 --- a/test/spec_lobster.rb +++ b/test/spec_lobster.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/lobster' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_lock.rb b/test/spec_lock.rb index 25d71fc6..cd9e1230 100644 --- a/test/spec_lock.rb +++ b/test/spec_lock.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/lock' require 'rack/mock' diff --git a/test/spec_logger.rb b/test/spec_logger.rb index d6876c5f..f453b14d 100644 --- a/test/spec_logger.rb +++ b/test/spec_logger.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'stringio' require 'rack/lint' require 'rack/logger' diff --git a/test/spec_media_type.rb b/test/spec_media_type.rb index 580d24ce..7d52b4d4 100644 --- a/test/spec_media_type.rb +++ b/test/spec_media_type.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/media_type' describe Rack::MediaType do diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb index 00990f9b..6b01f7c9 100644 --- a/test/spec_method_override.rb +++ b/test/spec_method_override.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'stringio' require 'rack/method_override' require 'rack/mock' diff --git a/test/spec_mime.rb b/test/spec_mime.rb index b9258eb6..8d1ca256 100644 --- a/test/spec_mime.rb +++ b/test/spec_mime.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/mime' describe Rack::Mime do diff --git a/test/spec_mock.rb b/test/spec_mock.rb index 4c5e1b7c..d7246d3f 100644 --- a/test/spec_mock.rb +++ b/test/spec_mock.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'yaml' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index 67888dc0..b029048e 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/multipart' require 'rack/multipart/parser' diff --git a/test/spec_null_logger.rb b/test/spec_null_logger.rb index f15d47a9..1037c9fa 100644 --- a/test/spec_null_logger.rb +++ b/test/spec_null_logger.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/mock' require 'rack/null_logger' diff --git a/test/spec_recursive.rb b/test/spec_recursive.rb index be1e97e6..e77d966d 100644 --- a/test/spec_recursive.rb +++ b/test/spec_recursive.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/recursive' require 'rack/mock' diff --git a/test/spec_request.rb b/test/spec_request.rb index 5c7a9639..583a367e 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'stringio' require 'cgi' require 'rack/request' @@ -1321,26 +1321,26 @@ EOF it "regards local addresses as proxies" do req = make_request(Rack::MockRequest.env_for("/")) - req.trusted_proxy?('127.0.0.1').must_equal 0 - req.trusted_proxy?('10.0.0.1').must_equal 0 - req.trusted_proxy?('172.16.0.1').must_equal 0 - req.trusted_proxy?('172.20.0.1').must_equal 0 - req.trusted_proxy?('172.30.0.1').must_equal 0 - req.trusted_proxy?('172.31.0.1').must_equal 0 - req.trusted_proxy?('192.168.0.1').must_equal 0 - req.trusted_proxy?('::1').must_equal 0 - req.trusted_proxy?('fd00::').must_equal 0 - req.trusted_proxy?('localhost').must_equal 0 - req.trusted_proxy?('unix').must_equal 0 - req.trusted_proxy?('unix:/tmp/sock').must_equal 0 - - req.trusted_proxy?("unix.example.org").must_be_nil - req.trusted_proxy?("example.org\n127.0.0.1").must_be_nil - req.trusted_proxy?("127.0.0.1\nexample.org").must_be_nil - req.trusted_proxy?("11.0.0.1").must_be_nil - req.trusted_proxy?("172.15.0.1").must_be_nil - req.trusted_proxy?("172.32.0.1").must_be_nil - req.trusted_proxy?("2001:470:1f0b:18f8::1").must_be_nil + req.trusted_proxy?('127.0.0.1').must_equal true + req.trusted_proxy?('10.0.0.1').must_equal true + req.trusted_proxy?('172.16.0.1').must_equal true + req.trusted_proxy?('172.20.0.1').must_equal true + req.trusted_proxy?('172.30.0.1').must_equal true + req.trusted_proxy?('172.31.0.1').must_equal true + req.trusted_proxy?('192.168.0.1').must_equal true + req.trusted_proxy?('::1').must_equal true + req.trusted_proxy?('fd00::').must_equal true + req.trusted_proxy?('localhost').must_equal true + req.trusted_proxy?('unix').must_equal true + req.trusted_proxy?('unix:/tmp/sock').must_equal true + + req.trusted_proxy?("unix.example.org").must_equal false + req.trusted_proxy?("example.org\n127.0.0.1").must_equal false + req.trusted_proxy?("127.0.0.1\nexample.org").must_equal false + req.trusted_proxy?("11.0.0.1").must_equal false + req.trusted_proxy?("172.15.0.1").must_equal false + req.trusted_proxy?("172.32.0.1").must_equal false + req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal false end it "sets the default session to an empty hash" do diff --git a/test/spec_response.rb b/test/spec_response.rb index f4248dd9..3cd56664 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/response' require 'stringio' @@ -62,6 +62,16 @@ describe Rack::Response do response["Content-Type"].must_equal "text/plain" end + it "doesn't mutate given headers" do + [{}, Rack::Utils::HeaderHash.new].each do |header| + response = Rack::Response.new([], 200, header) + response.header["Content-Type"] = "text/plain" + response.header["Content-Type"].must_equal "text/plain" + + header.wont_include("Content-Type") + end + end + it "can override the initial Content-Type with a different case" do response = Rack::Response.new("", 200, "content-type" => "text/plain") response["Content-Type"].must_equal "text/plain" @@ -117,6 +127,24 @@ describe Rack::Response do response["Set-Cookie"].must_equal "foo=bar" end + it "can set SameSite cookies with symbol value :none" do + response = Rack::Response.new + response.set_cookie "foo", { value: "bar", same_site: :none } + response["Set-Cookie"].must_equal "foo=bar; SameSite=None" + end + + it "can set SameSite cookies with symbol value :None" do + response = Rack::Response.new + response.set_cookie "foo", { value: "bar", same_site: :None } + response["Set-Cookie"].must_equal "foo=bar; SameSite=None" + end + + it "can set SameSite cookies with string value 'None'" do + response = Rack::Response.new + response.set_cookie "foo", { value: "bar", same_site: "None" } + response["Set-Cookie"].must_equal "foo=bar; SameSite=None" + end + it "can set SameSite cookies with symbol value :lax" do response = Rack::Response.new response.set_cookie "foo", { value: "bar", same_site: :lax } diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb index 932e9de3..6bb5f5cf 100644 --- a/test/spec_rewindable_input.rb +++ b/test/spec_rewindable_input.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'stringio' require 'rack/rewindable_input' diff --git a/test/spec_runtime.rb b/test/spec_runtime.rb index 5a7e84bd..10e561de 100644 --- a/test/spec_runtime.rb +++ b/test/spec_runtime.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/mock' require 'rack/runtime' diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb index cae458e4..cbed8db3 100644 --- a/test/spec_sendfile.rb +++ b/test/spec_sendfile.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'fileutils' require 'rack/lint' require 'rack/sendfile' @@ -8,10 +8,10 @@ require 'rack/mock' require 'tmpdir' describe Rack::Sendfile do - def sendfile_body - FileUtils.touch File.join(Dir.tmpdir, "rack_sendfile") + def sendfile_body(filename = "rack_sendfile") + FileUtils.touch File.join(Dir.tmpdir, filename) res = ['Hello World'] - def res.to_path ; File.join(Dir.tmpdir, "rack_sendfile") ; end + res.define_singleton_method(:to_path) { File.join(Dir.tmpdir, filename) } res end @@ -74,6 +74,19 @@ describe Rack::Sendfile do end end + it "sets X-Accel-Redirect response header to percent-encoded path" do + headers = { + 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', + 'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar%/" + } + request headers, sendfile_body('file_with_%_?_symbol') do |response| + response.must_be :ok? + response.body.must_be :empty? + response.headers['Content-Length'].must_equal '0' + response.headers['X-Accel-Redirect'].must_equal '/foo/bar%25/file_with_%25_%3F_symbol' + end + end + it 'writes to rack.error when no X-Accel-Mapping is specified' do request 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' do |response| response.must_be :ok? diff --git a/test/spec_server.rb b/test/spec_server.rb index 7a60a61e..b09caf03 100644 --- a/test/spec_server.rb +++ b/test/spec_server.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/server' require 'tempfile' @@ -161,6 +161,8 @@ describe Rack::Server do end it "check pid file presence and not owned process" do + owns_pid_1 = (Process.kill(0, 1) rescue nil) == 1 + skip "cannot test if pid 1 owner matches current process (eg. docker/lxc)" if owns_pid_1 pidfile = Tempfile.open('pidfile') { |f| f.write(1); break f }.path server = Rack::Server.new(pid: pidfile) server.send(:pidfile_process_status).must_equal :not_owned diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb index 00140c16..3591a3de 100644 --- a/test/spec_session_abstract_id.rb +++ b/test/spec_session_abstract_id.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' ### WARNING: there be hax in this file. require 'rack/session/abstract/id' diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb index 37030a8c..5d0d10ce 100644 --- a/test/spec_session_abstract_session_hash.rb +++ b/test/spec_session_abstract_session_hash.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/session/abstract/id' describe Rack::Session::Abstract::SessionHash do @@ -44,4 +44,10 @@ describe Rack::Session::Abstract::SessionHash do lambda { hash.fetch(:unknown) }.must_raise KeyError end end + + describe "#stringify_keys" do + it "returns hash or session hash with keys stringified" do + assert_equal({ "foo" => :bar, "baz" => :qux }, hash.send(:stringify_keys, hash).to_h) + end + end end diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb index 8ecfde53..9b4442dd 100644 --- a/test/spec_session_cookie.rb +++ b/test/spec_session_cookie.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/session/cookie' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb index da90b340..a015cee6 100644 --- a/test/spec_session_memcache.rb +++ b/test/spec_session_memcache.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' begin require 'rack/session/memcache' require 'rack/lint' diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb index 6eecce36..fda5f56e 100644 --- a/test/spec_session_pool.rb +++ b/test/spec_session_pool.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'thread' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb index 9cad32ce..a4ade121 100644 --- a/test/spec_show_exceptions.rb +++ b/test/spec_show_exceptions.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/show_exceptions' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb index a4b58e2a..ca23134e 100644 --- a/test/spec_show_status.rb +++ b/test/spec_show_status.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/show_status' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_static.rb b/test/spec_static.rb index 7c510bf6..d33e8edc 100644 --- a/test/spec_static.rb +++ b/test/spec_static.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/static' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb index f6b79641..0e7de841 100644 --- a/test/spec_tempfile_reaper.rb +++ b/test/spec_tempfile_reaper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/tempfile_reaper' require 'rack/lint' require 'rack/mock' diff --git a/test/spec_thin.rb b/test/spec_thin.rb index cc4967b2..0729c3f3 100644 --- a/test/spec_thin.rb +++ b/test/spec_thin.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' begin require 'rack/handler/thin' require File.expand_path('../testrequest', __FILE__) diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb index f5d7e115..9ce38298 100644 --- a/test/spec_urlmap.rb +++ b/test/spec_urlmap.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/urlmap' require 'rack/mock' diff --git a/test/spec_utils.rb b/test/spec_utils.rb index 427bb455..6210fd73 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/utils' require 'rack/mock' require 'timeout' @@ -671,10 +671,10 @@ describe Rack::Utils::HeaderHash do h.delete("Hello").must_be_nil end - it "avoid unnecessary object creation if possible" do + it "dups given HeaderHash" do a = Rack::Utils::HeaderHash.new("foo" => "bar") b = Rack::Utils::HeaderHash.new(a) - b.object_id.must_equal a.object_id + b.object_id.wont_equal a.object_id b.must_equal a end diff --git a/test/spec_version.rb b/test/spec_version.rb index 04604ebf..d4191aa4 100644 --- a/test/spec_version.rb +++ b/test/spec_version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' describe Rack do diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb index cabb2e4a..0d0aa8f7 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack/mock' require 'thread' require File.expand_path('../testrequest', __FILE__) |