diff options
154 files changed, 2663 insertions, 1558 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..b778316a --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,96 @@ +workflows: + version: 2 + + test: + jobs: + - test-jruby + - test-ruby-2.2 + - test-ruby-2.3 + - test-ruby-2.4 + - test-ruby-2.5 + - test-ruby-2.6 + +version: 2 + +default-steps: &default-steps + - checkout + - run: sudo apt-get install lighttpd libfcgi-dev libmemcached-dev + + # Restore bundle cache + - type: cache-restore + key: rack-{{ checksum "rack.gemspec" }}-{{ checksum "Gemfile" }} + + # Bundle install dependencies + - run: bundle install --path vendor/bundle + + # Store bundle cache + - type: cache-save + key: rack-{{ checksum "rack.gemspec" }}-{{ checksum "Gemfile" }} + paths: + - vendor/bundle + + - run: bundle exec rubocop + + - run: bundle exec rake ci + +jobs: + test-ruby-2.2: + docker: + - image: circleci/ruby:2.2 + # Spawn a process owned by root + # This works around an issue explained here: + # https://github.com/circleci/circleci-images/pull/132 + command: sudo /bin/sh + - image: memcached:1.4 + steps: *default-steps + + test-ruby-2.3: + docker: + - image: circleci/ruby:2.3 + # Spawn a process owned by root + # This works around an issue explained here: + # https://github.com/circleci/circleci-images/pull/132 + command: sudo /bin/sh + - image: memcached:1.4 + steps: *default-steps + + test-ruby-2.4: + docker: + - image: circleci/ruby:2.4 + # Spawn a process owned by root + # This works around an issue explained here: + # https://github.com/circleci/circleci-images/pull/132 + command: sudo /bin/sh + - image: memcached:1.4 + steps: *default-steps + + test-ruby-2.5: + docker: + - image: circleci/ruby:2.5 + # Spawn a process owned by root + # This works around an issue explained here: + # https://github.com/circleci/circleci-images/pull/132 + command: sudo /bin/sh + - image: memcached:1.4 + steps: *default-steps + + test-ruby-2.6: + docker: + - image: circleci/ruby:2.6 + # Spawn a process owned by root + # This works around an issue explained here: + # https://github.com/circleci/circleci-images/pull/132 + command: sudo /bin/sh + - image: memcached:1.4 + steps: *default-steps + + test-jruby: + docker: + - image: circleci/jruby + # Spawn a process owned by root + # This works around an issue explained here: + # https://github.com/circleci/circleci-images/pull/132 + command: sudo /bin/sh + - image: memcached:1.4 + steps: *default-steps + diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..22ed9920 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,48 @@ +AllCops: + TargetRubyVersion: 2.2 + DisabledByDefault: true + Exclude: + - '**/vendor/**/*' + +Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + Exclude: + - 'test/builder/bom.ru' + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: true + +Layout/EmptyLineAfterMagicComment: + Enabled: true + +Layout/LeadingCommentSpace: + Enabled: true + Exclude: + - 'test/builder/options.ru' + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeComma: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +# Use `{ a: 1 }` not `{a:1}`. +Layout/SpaceInsideHashLiteralBraces: + Enabled: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cf999939..00000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: ruby -sudo: false -cache: - - bundler - - apt - -services: - - memcached - -addons: - apt: - packages: - - lighttpd - - libfcgi-dev - -before_install: - - gem env version | grep '^\(2\|1.\(8\|9\|[0-9][0-9]\)\)' || gem update --system - - gem list -i bundler || gem install bundler - -script: bundle exec rake ci - -rvm: - - 2.2.5 - - 2.3.1 - - ruby-head - - rbx-2 - - jruby-9.0.4.0 - - jruby-head - -notifications: - email: false - irc: "irc.freenode.org#rack" - -matrix: - allow_failures: - - rvm: rbx-2 - - rvm: jruby-head diff --git a/HISTORY.md b/CHANGELOG.md index 406d1758..1286d14a 100644 --- a/HISTORY.md +++ b/CHANGELOG.md @@ -1,162 +1,96 @@ -Sun Dec 4 18:48:03 2015 Jeremy Daer <jeremydaer@gmail.com> +# 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/) - * First-party "SameSite" cookies. Browsers omit SameSite cookies - from third-party requests, closing the door on many CSRF attacks. +## [Unreleased] +### Added +- CHANGELOG.md using keep a changelog formatting by @twitnithegirl - Pass `same_site: true` (or `:strict`) to enable: - response.set_cookie 'foo', value: 'bar', same_site: true - or `same_site: :lax` to use Lax enforcement: - response.set_cookie 'foo', value: 'bar', same_site: :lax +### 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. - Based on version 7 of the Same-site Cookies internet draft: - https://tools.ietf.org/html/draft-west-first-party-cookies-07 - - Thanks to Ben Toews (@mastahyeti) and Bob Long (@bobjflong) for - updating to drafts 5 and 7. - -Tue Nov 3 16:17:26 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Add `Rack::Events` middleware for adding event based middleware: - middleware that does not care about the response body, but only cares - about doing work at particular points in the request / response - lifecycle. - -Thu Oct 8 14:58:46 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Add `Rack::Request#authority` to calculate the authority under which - the response is being made (this will be handy for h2 pushes). - -Tue Oct 6 13:19:04 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Add `Rack::Response::Helpers#cache_control` and `cache_control=`. - Use this for setting cache control headers on your response objects. - -Tue Oct 6 13:12:21 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Add `Rack::Response::Helpers#etag` and `etag=`. Use this for - setting etag values on the response. - -Sun Oct 3 18:25:03 2015 Jeremy Daer <jeremydaer@gmail.com> - - * Introduce `Rack::Response::Helpers#add_header` to add a value to a - multi-valued response header. Implemented in terms of other - `Response#*_header` methods, so it's available to any response-like - class that includes the `Helpers` module. - - * Add `Rack::Request#add_header` to match. - -Fri Sep 4 18:34:53 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * `Rack::Session::Abstract::ID` IS DEPRECATED. Please switch to - `Rack::Session::Abstract::Persisted`. - `Rack::Session::Abstract::Persisted` uses a request object rather than - the `env` hash. - -Fri Sep 4 17:32:12 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Pull `ENV` access inside the request object in to a module. This - will help with legacy Request objects that are ENV based but don't - want to inherit from Rack::Request - -Fri Sep 4 16:09:11 2015 Aaron Patterson <tenderlove@ruby-lang.org> +### Removed +- HISTORY.md by @twitnithegirl +- NEWS.md by @twitnithegirl - * Move most methods on the `Rack::Request` to a module - `Rack::Request::Helpers` and use public API to get values from the - request object. This enables users to mix `Rack::Request::Helpers` in - to their own objects so they can implement - `(get|set|fetch|each)_header` as they see fit (for example a proxy - object). -Fri Sep 4 14:15:32 2015 Aaron Patterson <tenderlove@ruby-lang.org> +# +# +# History/News Archive +Items below this line are from the previously maintained HISTORY.md and NEWS.md files. +# - * Files and directories with + in the name are served correctly. - Rather than unescaping paths like a form, we unescape with a URI - parser using `Rack::Utils.unescape_path`. Fixes #265 +## [2.0.0] +- Rack::Session::Abstract::ID is deprecated. Please change to use Rack::Session::Abstract::Persisted -Thu Aug 27 15:43:48 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Tempfiles are automatically closed in the case that there were too +## [2.0.0.alpha] 2015-12-04 +- First-party "SameSite" cookies. Browsers omit SameSite cookies from third-party requests, closing the door on many CSRF attacks. +- Pass `same_site: true` (or `:strict`) to enable: response.set_cookie 'foo', value: 'bar', same_site: true or `same_site: :lax` to use Lax enforcement: response.set_cookie 'foo', value: 'bar', same_site: :lax +- Based on version 7 of the Same-site Cookies internet draft: + https://tools.ietf.org/html/draft-west-first-party-cookies-07 +- Thanks to Ben Toews (@mastahyeti) and Bob Long (@bobjflong) for updating to drafts 5 and 7. +- Add `Rack::Events` middleware for adding event based middleware: middleware that does not care about the response body, but only cares about doing work at particular points in the request / response lifecycle. +- Add `Rack::Request#authority` to calculate the authority under which the response is being made (this will be handy for h2 pushes). +- Add `Rack::Response::Helpers#cache_control` and `cache_control=`. Use this for setting cache control headers on your response objects. +- Add `Rack::Response::Helpers#etag` and `etag=`. Use this for setting etag values on the response. +- Introduce `Rack::Response::Helpers#add_header` to add a value to a multi-valued response header. Implemented in terms of other `Response#*_header` methods, so it's available to any response-like class that includes the `Helpers` module. +- Add `Rack::Request#add_header` to match. +- `Rack::Session::Abstract::ID` IS DEPRECATED. Please switch to `Rack::Session::Abstract::Persisted`. `Rack::Session::Abstract::Persisted` uses a request object rather than the `env` hash. +- Pull `ENV` access inside the request object in to a module. This will help with legacy Request objects that are ENV based but don't want to inherit from Rack::Request +- Move most methods on the `Rack::Request` to a module `Rack::Request::Helpers` and use public API to get values from the request object. This enables users to mix `Rack::Request::Helpers` in to their own objects so they can implement `(get|set|fetch|each)_header` as they see fit (for example a proxy object). +- Files and directories with + in the name are served correctly. Rather than unescaping paths like a form, we unescape with a URI parser using `Rack::Utils.unescape_path`. Fixes #265 +- Tempfiles are automatically closed in the case that there were too many posted. - -Thu Aug 27 11:00:03 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Added methods for manipulating response headers that don't assume +- Added methods for manipulating response headers that don't assume they're stored as a Hash. Response-like classes may include the Rack::Response::Helpers module if they define these methods: - - * Rack::Response#has_header? - * Rack::Response#get_header - * Rack::Response#set_header - * Rack::Response#delete_header - -Mon Aug 24 18:05:23 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Introduce Util.get_byte_ranges that will parse the value of the - HTTP_RANGE string passed to it without depending on the `env` hash. - `byte_ranges` is deprecated in favor of this method. - -Sat Aug 22 17:49:49 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Change Session internals to use Request objects for looking up - session information. This allows us to only allocate one request - object when dealing with session objects (rather than doing it every - time we need to manipulate cookies, etc). - -Fri Aug 21 16:30:51 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Add `Rack::Request#initialize_copy` so that the env is duped when - the request gets duped. - -Thu Aug 20 16:20:58 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Added methods for manipulating request specific data. This includes + - Rack::Response#has_header? + - Rack::Response#get_header + - Rack::Response#set_header + - Rack::Response#delete_header +- Introduce Util.get_byte_ranges that will parse the value of the HTTP_RANGE string passed to it without depending on the `env` hash. `byte_ranges` is deprecated in favor of this method. +- Change Session internals to use Request objects for looking up session information. This allows us to only allocate one request object when dealing with session objects (rather than doing it every time we need to manipulate cookies, etc). +- Add `Rack::Request#initialize_copy` so that the env is duped when the request gets duped. +- Added methods for manipulating request specific data. This includes data set as CGI parameters, and just any arbitrary data the user wants to associate with a particular request. New methods: - - * Rack::Request#has_header? - * Rack::Request#get_header - * Rack::Request#fetch_header - * Rack::Request#each_header - * Rack::Request#set_header - * Rack::Request#delete_header - -Thu Jun 18 16:00:05 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * lib/rack/utils.rb: add a method for constructing "delete" cookie + - Rack::Request#has_header? + - Rack::Request#get_header + - Rack::Request#fetch_header + - Rack::Request#each_header + - Rack::Request#set_header + - Rack::Request#delete_header +- lib/rack/utils.rb: add a method for constructing "delete" cookie headers. This allows us to construct cookie headers without depending on the side effects of mutating a hash. - -Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * Prevent extremely deep parameters from being parsed. CVE-2015-3225 - -### May 6th, 2015, Thirty seventh public release 1.6.1 - - Fix CVE-2014-9490, denial of service attack in OkJson ([8cd610](https://github.com/rack/rack/commit/8cd61062954f70e0a03e2855704e95ff4bdd4f6e)) - - Use a monotonic time for Rack::Runtime, if available ([d170b2](https://github.com/rack/rack/commit/d170b2363c949dce60871f9d5a6bfc83da2bedb5)) - - RACK_MULTIPART_LIMIT changed to RACK_MULTIPART_PART_LIMIT (RACK_MULTIPART_LIMIT is deprecated and will be removed in 1.7.0) ([c096c5](https://github.com/rack/rack/commit/c096c50c00230d8eee13ad5f79ad027d9a3f3ca9)) - - See the full [git history](https://github.com/rack/rack/compare/1.6.0...1.6.1) and [milestone tag](https://github.com/rack/rack/issues?utf8=%E2%9C%93&q=milestone%3A%22Rack+1.6%22) - -### May 6th, 2015, Thirty seventh public release 1.5.3 - - Fix CVE-2014-9490, denial of service attack in OkJson ([99f725](https://github.com/rack/rack/commit/99f725b583b357376ffbb7b3b042c5daa3106ad6)) - - Backport bug fixes to 1.5 series ([#585](https://github.com/rack/rack/pull/585), [#711](https://github.com/rack/rack/pull/711), [#756](https://github.com/rack/rack/pull/756)) - - See the full [git history](https://github.com/rack/rack/compare/1.5.2...1.5.3) and [milestone tag](https://github.com/rack/rack/issues?utf8=%E2%9C%93&q=milestone%3A%22Rack+1.5.3%22) - -### December 18th, 2014, Thirty sixth public release 1.6.0 - - Response#unauthorized? helper ([#580](https://github.com/rack/rack/pull/580)) - - Deflater now accepts an options hash to control compression on a per-request level ([#457](https://github.com/rack/rack/pull/457)) - - Builder#warmup method for app preloading ([#617](https://github.com/rack/rack/pull/617)) - - Request#accept_language method to extract HTTP_ACCEPT_LANGUAGE ([#623](https://github.com/rack/rack/pull/623)) - - Add quiet mode of rack server, rackup --quiet ([#674](https://github.com/rack/rack/pull/674)) - - Update HTTP Status Codes to RFC 7231 ([#754](https://github.com/rack/rack/pull/754)) - - Less strict header name validation according to [RFC 2616](https://tools.ietf.org/html/rfc2616) ([#399](https://github.com/rack/rack/pull/399)) - - SPEC updated to specify headers conform to RFC7230 specification ([6839fc](https://github.com/rack/rack/commit/6839fc203339f021cb3267fb09cba89410f086e9)) - - Etag correctly marks etags as weak ([#681](https://github.com/rack/rack/issues/681)) - - Request#port supports multiple x-http-forwarded-proto values ([#669](https://github.com/rack/rack/pull/669)) - - Utils#multipart_part_limit configures the maximum number of parts a request can contain ([#684](https://github.com/rack/rack/pull/684)) - - Default host to localhost when in development mode ([#514](https://github.com/rack/rack/pull/514)) - - Various bugfixes and performance improvements (See the full [git history](https://github.com/rack/rack/compare/1.5.2...1.6.0) and [milestone tag](https://github.com/rack/rack/issues?utf8=%E2%9C%93&q=milestone%3A%22Rack+1.6%22)) - -### February 7th, 2013, Thirty fifth public release 1.5.2 +- Prevent extremely deep parameters from being parsed. CVE-2015-3225 + +## [1.6.1] 2015-05-06 + - Fix CVE-2014-9490, denial of service attack in OkJson + - Use a monotonic time for Rack::Runtime, if available + - RACK_MULTIPART_LIMIT changed to RACK_MULTIPART_PART_LIMIT (RACK_MULTIPART_LIMIT is deprecated and will be removed in 1.7.0) + +## [1.5.3] 2015-05-06 + - Fix CVE-2014-9490, denial of service attack in OkJson + - Backport bug fixes to 1.5 series + +## [1.6.0] 2014-01-18 + - Response#unauthorized? helper + - Deflater now accepts an options hash to control compression on a per-request level + - Builder#warmup method for app preloading + - Request#accept_language method to extract HTTP_ACCEPT_LANGUAGE + - Add quiet mode of rack server, rackup --quiet + - Update HTTP Status Codes to RFC 7231 + - Less strict header name validation according to RFC 2616 + - SPEC updated to specify headers conform to RFC7230 specification + - Etag correctly marks etags as weak + - Request#port supports multiple x-http-forwarded-proto values + - Utils#multipart_part_limit configures the maximum number of parts a request can contain + - Default host to localhost when in development mode + - Various bugfixes and performance improvements + +## [1.5.2] 2013-02-07 - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie - Fix CVE-2013-0262, symlink path traversal in Rack::File - Add various methods to Session for enhanced Rails compatibility @@ -166,19 +100,19 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Fix a race condition that could result in overwritten pidfiles - Various documentation additions -### February 7th, 2013, Thirty fifth public release 1.4.5 +## [1.4.5] 2013-02-07 - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie - Fix CVE-2013-0262, symlink path traversal in Rack::File -### February 7th, Thirty fifth public release 1.1.6, 1.2.8, 1.3.10 +## [1.1.6, 1.2.8, 1.3.10] 2013-02-07 - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie -### January 28th, 2013: Thirty fourth public release 1.5.1 +## [1.5.1] 2013-01-28 - Rack::Lint check_hijack now conforms to other parts of SPEC - Added hash-like methods to Abstract::ID::SessionHash for compatibility - Various documentation corrections -### January 21st, 2013: Thirty third public release 1.5.0 +## [1.5.0] 2013-01-21 - Introduced hijack SPEC, for before-response and after-response hijacking - SessionHash is no longer a Hash subclass - Rack::File cache_control parameter is removed, in place of headers options @@ -202,17 +136,17 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Updated HTTP status codes - Ruby 1.8.6 likely no longer passes tests, and is no longer fully supported -### January 13th, 2013: Thirty second public release 1.4.4, 1.3.9, 1.2.7, 1.1.5 +## [1.4.4, 1.3.9, 1.2.7, 1.1.5] 2013-01-13 - [SEC] Rack::Auth::AbstractRequest no longer symbolizes arbitrary strings - Fixed erroneous test case in the 1.3.x series -### January 7th, 2013: Thirty first public release 1.4.3 +## [1.4.3] 2013-01-07 - Security: Prevent unbounded reads in large multipart boundaries -### January 7th, 2013: Thirtieth public release 1.3.8 +## [1.3.8] 2013-01-07 - Security: Prevent unbounded reads in large multipart boundaries -### January 6th, 2013: Twenty ninth public release 1.4.2 +## [1.4.2] 2013-01-06 - Add warnings when users do not provide a session secret - Fix parsing performance for unquoted filenames - Updated URI backports @@ -242,7 +176,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Rack::BodyProxy now explicitly defines #each, useful for C extensions - Cookies that are not URI escaped no longer cause exceptions -### January 6th, 2013: Twenty eighth public release 1.3.7 +## [1.3.7] 2013-01-06 - Add warnings when users do not provide a session secret - Fix parsing performance for unquoted filenames - Updated URI backports @@ -259,14 +193,14 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Additional notes regarding ECMA escape compatibility issues - Fix the parsing of multiple ranges in range headers -### January 6th, 2013: Twenty seventh public release 1.2.6 +## [1.2.6] 2013-01-06 - Add warnings when users do not provide a session secret - Fix parsing performance for unquoted filenames -### January 6th, 2013: Twenty sixth public release 1.1.4 +## [1.1.4] 2013-01-06 - Add warnings when users do not provide a session secret -### January 22nd, 2012: Twenty fifth public release 1.4.1 +## [1.4.1] 2012-01-22 - Alter the keyspace limit calculations to reduce issues with nested params - Add a workaround for multipart parsing where files contain unescaped "%" - Added Rack::Response::Helpers#method_not_allowed? (code 405) @@ -282,7 +216,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Rack::Static no longer defaults to serving index files - Rack.release was fixed -### December 28th, 2011: Twenty fourth public release 1.4.0 +## [1.4.0] 2011-12-28 - Ruby 1.8.6 support has officially been dropped. Not all tests pass. - Raise sane error messages for broken config.ru - Allow combining run and map in a config.ru @@ -301,32 +235,32 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Support added for HTTP_X_FORWARDED_SCHEME - Numerous bug fixes, including many fixes for new and alternate rubies -### December 28th, 2011: Twenty first public release: 1.1.3. +## [1.1.3] 2011-12-28 - Security fix. http://www.ocert.org/advisories/ocert-2011-003.html Further information here: http://jruby.org/2011/12/27/jruby-1-6-5-1 -### October 17, 2011: Twentieth public release 1.3.5 +## [1.3.5] 2011-10-17 - Fix annoying warnings caused by the backport in 1.3.4 -### October 1, 2011: Nineteenth public release 1.3.4 +## [1.3.4] 2011-10-01 - Backport security fix from 1.9.3, also fixes some roundtrip issues in URI - Small documentation update - Fix an issue where BodyProxy could cause an infinite recursion - Add some supporting files for travis-ci -### September 16, 2011: Eighteenth public release 1.2.4 +## [1.2.4] 2011-09-16 - Fix a bug with MRI regex engine to prevent XSS by malformed unicode -### September 16, 2011: Seventeenth public release 1.3.3 +## [1.3.3] 2011-09-16 - Fix bug with broken query parameters in Rack::ShowExceptions - Rack::Request#cookies no longer swallows exceptions on broken input - Prevents XSS attacks enabled by bug in Ruby 1.8's regexp engine - Rack::ConditionalGet handles broken If-Modified-Since helpers -### July 16, 2011: Sixteenth public release 1.3.2 +## [1.3.2] 2011-07-16 - Fix for Rails and rack-test, Rack::Utils#escape calls to_s -### July 13, 2011: Fifteenth public release 1.3.1 +## [1.3.1] 2011-07-13 - Fix 1.9.1 support - Fix JRuby support - Properly handle $KCODE in Rack::Utils.escape @@ -337,11 +271,11 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Rack::MockResponse calls close on the body object - Fix a DOS vector from MRI stdlib backport -### May 22nd, 2011: Fourteenth public release 1.2.3 +## [1.2.3] 2011-05-22 - Pulled in relevant bug fixes from 1.3 - Fixed 1.8.6 support -### May 22nd, 2011: Thirteenth public release 1.3.0 +## [1.3.0] 2011-05-22 - Various performance optimizations - Various multipart fixes - Various multipart refactors @@ -361,16 +295,16 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Cookies respect renew - Session middleware uses SecureRandom.hex -### March 13th, 2011: Twelfth public release 1.2.2/1.1.2. +## [1.2.2, 1.1.2] 2011-03-13 - Security fix in Rack::Auth::Digest::MD5: when authenticator returned nil, permission was granted on empty password. -### June 15th, 2010: Eleventh public release 1.2.1. +## [1.2.1] 2010-06-15 - Make CGI handler rewindable - Rename spec/ to test/ to not conflict with SPEC on lesser operating systems -### June 13th, 2010: Tenth public release 1.2.0. +## [1.2.0] 2010-06-13 - Removed Camping adapter: Camping 2.0 supports Rack as-is - Removed parsing of quoted values - Add Request.trace? and Request.options? @@ -379,7 +313,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Various multipart fixes - Switch test suite to bacon -### January 3rd, 2010: Ninth public release 1.1.0. +## [1.1.0] 2010-01-03 - Moved Auth::OpenID to rack-contrib. - SPEC change that relaxes Lint slightly to allow subclasses of the required types @@ -414,7 +348,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Enforce binary encoding in RewindableInput - Set correct external_encoding for handlers that don't use RewindableInput -### October 18th, 2009: Eighth public release 1.0.1. +## [1.0.1] 2009-10-18 - Bump remainder of rack.versions. - Support the pure Ruby FCGI implementation. - Fix for form names containing "=": split first then unescape components @@ -425,7 +359,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Make sure WEBrick respects the :Host option - Many Ruby 1.9 fixes. -### April 25th, 2009: Seventh public release 1.0.0. +## [1.0.0] 2009-04-25 - SPEC change: Rack::VERSION has been pushed to [1,0]. - SPEC change: header values must be Strings now, split on "\n". - SPEC change: Content-Length can be missing, in this case chunked transfer @@ -447,10 +381,10 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - The Rakefile has been rewritten. - Many bugfixes and small improvements. -### January 9th, 2009: Sixth public release 0.9.1. +## [0.9.1] 2009-01-09 - Fix directory traversal exploits in Rack::File and Rack::Directory. -### January 6th, 2009: Fifth public release 0.9. +## [0.9] 2009-01-06 - Rack is now managed by the Rack Core Team. - Rack::Lint is stricter and follows the HTTP RFCs more closely. - Added ConditionalGet middleware. @@ -466,7 +400,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Made HeaderHash case-preserving. - Many bugfixes and small improvements. -### August 21st, 2008: Fourth public release 0.4. +## [0.4] 2008-08-21 - New middleware, Rack::Deflater, by Christoffer Sawicki. - OpenID authentication now needs ruby-openid 2. - New Memcache sessions, by blink. @@ -478,7 +412,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Improved tests. - Rack moved to Git. -### February 26th, 2008: Third public release 0.3. +## [0.3] 2008-02-26 - LiteSpeed handler, by Adrian Madrid. - SCGI handler, by Jeremy Evans. - Pool sessions, by blink. @@ -490,7 +424,7 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - HTTP status 201 can contain a Content-Type and a body now. - Many bugfixes, especially related to Cookie handling. -### May 16th, 2007: Second public release 0.2. +## [0.2] 2007-05-16 - HTTP Basic authentication. - Cookie Sessions. - Static file handler. @@ -500,6 +434,4 @@ Fri Jun 12 11:37:41 2015 Aaron Patterson <tenderlove@ruby-lang.org> - Bug fixes in the Camping adapter. - Removed Rails adapter, was too alpha. -### March 3rd, 2007: First public release 0.1. - -/* vim: set filetype=changelog */ +## [0.1] 2007-03-03 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec @@ -10,12 +12,18 @@ c_platforms = Bundler::Dsl::VALID_PLATFORMS.dup.delete_if do |platform| platform =~ /jruby/ end +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: # c_platforms = [:mri] if Gem.platforms.last.os == "java" group :extra do - gem 'fcgi', :platforms => c_platforms + gem 'fcgi', platforms: c_platforms gem 'memcache-client' - gem 'thin', :platforms => c_platforms + gem 'thin', platforms: c_platforms +end + +group :doc do + gem 'rdoc' end @@ -1,4 +1,6 @@ -Copyright (c) 2007-2016 Christian Neukirchen <purl.org/net/chneukirchen> +The MIT License (MIT) + +Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to @@ -13,6 +15,6 @@ all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. @@ -1,14 +0,0 @@ -# NEWS for Rack 2.0.0 - -This document is a list of user visible feature changes made between -releases except for bug fixes. - -Note that each entry is kept so brief that no reason behind or -reference information is supplied with. For a full list of changes -with all sufficient information, see the HISTORY.md file or the commit log. - -## Changes Since 1.6 - -* Rack::Session::Abstract::ID is deprecated. Please change to use -Rack::Session::Abstract::Persisted - diff --git a/README.rdoc b/README.rdoc index bedcda99..883380c1 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,17 +1,24 @@ -= Rack, a modular Ruby webserver interface {<img src="https://secure.travis-ci.org/rack/rack.svg" alt="Build Status" />}[http://travis-ci.org/rack/rack] {<img src="https://gemnasium.com/rack/rack.svg" alt="Dependency Status" />}[https://gemnasium.com/rack/rack] += \Rack, a modular Ruby webserver interface -Rack provides a minimal, modular, and adaptable interface for developing -web applications in Ruby. By wrapping HTTP requests and responses in +{<img src="https://rack.github.io/rack-logo.png" width="400" alt="rack powers web applications" />}[https://rack.github.io/] + +{<img src="https://circleci.com/gh/rack/rack.svg?style=svg" alt="CircleCI" />}[https://circleci.com/gh/rack/rack] +{<img src="https://badge.fury.io/rb/rack.svg" alt="Gem Version" />}[http://badge.fury.io/rb/rack] +{<img src="https://api.dependabot.com/badges/compatibility_score?dependency-name=rack&package-manager=bundler&version-scheme=semver" alt="SemVer Stability" />}[https://dependabot.com/compatibility-score.html?dependency-name=rack&package-manager=bundler&version-scheme=semver] + +\Rack provides a minimal, modular, and adaptable interface for developing +web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -The exact details of this are described in the Rack specification, -which all Rack applications should conform to. +The exact details of this are described in the \Rack specification, +which all \Rack applications should conform to. == Supported web servers -The included *handlers* connect all kinds of web servers to Rack: +The included *handlers* connect all kinds of web servers to \Rack: + * WEBrick * FCGI * CGI @@ -19,31 +26,34 @@ The included *handlers* connect all kinds of web servers to Rack: * LiteSpeed * Thin -These web servers include Rack handlers in their distributions: +These web servers include \Rack handlers in their distributions: + +* Agoo * Ebb * Fuzed * Glassfish v3 * Phusion Passenger (which is mod_rack for Apache and for nginx) * Puma * Reel -* Unicorn * unixrack * uWSGI -* yahns -Any valid Rack app will run the same on all these handlers, without +Any valid \Rack app will run the same on all these handlers, without changing anything. == Supported web frameworks -These frameworks include Rack adapters in their distributions: +These frameworks include \Rack adapters in their distributions: + * Camping * Coset * Espresso * Halcyon +* Hanami * Mack * Maveric * Merb +* Padrino * Racktools::SimpleApplication * Ramaze * Ruby on Rails @@ -51,14 +61,16 @@ These frameworks include Rack adapters in their distributions: * Sinatra * Sin * Vintage +* WABuR * Waves * Wee * ... and many others. == Available middleware -Between the server and the framework, Rack can be customized to your +Between the server and the framework, \Rack can be customized to your applications needs using middleware, for example: + * Rack::URLMap, to route to multiple applications inside the same process. * Rack::CommonLogger, for creating Apache-style logfiles. * Rack::ShowException, for catching unhandled exceptions and @@ -67,33 +79,34 @@ applications needs using middleware, for example: * ...many others! All these components use the same interface, which is described in -detail in the Rack specification. These optional components can be +detail in the \Rack specification. These optional components can be used in any way you wish. == Convenience If you want to develop outside of existing frameworks, implement your -own ones, or develop middleware, Rack provides many helpers to create -Rack applications quickly and without doing the same web stuff all +own ones, or develop middleware, \Rack provides many helpers to create +\Rack applications quickly and without doing the same web stuff all over: + * Rack::Request, which also provides query string parsing and multipart handling. * Rack::Response, for convenient generation of HTTP replies and cookie handling. * Rack::MockRequest and Rack::MockResponse for efficient and quick - testing of Rack application without real HTTP round-trips. + testing of \Rack application without real HTTP round-trips. == rack-contrib The plethora of useful middleware created the need for a project that -collects fresh Rack middleware. rack-contrib includes a variety of -add-on components for Rack and it is easy to contribute new modules. +collects fresh \Rack middleware. rack-contrib includes a variety of +add-on components for \Rack and it is easy to contribute new modules. * https://github.com/rack/rack-contrib == rackup -rackup is a useful tool for running Rack applications, which uses the +rackup is a useful tool for running \Rack applications, which uses the Rack::Builder DSL to configure middleware and build up applications easily. @@ -117,18 +130,13 @@ By default, the lobster is found at http://localhost:9292. == Installing with RubyGems -A Gem of Rack is available at rubygems.org. You can install it with: +A Gem of \Rack is available at {rubygems.org}[https://rubygems.org/gems/rack]. You can install it with: gem install rack -I also provide a local mirror of the gems (and development snapshots) -at my site: - - gem install rack --source http://chneukirchen.org/releases/gems/ - == Running the tests -Testing Rack requires the bacon testing framework: +Testing \Rack requires the bacon testing framework: bundle install --without extra # to be able to run the fast tests @@ -138,7 +146,7 @@ Or: There is a rake-based test task: - rake test tests all the tests + rake test # tests all the tests The testsuite has no dependencies outside of the core Ruby installation and bacon. @@ -159,9 +167,9 @@ Download and install lighttpd: Installing the FCGI libraries: - curl -O http://www.fastcgi.com/dist/fcgi-2.4.0.tar.gz - tar xzvf fcgi-2.4.0.tar.gz - cd fcgi-2.4.0 + curl -O -k -L https://github.com/FastCGI-Archives/FastCGI.com/raw/master/original_snapshot/fcgi-2.4.1-SNAP-0910052249.tar.gz + tar xvfz fcgi-2.4.1-SNAP-0910052249.tar.gz + cd fcgi-2.4.1-SNAP-0910052249 ./configure --prefix=/usr/local make sudo make install @@ -176,7 +184,7 @@ run on port 11211) and memcache-client installed. == Configuration -Several parameters can be modified on Rack::Utils to configure Rack behaviour. +Several parameters can be modified on Rack::Utils to configure \Rack behaviour. e.g: @@ -198,27 +206,28 @@ The default is 128, which means that a single request can't upload more than 128 Set to 0 for no limit. -Can also be set via the RACK_MULTIPART_PART_LIMIT environment variable. +Can also be set via the +RACK_MULTIPART_PART_LIMIT+ environment variable. -== History +== Changelog -See <https://github.com/rack/rack/blob/master/HISTORY.md>. +See {CHANGELOG.md}[https://github.com/rack/rack/blob/master/CHANGELOG.md]. == Contact Please post bugs, suggestions and patches to -the bug tracker at <https://github.com/rack/rack/issues>. +the bug tracker at {issues}[https://github.com/rack/rack/issues]. Please post security related bugs and suggestions to the core team at -<https://groups.google.com/group/rack-core> or rack-core@googlegroups.com. This +<https://groups.google.com/forum/#!forum/rack-core> or rack-core@googlegroups.com. This list is not public. Due to wide usage of the library, it is strongly preferred that we manage timing in order to provide viable patches at the time of disclosure. Your assistance in this matter is greatly appreciated. Mailing list archives are available at -<https://groups.google.com/group/rack-devel>. +<https://groups.google.com/forum/#!forum/rack-devel>. Git repository (send Git patches to the mailing list): + * https://github.com/rack/rack * http://git.vuxu.org/cgi-bin/gitweb.cgi?p=rack-github.git @@ -226,9 +235,9 @@ You are also welcome to join the #rack channel on irc.freenode.net. == Thanks -The Rack Core Team, consisting of +The \Rack Core Team, consisting of -* Christian Neukirchen (chneukirchen[https://github.com/chneukirchen]) +* Leah Neukirchen (leahneukirchen[https://github.com/leahneukirchen]) * James Tucker (raggi[https://github.com/raggi]) * Josh Peek (josh[https://github.com/josh]) * José Valim (josevalim[https://github.com/josevalim]) @@ -237,7 +246,7 @@ The Rack Core Team, consisting of * Santiago Pastorino (spastorino[https://github.com/spastorino]) * Konstantin Haase (rkh[https://github.com/rkh]) -and the Rack Alumnis +and the \Rack Alumnis * Ryan Tomayko (rtomayko[https://github.com/rtomayko]) * Scytrin dai Kinthra (scytrin[https://github.com/scytrin]) @@ -269,36 +278,16 @@ would like to thank: * Alexander Kellett for testing the Gem and reviewing the announcement. * Marcus Rückert, for help with configuring and debugging lighttpd. * The WSGI team for the well-done and documented work they've done and - Rack builds up on. + \Rack builds up on. * All bug reporters and patch contributors not mentioned above. -== Copyright - -Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <http://purl.org/net/chneukirchen> - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - == Links -Rack:: <http://rack.github.io/> -Official Rack repositories:: <https://github.com/rack> -Rack Bug Tracking:: <https://github.com/rack/rack/issues> -rack-devel mailing list:: <https://groups.google.com/group/rack-devel> -Rack's Rubyforge project:: <http://rubyforge.org/projects/rack> +\Rack:: <https://rack.github.io/> +Official \Rack repositories:: <https://github.com/rack> +\Rack Bug Tracking:: <https://github.com/rack/rack/issues> +rack-devel mailing list:: <https://groups.google.com/forum/#!forum/rack-devel> + +== License -Christian Neukirchen:: <http://chneukirchen.org/> +\Rack is released under the {MIT License}[https://opensource.org/licenses/MIT]. @@ -1,7 +1,9 @@ -# Rakefile for Rack. -*-ruby-*- +# frozen_string_literal: true + +require "rake/testtask" desc "Run all the tests" -task :default => [:test] +task default: :test desc "Install gem dependencies" task :deps do @@ -16,7 +18,7 @@ task :deps do end desc "Make an archive as .tar.gz" -task :dist => %w[chmod ChangeLog SPEC rdoc] do +task dist: %w[chmod changelog spec rdoc] do sh "git archive --format=tar --prefix=#{release}/ HEAD^{tree} >#{release}.tar" sh "pax -waf #{release}.tar -s ':^:#{release}/:' SPEC ChangeLog doc rack.gemspec" sh "gzip -f -9 #{release}.tar" @@ -31,7 +33,7 @@ task :officialrelease do sh "mv stage/#{release}.tar.gz stage/#{release}.gem ." end -task :officialrelease_really => %w[SPEC dist gem] do +task officialrelease_really: %w[spec dist gem] do sh "shasum #{release}.tar.gz #{release}.gem" end @@ -46,7 +48,7 @@ task :chmod do end desc "Generate a ChangeLog" -task :changelog => %w[ChangeLog] +task changelog: "ChangeLog" file '.git/index' file "ChangeLog" => '.git/index' do @@ -68,8 +70,10 @@ file "ChangeLog" => '.git/index' do } end -file 'lib/rack/lint.rb' desc "Generate Rack Specification" +task spec: "SPEC" + +file 'lib/rack/lint.rb' file "SPEC" => 'lib/rack/lint.rb' do File.open("SPEC", "wb") { |file| IO.foreach("lib/rack/lint.rb") { |line| @@ -80,24 +84,27 @@ file "SPEC" => 'lib/rack/lint.rb' do } end -desc "Run all the fast + platform agnostic tests" -task :test => 'SPEC' do - opts = ENV['TEST'] || '' - specopts = ENV['TESTOPTS'] - - sh "ruby -I./lib:./test -S minitest #{opts} #{specopts} test/gemloader.rb test/spec*.rb" +Rake::TestTask.new("test:regular") do |t| + t.libs << "test" + t.test_files = FileList["test/**/*_test.rb", "test/**/spec_*.rb", "test/gemloader.rb"] + t.warning = false + t.verbose = true end +desc "Run all the fast + platform agnostic tests" +task test: %w[spec test:regular] + desc "Run all the tests we run on CI" -task :ci => :test +task ci: :test -task :gem => ["SPEC"] do +task gem: :spec do sh "gem build rack.gemspec" end -task :doc => :rdoc +task doc: :rdoc + desc "Generate RDoc documentation" -task :rdoc => %w[ChangeLog SPEC] do +task rdoc: %w[changelog spec] do sh(*%w{rdoc --line-numbers --main README.rdoc --title 'Rack\ Documentation' --charset utf-8 -U -o doc} + %w{README.rdoc KNOWN-ISSUES SPEC ChangeLog} + @@ -105,11 +112,11 @@ task :rdoc => %w[ChangeLog SPEC] do cp "contrib/rdoc.css", "doc/rdoc.css" end -task :pushdoc => %w[rdoc] do +task pushdoc: :rdoc do sh "rsync -avz doc/ rack.rubyforge.org:/var/www/gforge-projects/rack/doc/" end -task :pushsite => %w[pushdoc] do +task pushsite: :pushdoc do sh "cd site && git gc" sh "rsync -avz site/ rack.rubyforge.org:/var/www/gforge-projects/rack/" sh "cd site && git push" diff --git a/SECURITY_POLICY.md b/SECURITY_POLICY.md index 844d6969..04fdd488 100644 --- a/SECURITY_POLICY.md +++ b/SECURITY_POLICY.md @@ -10,22 +10,22 @@ New features will only be added to the master branch and will not be made availa Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from. -* Current release series: 1.6.x +* Current release series: 2.0.x ### Security issues The current release series and the next most recent one will receive patches and new versions in case of a security issue. -* Current release series: 1.6.x -* Next most recent release series: 1.5.x +* Current release series: 2.0.x +* Next most recent release series: 1.6.x ### Severe security issues For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. -* Current release series: 1.6.x -* Next most recent release series: 1.5.x -* Last most recent release series: 1.4.x +* Current release series: 2.0.x +* Next most recent release series: 1.6.x +* Last most recent release series: 1.5.x ### Unsupported Release Series @@ -33,14 +33,13 @@ When a release series is no longer supported, it’s your own responsibility to ## Reporting a bug -All security bugs in Rack should be reported to the core team through our private mailing list [rack-core@googlegroups.com](https://groups.google.com/group/rack-core). Your report will be acknowledged within 24 hours, and you’ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report. +All security bugs in Rack should be reported to the core team through our private mailing list [rack-core@googlegroups.com](https://groups.google.com/forum/#!forum/rack-core). Your report will be acknowledged within 24 hours, and you’ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report. After the initial reply to your report the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least every five days, in reality this is more likely to be every 24-48 hours. If you have not received a reply to your email within 48 hours, or have not heard from the security team for the past five days there are a few steps you can take: * Contact the current security coordinator [Aaron Patterson](mailto:tenderlove@ruby-lang.org) directly -* Contact the back-up contact [Santiago Pastorino](mailto:santiago@wyeworks.com) directly. ## Disclosure Policy @@ -64,4 +63,4 @@ No one outside the core team, the initial reporter or vendor-sec will be notifie ## Comments on this Policy -If you have any suggestions to improve this policy, please send an email the core team at [rack-core@googlegroups.com](https://groups.google.com/group/rack-core). +If you have any suggestions to improve this policy, please send an email the core team at [rack-core@googlegroups.com](https://groups.google.com/forum/#!forum/rack-core). @@ -60,9 +60,8 @@ below. the presence or absence of the appropriate HTTP header in the request. See - {https://tools.ietf.org/html/rfc3875#section-4.1.18 - RFC3875 section 4.1.18} for - specific behavior. + {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18] + for specific behavior. In addition to this, the Rack environment must include these Rack-specific variables: <tt>rack.version</tt>:: The Array representing this version of Rack @@ -98,12 +97,13 @@ Rack-specific variables: Additional environment specifications have approved to standardized middleware APIs. None of these are required to be implemented by the server. -<tt>rack.session</tt>:: A hash like interface for storing request session data. +<tt>rack.session</tt>:: A hash like interface for storing + request session data. The store must implement: - store(key, value) (aliased as []=); - fetch(key, default = nil) (aliased as []); - delete(key); - clear; + store(key, value) (aliased as []=); + fetch(key, default = nil) (aliased as []); + delete(key); + clear; <tt>rack.logger</tt>:: A common object interface for logging messages. The object must implement: info(message, &block) @@ -237,10 +237,10 @@ consisting of lines (for multiple header values, e.g. multiple The lines must not contain characters below 037. === The Content-Type There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, -204, 205 or 304. +204 or 304. === The Content-Length There must not be a <tt>Content-Length</tt> header when the -+Status+ is 1xx, 204, 205 or 304. ++Status+ is 1xx, 204 or 304. === The Body The Body must respond to +each+ and must only yield String values. @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "rack" Rack::Server.start diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 08e2b358..00000000 --- a/circle.yml +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: - pre: - - sudo apt-get install lighttpd libfcgi-dev libmemcache-dev memcached -machine: - ruby: - version: ruby-head diff --git a/example/lobster.ru b/example/lobster.ru index cc7ffcae..901e18a5 100644 --- a/example/lobster.ru +++ b/example/lobster.ru @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/lobster' use Rack::ShowExceptions diff --git a/example/protectedlobster.rb b/example/protectedlobster.rb index 26b23661..fe4f0b09 100644 --- a/example/protectedlobster.rb +++ b/example/protectedlobster.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack' require 'rack/lobster' @@ -11,4 +13,4 @@ protected_lobster.realm = 'Lobster 2.0' pretty_protected_lobster = Rack::ShowStatus.new(Rack::ShowExceptions.new(protected_lobster)) -Rack::Server.start :app => pretty_protected_lobster, :Port => 9292 +Rack::Server.start app: pretty_protected_lobster, Port: 9292 diff --git a/example/protectedlobster.ru b/example/protectedlobster.ru index 1ba48702..0eb243cc 100644 --- a/example/protectedlobster.ru +++ b/example/protectedlobster.ru @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/lobster' use Rack::ShowExceptions diff --git a/lib/rack.rb b/lib/rack.rb index 00621178..5a8d71c4 100644 --- a/lib/rack.rb +++ b/lib/rack.rb @@ -1,7 +1,9 @@ -# Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <purl.org/net/chneukirchen> +# frozen_string_literal: true + +# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html> # # Rack is freely distributable under the terms of an MIT-style license. -# See COPYING or http://www.opensource.org/licenses/mit-license.php. +# See MIT-LICENSE or https://opensource.org/licenses/MIT. # The Rack main module, serving as a namespace for all core Rack # modules and classes. @@ -11,80 +13,80 @@ module Rack # The Rack protocol version number implemented. - VERSION = [1,3] + VERSION = [1, 3] # Return the Rack protocol version as a dotted string. def self.version VERSION.join(".") end - RELEASE = "2.0.0.rc1" + RELEASE = "2.1.0" # Return the Rack release as a dotted string. def self.release RELEASE end - HTTP_HOST = 'HTTP_HOST'.freeze - HTTP_VERSION = 'HTTP_VERSION'.freeze - HTTPS = 'HTTPS'.freeze - PATH_INFO = 'PATH_INFO'.freeze - REQUEST_METHOD = 'REQUEST_METHOD'.freeze - REQUEST_PATH = 'REQUEST_PATH'.freeze - SCRIPT_NAME = 'SCRIPT_NAME'.freeze - QUERY_STRING = 'QUERY_STRING'.freeze - SERVER_PROTOCOL = 'SERVER_PROTOCOL'.freeze - SERVER_NAME = 'SERVER_NAME'.freeze - SERVER_ADDR = 'SERVER_ADDR'.freeze - SERVER_PORT = 'SERVER_PORT'.freeze - CACHE_CONTROL = 'Cache-Control'.freeze - CONTENT_LENGTH = 'Content-Length'.freeze - CONTENT_TYPE = 'Content-Type'.freeze - SET_COOKIE = 'Set-Cookie'.freeze - TRANSFER_ENCODING = 'Transfer-Encoding'.freeze - HTTP_COOKIE = 'HTTP_COOKIE'.freeze - ETAG = 'ETag'.freeze + HTTP_HOST = 'HTTP_HOST' + HTTP_VERSION = 'HTTP_VERSION' + HTTPS = 'HTTPS' + PATH_INFO = 'PATH_INFO' + REQUEST_METHOD = 'REQUEST_METHOD' + REQUEST_PATH = 'REQUEST_PATH' + SCRIPT_NAME = 'SCRIPT_NAME' + QUERY_STRING = 'QUERY_STRING' + SERVER_PROTOCOL = 'SERVER_PROTOCOL' + SERVER_NAME = 'SERVER_NAME' + SERVER_ADDR = 'SERVER_ADDR' + SERVER_PORT = 'SERVER_PORT' + CACHE_CONTROL = 'Cache-Control' + CONTENT_LENGTH = 'Content-Length' + CONTENT_TYPE = 'Content-Type' + SET_COOKIE = 'Set-Cookie' + TRANSFER_ENCODING = 'Transfer-Encoding' + HTTP_COOKIE = 'HTTP_COOKIE' + ETAG = 'ETag' # HTTP method verbs - GET = 'GET'.freeze - POST = 'POST'.freeze - PUT = 'PUT'.freeze - PATCH = 'PATCH'.freeze - DELETE = 'DELETE'.freeze - HEAD = 'HEAD'.freeze - OPTIONS = 'OPTIONS'.freeze - LINK = 'LINK'.freeze - UNLINK = 'UNLINK'.freeze - TRACE = 'TRACE'.freeze + GET = 'GET' + POST = 'POST' + PUT = 'PUT' + PATCH = 'PATCH' + DELETE = 'DELETE' + HEAD = 'HEAD' + OPTIONS = 'OPTIONS' + LINK = 'LINK' + UNLINK = 'UNLINK' + TRACE = 'TRACE' # Rack environment variables - RACK_VERSION = 'rack.version'.freeze - RACK_TEMPFILES = 'rack.tempfiles'.freeze - RACK_ERRORS = 'rack.errors'.freeze - RACK_LOGGER = 'rack.logger'.freeze - RACK_INPUT = 'rack.input'.freeze - RACK_SESSION = 'rack.session'.freeze - RACK_SESSION_OPTIONS = 'rack.session.options'.freeze - RACK_SHOWSTATUS_DETAIL = 'rack.showstatus.detail'.freeze - RACK_MULTITHREAD = 'rack.multithread'.freeze - RACK_MULTIPROCESS = 'rack.multiprocess'.freeze - RACK_RUNONCE = 'rack.run_once'.freeze - RACK_URL_SCHEME = 'rack.url_scheme'.freeze - RACK_HIJACK = 'rack.hijack'.freeze - RACK_IS_HIJACK = 'rack.hijack?'.freeze - RACK_HIJACK_IO = 'rack.hijack_io'.freeze - RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'.freeze - RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'.freeze - RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'.freeze - RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'.freeze - RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'.freeze - RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'.freeze - RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'.freeze - RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'.freeze - RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'.freeze - RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'.freeze - RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'.freeze - RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'.freeze + RACK_VERSION = 'rack.version' + RACK_TEMPFILES = 'rack.tempfiles' + RACK_ERRORS = 'rack.errors' + RACK_LOGGER = 'rack.logger' + RACK_INPUT = 'rack.input' + RACK_SESSION = 'rack.session' + RACK_SESSION_OPTIONS = 'rack.session.options' + RACK_SHOWSTATUS_DETAIL = 'rack.showstatus.detail' + RACK_MULTITHREAD = 'rack.multithread' + RACK_MULTIPROCESS = 'rack.multiprocess' + RACK_RUNONCE = 'rack.run_once' + RACK_URL_SCHEME = 'rack.url_scheme' + RACK_HIJACK = 'rack.hijack' + RACK_IS_HIJACK = 'rack.hijack?' + RACK_HIJACK_IO = 'rack.hijack_io' + RACK_RECURSIVE_INCLUDE = 'rack.recursive.include' + RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size' + RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory' + 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_COOKIE_HASH = 'rack.request.cookie_hash' + RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string' + RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash' + RACK_REQUEST_QUERY_STRING = 'rack.request.query_string' + RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method' + RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data' autoload :Builder, "rack/builder" autoload :BodyProxy, "rack/body_proxy" diff --git a/lib/rack/auth/abstract/handler.rb b/lib/rack/auth/abstract/handler.rb index c657691e..3ed87091 100644 --- a/lib/rack/auth/abstract/handler.rb +++ b/lib/rack/auth/abstract/handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Auth # Rack::Auth::AbstractHandler implements common authentication functionality. @@ -8,7 +10,7 @@ module Rack attr_accessor :realm - def initialize(app, realm=nil, &authenticator) + def initialize(app, realm = nil, &authenticator) @app, @realm, @authenticator = app, realm, authenticator end diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb index 80d1c272..23da4bf2 100644 --- a/lib/rack/auth/abstract/request.rb +++ b/lib/rack/auth/abstract/request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/request' module Rack @@ -13,7 +15,11 @@ module Rack end def provided? - !authorization_key.nil? + !authorization_key.nil? && valid? + end + + def valid? + !@env[authorization_key].nil? end def parts diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb index 9c589214..95bbafc4 100644 --- a/lib/rack/auth/basic.rb +++ b/lib/rack/auth/basic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/auth/abstract/handler' require 'rack/auth/abstract/request' @@ -45,7 +47,7 @@ module Rack end def credentials - @credentials ||= params.unpack("m*").first.split(/:/, 2) + @credentials ||= params.unpack("m*").first.split(':', 2) end def username diff --git a/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb index ddee35de..62bff984 100644 --- a/lib/rack/auth/digest/md5.rb +++ b/lib/rack/auth/digest/md5.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/auth/abstract/handler' require 'rack/auth/digest/request' require 'rack/auth/digest/params' @@ -21,7 +23,7 @@ module Rack attr_writer :passwords_hashed - def initialize(app, realm=nil, opaque=nil, &authenticator) + def initialize(app, realm = nil, opaque = nil, &authenticator) @passwords_hashed = nil if opaque.nil? and realm.respond_to? :values_at realm, opaque, @passwords_hashed = realm.values_at :realm, :opaque, :passwords_hashed @@ -47,7 +49,7 @@ module Rack if valid?(auth) if auth.nonce.stale? - return unauthorized(challenge(:stale => true)) + return unauthorized(challenge(stale: true)) else env['REMOTE_USER'] = auth.username @@ -61,7 +63,7 @@ module Rack private - QOP = 'auth'.freeze + QOP = 'auth' def params(hash = {}) Params.new do |params| @@ -106,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 57089cb3..6c1f28a3 100644 --- a/lib/rack/auth/digest/nonce.rb +++ b/lib/rack/auth/digest/nonce.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'digest/md5' module Rack diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb index 2b226e62..f611b3c3 100644 --- a/lib/rack/auth/digest/params.rb +++ b/lib/rack/auth/digest/params.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Auth module Digest @@ -38,12 +40,12 @@ module Rack def to_s map do |k, v| - "#{k}=" << (UNQUOTED.include?(k) ? v.to_s : quote(v)) + "#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}" end.join(', ') end def quote(str) # From WEBrick::HTTPUtils - '"' << str.gsub(/[\\\"]/o, "\\\1") << '"' + '"' + str.gsub(/[\\\"]/o, "\\\1") + '"' end end diff --git a/lib/rack/auth/digest/request.rb b/lib/rack/auth/digest/request.rb index 105c7674..a3ab4743 100644 --- a/lib/rack/auth/digest/request.rb +++ b/lib/rack/auth/digest/request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/auth/abstract/request' require 'rack/auth/digest/params' require 'rack/auth/digest/nonce' diff --git a/lib/rack/body_proxy.rb b/lib/rack/body_proxy.rb index 7fcfe316..15e4a84f 100644 --- a/lib/rack/body_proxy.rb +++ b/lib/rack/body_proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack class BodyProxy def initialize(body, &block) @@ -6,7 +8,7 @@ module Rack @closed = false end - def respond_to?(method_name, include_all=false) + def respond_to?(method_name, include_all = false) case method_name when :to_ary, 'to_ary' return false diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index 975cf1e1..dcd40c76 100644 --- a/lib/rack/builder.rb +++ b/lib/rack/builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack # Rack::Builder implements a small DSL to iteratively construct Rack # applications. @@ -29,29 +31,43 @@ 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) - options = {} - if config =~ /\.ru$/ - cfgfile = ::File.read(config) - if cfgfile[/^#\\(.*)/] && opts - options = opts.parse! $1.split(/\s+/) - end - cfgfile.sub!(/^__END__\n.*\Z/m, '') - app = new_from_string cfgfile, config + if config.end_with?('.ru') + return self.load_file(config, opts) else require config app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join('')) + return app, {} + end + end + + def self.load_file(path, opts = Server::Options.new) + options = {} + + cfgfile = ::File.read(path) + cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8 + + if cfgfile[/^#\\(.*)/] && opts + options = opts.parse! $1.split(/\s+/) end + + cfgfile.sub!(/^__END__\n.*\Z/m, '') + app = new_from_string cfgfile, path + return app, options end - def self.new_from_string(builder_script, file="(rackup)") + def self.new_from_string(builder_script, file = "(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end def initialize(default_app = nil, &block) - @use, @map, @run, @warmup = [], nil, default_app, nil + @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false instance_eval(&block) if block_given? end @@ -81,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 @@ -113,7 +129,7 @@ module Rack # # use SomeMiddleware # run MyApp - def warmup(prc=nil, &block) + def warmup(prc = nil, &block) @warmup = prc || block end @@ -141,10 +157,17 @@ module Rack @map[path] = block end + # Freeze the app (set using run) and all middleware instances when building the application + # in to_app. + def freeze_app + @freeze_app = true + end + def to_app app = @map ? generate_map(@run, @map) : @run fail "missing run or map statement" unless app - app = @use.reverse.inject(app) { |a,e| e[a] } + app.freeze if @freeze_app + app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } } @warmup.call(app) if @warmup app end @@ -156,8 +179,8 @@ module Rack private def generate_map(default_app, mapping) - mapped = default_app ? {'/' => default_app} : {} - mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app } + mapped = default_app ? { '/' => default_app } : {} + mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app } URLMap.new(mapped) end end diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb index 6b8f415a..76bc9a1a 100644 --- a/lib/rack/cascade.rb +++ b/lib/rack/cascade.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + module Rack # Rack::Cascade tries a request on several apps, and returns the # first response that is not 404 or 405 (or in a list of configurable # status codes). class Cascade - NotFound = [404, {CONTENT_TYPE => "text/plain"}, []] + NotFound = [404, { CONTENT_TYPE => "text/plain" }, []] attr_reader :apps - def initialize(apps, catch=[404, 405]) + def initialize(apps, catch = [404, 405]) @apps = []; @has_app = {} apps.each { |app| add app } diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb index 4b8f270e..0c8b4ae8 100644 --- a/lib/rack/chunked.rb +++ b/lib/rack/chunked.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/utils' module Rack @@ -10,7 +12,7 @@ module Rack # A body wrapper that emits chunked responses class Body TERM = "\r\n" - TAIL = "0#{TERM}#{TERM}" + TAIL = "0#{TERM}" include Rack::Utils @@ -18,21 +20,38 @@ module Rack @body = body end - def each + def each(&block) term = TERM @body.each do |chunk| size = chunk.bytesize next if size == 0 - chunk = chunk.dup.force_encoding(Encoding::BINARY) + chunk = chunk.b yield [size.to_s(16), term, chunk, term].join end yield TAIL + insert_trailers(&block) + yield TERM end def close @body.close if @body.respond_to?(:close) end + + private + + def insert_trailers(&block) + end + end + + class TrailerBody < Body + private + + def insert_trailers(&block) + @body.trailers.each_pair do |k, v| + yield "#{k}: #{v}\r\n" + end + end end def initialize(app) @@ -55,14 +74,18 @@ module Rack headers = HeaderHash.new(headers) if ! chunkable_version?(env[HTTP_VERSION]) || - STATUS_WITH_NO_ENTITY_BODY.include?(status) || + STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) || headers[CONTENT_LENGTH] || headers[TRANSFER_ENCODING] [status, headers, body] else headers.delete(CONTENT_LENGTH) headers[TRANSFER_ENCODING] = 'chunked' - [status, headers, Body.new(body)] + if headers['Trailer'] + [status, headers, TrailerBody.new(body)] + else + [status, headers, Body.new(body)] + end end end end diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb index ae410430..692b2070 100644 --- a/lib/rack/common_logger.rb +++ b/lib/rack/common_logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/body_proxy' module Rack @@ -23,13 +25,13 @@ module Rack # %{%s - %s [%s] "%s %s%s %s" %d %s\n} % FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} - def initialize(app, logger=nil) + def initialize(app, logger = nil) @app = app @logger = logger end def call(env) - began_at = Time.now + began_at = Utils.clock_time status, header, body = @app.call(env) header = Utils::HeaderHash.new(header) body = BodyProxy.new(body) { log(env, status, header, began_at) } @@ -39,20 +41,19 @@ module Rack private def log(env, status, header, began_at) - now = Time.now length = extract_content_length(header) msg = FORMAT % [ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", env["REMOTE_USER"] || "-", - now.strftime("%d/%b/%Y:%H:%M:%S %z"), + Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"), env[REQUEST_METHOD], env[PATH_INFO], env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}", env[HTTP_VERSION], status.to_s[0..3], length, - now - began_at ] + Utils.clock_time - began_at ] logger = @logger || env[RACK_ERRORS] # Standard library logger doesn't support write but it supports << which actually @@ -65,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/conditional_get.rb b/lib/rack/conditional_get.rb index 441dd382..bda8daf6 100644 --- a/lib/rack/conditional_get.rb +++ b/lib/rack/conditional_get.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/utils' module Rack @@ -68,7 +70,7 @@ module Rack # anything shorter is invalid, this avoids exceptions for common cases # most common being the empty string if since && since.length >= 16 - # NOTE: there is no trivial way to write this in a non execption way + # NOTE: there is no trivial way to write this in a non exception way # _rfc2822 returns a hash but is not that usable Time.rfc2822(since) rescue nil else diff --git a/lib/rack/config.rb b/lib/rack/config.rb index dc255d27..41f6f7dd 100644 --- a/lib/rack/config.rb +++ b/lib/rack/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack # Rack::Config modifies the environment using the block given during # initialization. diff --git a/lib/rack/content_length.rb b/lib/rack/content_length.rb index 2df7dfc8..e37fc305 100644 --- a/lib/rack/content_length.rb +++ b/lib/rack/content_length.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/utils' require 'rack/body_proxy' @@ -15,7 +17,7 @@ module Rack status, headers, body = @app.call(env) headers = HeaderHash.new(headers) - if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) && + if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && !headers[CONTENT_LENGTH] && !headers[TRANSFER_ENCODING] && body.respond_to?(:to_ary) diff --git a/lib/rack/content_type.rb b/lib/rack/content_type.rb index 78ba43b7..010cc37b 100644 --- a/lib/rack/content_type.rb +++ b/lib/rack/content_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/utils' module Rack @@ -19,7 +21,7 @@ module Rack status, headers, body = @app.call(env) headers = Utils::HeaderHash.new(headers) - unless STATUS_WITH_NO_ENTITY_BODY.include?(status) + unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) headers[CONTENT_TYPE] ||= @content_type end diff --git a/lib/rack/core_ext/regexp.rb b/lib/rack/core_ext/regexp.rb new file mode 100644 index 00000000..02975345 --- /dev/null +++ b/lib/rack/core_ext/regexp.rb @@ -0,0 +1,16 @@ +# 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 + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new(2.4) + refine Regexp do + def match?(string, pos = 0) + !!match(string, pos) + end unless //.respond_to?(:match?) + end + end + end +end diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 62a11243..939832ce 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -1,14 +1,17 @@ +# frozen_string_literal: true + require "zlib" require "time" # for Time.httpdate require 'rack/utils' +require_relative 'core_ext/regexp' + module Rack # This middleware enables compression of http responses. # # Currently supported compression algorithms: # # * gzip - # * deflate # * identity (no transformation) # # The middleware automatically detects when compression is supported @@ -16,19 +19,26 @@ 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. # # [app] rack app instance # [options] hash of deflater options, i.e. # 'if' - a lambda enabling / disabling deflation based on returned boolean value - # e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 } + # e.g use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 } # 'include' - a list of content types that should be compressed + # 'sync' - determines if the stream is going to be flused after every chunk. + # Flushing after every chunk reduces latency for + # time-sensitive streaming applications, but hurts + # compression and throughput. Defaults to `true'. def initialize(app, options = {}) @app = app @condition = options[:if] @compressible_types = options[:include] + @sync = options[:sync] == false ? false : true end def call(env) @@ -41,7 +51,7 @@ module Rack request = Request.new(env) - encoding = Utils.select_best_encoding(%w(gzip deflate identity), + encoding = Utils.select_best_encoding(%w(gzip identity), request.accept_encoding) # Set the Vary HTTP header. @@ -53,37 +63,34 @@ module Rack case encoding when "gzip" headers['Content-Encoding'] = "gzip" - headers.delete(CONTENT_LENGTH) - mtime = headers.key?("Last-Modified") ? - Time.httpdate(headers["Last-Modified"]) : Time.now - [status, headers, GzipStream.new(body, mtime)] - when "deflate" - headers['Content-Encoding'] = "deflate" - headers.delete(CONTENT_LENGTH) - [status, headers, DeflateStream.new(body)] + headers.delete('Content-Length') + mtime = headers["Last-Modified"] + mtime = Time.httpdate(mtime).to_i if mtime + [status, headers, GzipStream.new(body, mtime, @sync)] when "identity" [status, headers, body] when nil message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found." bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) } - [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp] + [406, { 'Content-Type' => "text/plain", 'Content-Length' => message.length.to_s }, bp] end end class GzipStream - def initialize(body, mtime) + def initialize(body, mtime, sync) + @sync = sync @body = body @mtime = mtime - @closed = false end def each(&block) @writer = block - gzip =::Zlib::GzipWriter.new(self) - gzip.mtime = @mtime + gzip = ::Zlib::GzipWriter.new(self) + gzip.mtime = @mtime if @mtime @body.each { |part| - gzip.write(part) - gzip.flush + len = gzip.write(part) + # Flushing empty parts would raise Zlib::BufError. + gzip.flush if @sync && len > 0 } ensure gzip.close @@ -95,39 +102,8 @@ module Rack end def close - return if @closed - @closed = true - @body.close if @body.respond_to?(:close) - end - end - - class DeflateStream - DEFLATE_ARGS = [ - Zlib::DEFAULT_COMPRESSION, - # drop the zlib header which causes both Safari and IE to choke - -Zlib::MAX_WBITS, - Zlib::DEF_MEM_LEVEL, - Zlib::DEFAULT_STRATEGY - ] - - def initialize(body) - @body = body - @closed = false - end - - def each - deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS) - @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) } - yield fin = deflator.finish - ensure - deflator.finish unless fin - deflator.close - end - - def close - return if @closed - @closed = true @body.close if @body.respond_to?(:close) + @body = nil end end @@ -136,14 +112,14 @@ module Rack def should_deflate?(env, status, headers, body) # Skip compressing empty entity body responses and responses with # no-transform set. - if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) || - headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ || + if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) || + /\bno-transform\b/.match?(headers['Cache-Control'].to_s) || (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/) return false end # Skip if @compressible_types are given and does not include request's content type - return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/])) + return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/])) # Skip if @condition lambda is given and evaluates to false return false if @condition && !@condition.call(env, status, headers, body) diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index 89cfe807..f0acc40d 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'time' require 'rack/utils' require 'rack/mime' @@ -41,9 +43,9 @@ table { width:100%%; } class DirectoryBody < Struct.new(:root, :path, :files) def each - show_path = Rack::Utils.escape_html(path.sub(/^#{root}/,'')) - listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n" - page = DIR_PAGE % [ show_path, show_path , listings ] + show_path = Rack::Utils.escape_html(path.sub(/^#{root}/, '')) + listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) } * "\n" + page = DIR_PAGE % [ show_path, show_path, listings ] page.each_line{|l| yield l } end @@ -56,7 +58,7 @@ table { width:100%%; } attr_reader :root, :path - def initialize(root, app=nil) + def initialize(root, app = nil) @root = ::File.expand_path(root) @app = app || Rack::File.new(@root) @head = Rack::Head.new(lambda { |env| get env }) @@ -86,9 +88,9 @@ table { width:100%%; } body = "Bad Request\n" size = body.bytesize - return [400, {CONTENT_TYPE => "text/plain", + return [400, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => size.to_s, - "X-Cascade" => "pass"}, [body]] + "X-Cascade" => "pass" }, [body]] end def check_forbidden(path_info) @@ -96,13 +98,13 @@ table { width:100%%; } body = "Forbidden\n" size = body.bytesize - return [403, {CONTENT_TYPE => "text/plain", + return [403, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => size.to_s, - "X-Cascade" => "pass"}, [body]] + "X-Cascade" => "pass" }, [body]] end def list_directory(path_info, path, script_name) - files = [['../','Parent Directory','','','']] + files = [['../', 'Parent Directory', '', '', '']] glob = ::File.join(path, '*') url_head = (script_name.split('/') + path_info.split('/')).map do |part| @@ -126,7 +128,7 @@ table { width:100%%; } files << [ url, basename, size, type, mtime ] end - return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, DirectoryBody.new(@root, path, files) ] + return [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, DirectoryBody.new(@root, path, files) ] end def stat(node) @@ -154,9 +156,9 @@ table { width:100%%; } def entity_not_found(path_info) body = "Entity not found: #{path_info}\n" size = body.bytesize - return [404, {CONTENT_TYPE => "text/plain", + return [404, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => size.to_s, - "X-Cascade" => "pass"}, [body]] + "X-Cascade" => "pass" }, [body]] end # Stolen from Ramaze diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb index a0041062..fd3de554 100644 --- a/lib/rack/etag.rb +++ b/lib/rack/etag.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'rack' -require 'digest/md5' +require 'digest/sha2' module Rack # Automatically sets the ETag header on all String bodies. @@ -13,7 +15,7 @@ module Rack # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate" class ETag ETAG_STRING = Rack::ETAG - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze + DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL) @app = app diff --git a/lib/rack/events.rb b/lib/rack/events.rb index 3782a22e..77b71675 100644 --- a/lib/rack/events.rb +++ b/lib/rack/events.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + require 'rack/response' require 'rack/body_proxy' module Rack ### This middleware provides hooks to certain places in the request / - #response lifecycle. This is so that middleware that don't need to filter - #the response data can safely leave it alone and not have to send messages - #down the traditional "rack stack". + # response lifecycle. This is so that middleware that don't need to filter + # the response data can safely leave it alone and not have to send messages + # down the traditional "rack stack". # # The events are: # diff --git a/lib/rack/file.rb b/lib/rack/file.rb index 0a257b3d..425c1d38 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'time' require 'rack/utils' require 'rack/mime' @@ -19,8 +21,8 @@ module Rack attr_reader :root - def initialize(root, headers={}, default_mime = 'text/plain') - @root = 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 }) @@ -34,7 +36,7 @@ module Rack def get(env) request = Rack::Request.new env unless ALLOWED_VERBS.include? request.request_method - return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER}) + return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER }) end path_info = Utils.unescape_path request.path_info @@ -58,7 +60,7 @@ module Rack def serving(request, path) if request.options? - return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []] + 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 @@ -80,7 +82,7 @@ module Rack # No ranges, or multiple ranges (which we don't support): # TODO: Support multiple byte-ranges response[0] = 200 - range = 0..size-1 + range = 0..size - 1 elsif ranges.empty? # Unsatisfiable. Return error, and file size: response = fail(416, "Byte range unsatisfiable") @@ -113,7 +115,7 @@ module Rack def each ::File.open(path, "rb") do |file| file.seek(range.begin) - remaining_len = range.end-range.begin+1 + remaining_len = range.end - range.begin + 1 while remaining_len > 0 part = file.read([8192, remaining_len].min) break unless part @@ -158,7 +160,7 @@ module Rack def filesize path # If response_body is present, use its size. - return Rack::Utils.bytesize(response_body) if response_body + 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 diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb index 70a77fa9..bc0a3bf8 100644 --- a/lib/rack/handler.rb +++ b/lib/rack/handler.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack # *Handlers* connect web servers with Rack. # @@ -52,7 +54,7 @@ module Rack elsif ENV.include?("RACK_HANDLER") self.get(ENV["RACK_HANDLER"]) else - pick ['puma', 'thin', 'webrick'] + pick ['puma', 'thin', 'falcon', 'webrick'] end end diff --git a/lib/rack/handler/cgi.rb b/lib/rack/handler/cgi.rb index 52807694..a223c545 100644 --- a/lib/rack/handler/cgi.rb +++ b/lib/rack/handler/cgi.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'rack/content_length' require 'rack/rewindable_input' module Rack module Handler class CGI - def self.run(app, options=nil) + def self.run(app, options = nil) $stdin.binmode serve app end diff --git a/lib/rack/handler/fastcgi.rb b/lib/rack/handler/fastcgi.rb index e918dc94..b3f825da 100644 --- a/lib/rack/handler/fastcgi.rb +++ b/lib/rack/handler/fastcgi.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fcgi' require 'socket' require 'rack/content_length' @@ -7,7 +9,7 @@ if defined? FCGI::Stream class FCGI::Stream alias _rack_read_without_buffer read - def read(n, buffer=nil) + def read(n, buffer = nil) buf = _rack_read_without_buffer n buffer.replace(buf.to_s) if buffer buf @@ -18,7 +20,7 @@ end module Rack module Handler class FastCGI - def self.run(app, options={}) + def self.run(app, options = {}) if options[:File] STDIN.reopen(UNIXServer.new(options[:File])) elsif options[:Port] diff --git a/lib/rack/handler/lsws.rb b/lib/rack/handler/lsws.rb index d2cfd793..803182a2 100644 --- a/lib/rack/handler/lsws.rb +++ b/lib/rack/handler/lsws.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'lsapi' require 'rack/content_length' require 'rack/rewindable_input' @@ -5,7 +7,7 @@ require 'rack/rewindable_input' module Rack module Handler class LSWS - def self.run(app, options=nil) + def self.run(app, options = nil) while LSAPI.accept != nil serve app end diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb index e056a01d..c8e91606 100644 --- a/lib/rack/handler/scgi.rb +++ b/lib/rack/handler/scgi.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'scgi' require 'stringio' require 'rack/content_length' @@ -8,12 +10,12 @@ module Rack class SCGI < ::SCGI::Processor attr_accessor :app - def self.run(app, options=nil) + def self.run(app, options = nil) options[:Socket] = UNIXServer.new(options[:File]) if options[:File] - new(options.merge(:app=>app, - :host=>options[:Host], - :port=>options[:Port], - :socket=>options[:Socket])).listen + new(options.merge(app: app, + host: options[:Host], + port: options[:Port], + socket: options[:Socket])).listen end def self.valid_options @@ -41,7 +43,8 @@ module Rack env[QUERY_STRING] ||= "" env[SCRIPT_NAME] = "" - rack_input = StringIO.new(input_body, encoding: Encoding::BINARY) + rack_input = StringIO.new(input_body) + rack_input.set_encoding(Encoding::BINARY) env.update( RACK_VERSION => Rack::VERSION, diff --git a/lib/rack/handler/thin.rb b/lib/rack/handler/thin.rb index ca880646..100dfd11 100644 --- a/lib/rack/handler/thin.rb +++ b/lib/rack/handler/thin.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "thin" require "thin/server" require "thin/logging" @@ -8,7 +10,7 @@ require "rack/chunked" module Rack module Handler class Thin - def self.run(app, options={}) + def self.run(app, options = {}) environment = ENV['RACK_ENV'] || 'development' default_host = environment == 'development' ? 'localhost' : '0.0.0.0' diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb index 95aa8927..4affdbde 100644 --- a/lib/rack/handler/webrick.rb +++ b/lib/rack/handler/webrick.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'webrick' require 'stringio' require 'rack/content_length' @@ -22,7 +24,7 @@ end module Rack module Handler class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet - def self.run(app, options={}) + def self.run(app, options = {}) environment = ENV['RACK_ENV'] || 'development' default_host = environment == 'development' ? 'localhost' : nil @@ -79,17 +81,18 @@ module Rack env[QUERY_STRING] ||= "" unless env[PATH_INFO] == "" path, n = req.request_uri.path, env[SCRIPT_NAME].length - env[PATH_INFO] = path[n, path.length-n] + env[PATH_INFO] = path[n, path.length - n] end env[REQUEST_PATH] ||= [env[SCRIPT_NAME], env[PATH_INFO]].join status, headers, body = @app.call(env) begin res.status = status.to_i + io_lambda = nil headers.each { |k, vs| - next if k.downcase == RACK_HIJACK - - if k.downcase == "set-cookie" + if k == RACK_HIJACK + io_lambda = vs + elsif k.downcase == "set-cookie" res.cookies.concat vs.split("\n") else # Since WEBrick won't accept repeated headers, @@ -98,7 +101,6 @@ module Rack end } - io_lambda = headers[RACK_HIJACK] if io_lambda rd, wr = IO.pipe res.body = rd diff --git a/lib/rack/head.rb b/lib/rack/head.rb index 6f1d7472..c257ae4d 100644 --- a/lib/rack/head.rb +++ b/lib/rack/head.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/body_proxy' module Rack diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb index 54d37822..17b188f4 100644 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/utils' require 'forwardable' @@ -33,7 +35,7 @@ module Rack ## A Rack application is a Ruby object (not a class) that ## responds to +call+. - def call(env=nil) + def call(env = nil) dup._call(env) end @@ -123,9 +125,8 @@ module Rack ## the presence or absence of the ## appropriate HTTP header in the ## request. See - ## <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18"> - ## RFC3875 section 4.1.18</a> for - ## specific behavior. + ## {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18] + ## for specific behavior. ## In addition to this, the Rack environment must include these ## Rack-specific variables: @@ -263,7 +264,7 @@ module Rack ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt> ## (use the versions without <tt>HTTP_</tt>). %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header| - assert("env contains #{header}, must use #{header[5,-1]}") { + assert("env contains #{header}, must use #{header[5, -1]}") { not env.include? header } } @@ -659,10 +660,10 @@ module Rack def check_content_type(status, headers) headers.each { |key, value| ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, - ## 204, 205 or 304. + ## 204 or 304. if key.downcase == "content-type" assert("Content-Type header found in #{status} response, not allowed") { - not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i } return end @@ -674,9 +675,9 @@ module Rack headers.each { |key, value| if key.downcase == 'content-length' ## There must not be a <tt>Content-Length</tt> header when the - ## +Status+ is 1xx, 204, 205 or 304. + ## +Status+ is 1xx, 204 or 304. assert("Content-Length header found in #{status} response, not allowed") { - not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i } @content_length = value end diff --git a/lib/rack/lobster.rb b/lib/rack/lobster.rb index 4d6e39f2..77b607c3 100644 --- a/lib/rack/lobster.rb +++ b/lib/rack/lobster.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'zlib' require 'rack/request' @@ -25,8 +27,8 @@ module Rack content = ["<title>Lobstericious!</title>", "<pre>", lobster, "</pre>", "<a href='#{href}'>flip!</a>"] - length = content.inject(0) { |a,e| a+e.size }.to_s - [200, {CONTENT_TYPE => "text/html", CONTENT_LENGTH => length}, content] + length = content.inject(0) { |a, e| a + e.size }.to_s + [200, { CONTENT_TYPE => "text/html", CONTENT_LENGTH => length }, content] } def call(env) @@ -37,8 +39,8 @@ module Rack gsub('\\', 'TEMP'). gsub('/', '\\'). gsub('TEMP', '/'). - gsub('{','}'). - gsub('(',')') + gsub('{', '}'). + gsub('(', ')') end.join("\n") href = "?flip=right" elsif req.GET["flip"] == "crash" @@ -65,6 +67,6 @@ if $0 == __FILE__ require 'rack' require 'rack/show_exceptions' Rack::Server.start( - :app => Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), :Port => 9292 + app: Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), Port: 9292 ) end diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb index 923dca59..96366cd3 100644 --- a/lib/rack/lock.rb +++ b/lib/rack/lock.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'thread' require 'rack/body_proxy' @@ -11,12 +13,21 @@ module Rack def call(env) @mutex.lock + @env = env + @old_rack_multithread = env[RACK_MULTITHREAD] begin - response = @app.call(env.merge(RACK_MULTITHREAD => false)) - returned = response << BodyProxy.new(response.pop) { @mutex.unlock } + response = @app.call(env.merge!(RACK_MULTITHREAD => false)) + returned = response << BodyProxy.new(response.pop) { unlock } ensure - @mutex.unlock unless returned + unlock unless returned end end + + private + + def unlock + @mutex.unlock + @env[RACK_MULTITHREAD] = @old_rack_multithread + end end end diff --git a/lib/rack/logger.rb b/lib/rack/logger.rb index 01fc321c..6c4bede0 100644 --- a/lib/rack/logger.rb +++ b/lib/rack/logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'logger' module Rack diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb index f9397453..41937c99 100644 --- a/lib/rack/media_type.rb +++ b/lib/rack/media_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack # Rack::MediaType parse media type and parameters out of content_type string @@ -13,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 @@ -23,9 +25,12 @@ 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 diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb index f5637771..453901fc 100644 --- a/lib/rack/method_override.rb +++ b/lib/rack/method_override.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module Rack class MethodOverride HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK] - METHOD_OVERRIDE_PARAM_KEY = "_method".freeze - HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze + METHOD_OVERRIDE_PARAM_KEY = "_method" + HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE" ALLOWED_METHODS = %w[POST] def initialize(app) @@ -26,7 +28,11 @@ module Rack req = Request.new(env) method = method_override_param(req) || env[HTTP_METHOD_OVERRIDE_HEADER] - method.to_s.upcase + begin + method.to_s.upcase + rescue ArgumentError + env[RACK_ERRORS].puts "Invalid string for method" + end end private @@ -38,6 +44,9 @@ module Rack def method_override_param(req) req.POST[METHOD_OVERRIDE_PARAM_KEY] rescue Utils::InvalidParameterError, Utils::ParameterTypeError + req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params" + rescue EOFError + req.get_header(RACK_ERRORS).puts "Bad request content body" end end end diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb index d82dc131..f6c02c1f 100644 --- a/lib/rack/mime.rb +++ b/lib/rack/mime.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Mime # Returns String with mime type if found, otherwise use +fallback+. @@ -13,7 +15,7 @@ module Rack # This is a shortcut for: # Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream') - def mime_type(ext, fallback='application/octet-stream') + def mime_type(ext, fallback = 'application/octet-stream') MIME_TYPES.fetch(ext.to_s.downcase, fallback) end module_function :mime_type @@ -306,6 +308,7 @@ module Rack ".lvp" => "audio/vnd.lucent.voice", ".lwp" => "application/vnd.lotus-wordpro", ".m3u" => "audio/x-mpegurl", + ".m3u8" => "application/x-mpegurl", ".m4a" => "audio/mp4a-latm", ".m4v" => "video/mp4", ".ma" => "application/mathematica", @@ -343,6 +346,7 @@ module Rack ".mp4s" => "application/mp4", ".mp4v" => "video/mp4", ".mpc" => "application/vnd.mophun.certificate", + ".mpd" => "application/dash+xml", ".mpeg" => "video/mpeg", ".mpg" => "video/mpeg", ".mpga" => "audio/mpeg", @@ -542,6 +546,7 @@ module Rack ".spp" => "application/scvp-vp-response", ".spq" => "application/scvp-vp-request", ".src" => "application/x-wais-source", + ".srt" => "text/srt", ".srx" => "application/sparql-results+xml", ".sse" => "application/vnd.kodak-descriptor", ".ssf" => "application/vnd.epson.ssf", @@ -576,6 +581,7 @@ module Rack ".tr" => "text/troff", ".tra" => "application/vnd.trueapp", ".trm" => "application/x-msterminal", + ".ts" => "video/mp2t", ".tsv" => "text/tab-separated-values", ".ttf" => "application/octet-stream", ".twd" => "application/vnd.simtech-mindmapper", @@ -600,9 +606,11 @@ module Rack ".vrml" => "model/vrml", ".vsd" => "application/vnd.visio", ".vsf" => "application/vnd.vsf", + ".vtt" => "text/vtt", ".vtu" => "model/vnd.vtu", ".vxml" => "application/voicexml+xml", ".war" => "application/java-archive", + ".wasm" => "application/wasm", ".wav" => "audio/x-wav", ".wax" => "audio/x-ms-wax", ".wbmp" => "image/vnd.wap.wbmp", diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb index 4ebc4df1..f2b94832 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'uri' require 'stringio' require 'rack' require 'rack/lint' require 'rack/utils' require 'rack/response' +require 'cgi/cookie' module Rack # Rack::MockRequest helps testing your Rack application without @@ -53,16 +56,16 @@ module Rack @app = app end - def get(uri, opts={}) request(GET, uri, opts) end - def post(uri, opts={}) request(POST, uri, opts) end - def put(uri, opts={}) request(PUT, uri, opts) end - def patch(uri, opts={}) request(PATCH, uri, opts) end - def delete(uri, opts={}) request(DELETE, uri, opts) end - def head(uri, opts={}) request(HEAD, uri, opts) end - def options(uri, opts={}) request(OPTIONS, uri, opts) end + def get(uri, opts = {}) request(GET, uri, opts) end + def post(uri, opts = {}) request(POST, uri, opts) end + def put(uri, opts = {}) request(PUT, uri, opts) end + def patch(uri, opts = {}) request(PATCH, uri, opts) end + def delete(uri, opts = {}) request(DELETE, uri, opts) end + def head(uri, opts = {}) request(HEAD, uri, opts) end + def options(uri, opts = {}) request(OPTIONS, uri, opts) end - def request(method=GET, uri="", opts={}) - env = self.class.env_for(uri, opts.merge(:method => method)) + def request(method = GET, uri = "", opts = {}) + env = self.class.env_for(uri, opts.merge(method: method)) if opts[:lint] app = Rack::Lint.new(@app) @@ -71,7 +74,7 @@ module Rack end errors = env[RACK_ERRORS] - status, headers, body = app.call(env) + status, headers, body = app.call(env) MockResponse.new(status, headers, body, errors) ensure body.close if body.respond_to?(:close) @@ -85,19 +88,19 @@ module Rack end # Return the Rack environment used for a request to +uri+. - def self.env_for(uri="", opts={}) + def self.env_for(uri = "", opts = {}) uri = parse_uri_rfc2396(uri) uri.path = "/#{uri.path}" unless uri.path[0] == ?/ env = DEFAULT_ENV.dup - env[REQUEST_METHOD] = opts[:method] ? opts[:method].to_s.upcase : GET - env[SERVER_NAME] = uri.host || "example.org" - env[SERVER_PORT] = uri.port ? uri.port.to_s : "80" - env[QUERY_STRING] = uri.query.to_s - env[PATH_INFO] = (!uri.path || uri.path.empty?) ? "/" : uri.path - env[RACK_URL_SCHEME] = uri.scheme || "http" - env[HTTPS] = env[RACK_URL_SCHEME] == "https" ? "on" : "off" + env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b + env[SERVER_NAME] = (uri.host || "example.org").b + env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b + env[QUERY_STRING] = (uri.query.to_s).b + env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b + env[RACK_URL_SCHEME] = (uri.scheme || "http").b + env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b env[SCRIPT_NAME] = opts[:script_name] || "" @@ -128,7 +131,7 @@ module Rack end end - empty_str = String.new.force_encoding(Encoding::ASCII_8BIT) + empty_str = String.new opts[:input] ||= empty_str if String === opts[:input] rack_input = StringIO.new(opts[:input]) @@ -139,7 +142,7 @@ module Rack rack_input.set_encoding(Encoding::BINARY) env[RACK_INPUT] = rack_input - env["CONTENT_LENGTH"] ||= env[RACK_INPUT].length.to_s + env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size) opts.each { |field, value| env[field] = value if String === field @@ -155,14 +158,15 @@ module Rack class MockResponse < Rack::Response # Headers - attr_reader :original_headers + attr_reader :original_headers, :cookies # Errors attr_accessor :errors - def initialize(status, headers, body, errors=StringIO.new("")) + def initialize(status, headers, body, errors = StringIO.new("")) @original_headers = headers @errors = errors.string if errors.respond_to?(:string) + @cookies = parse_cookies_from_header super(body, status, headers) end @@ -190,7 +194,54 @@ module Rack end def empty? - [201, 204, 205, 304].include? status + [201, 204, 304].include? status + end + + def cookie(name) + cookies.fetch(name, nil) + end + + private + + def parse_cookies_from_header + cookies = Hash.new + if original_headers.has_key? 'Set-Cookie' + set_cookie_header = original_headers.fetch('Set-Cookie') + set_cookie_header.split("\n").each do |cookie| + cookie_name, cookie_filling = cookie.split('=', 2) + cookie_attributes = identify_cookie_attributes cookie_filling + parsed_cookie = CGI::Cookie.new( + 'name' => cookie_name.strip, + 'value' => cookie_attributes.fetch('value'), + 'path' => cookie_attributes.fetch('path', nil), + 'domain' => cookie_attributes.fetch('domain', nil), + 'expires' => cookie_attributes.fetch('expires', nil), + 'secure' => cookie_attributes.fetch('secure', false) + ) + cookies.store(cookie_name, parsed_cookie) + end + end + cookies end + + def identify_cookie_attributes(cookie_filling) + cookie_bits = cookie_filling.split(';') + cookie_attributes = Hash.new + cookie_attributes.store('value', cookie_bits[0].strip) + cookie_bits.each do |bit| + if bit.include? '=' + cookie_attribute, attribute_value = bit.split('=') + cookie_attributes.store(cookie_attribute.strip, attribute_value.strip) + if cookie_attribute.include? 'max-age' + cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i) + end + end + if bit.include? 'secure' + cookie_attributes.store('secure', true) + end + end + cookie_attributes + end + end end diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb index db59ee59..31ac29eb 100644 --- a/lib/rack/multipart.rb +++ b/lib/rack/multipart.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/multipart/parser' module Rack diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb index f0b70a8d..9ed2bb07 100644 --- a/lib/rack/multipart/generator.rb +++ b/lib/rack/multipart/generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Multipart class Generator @@ -27,21 +29,18 @@ module Rack private def multipart? - multipart = false - query = lambda { |value| case value when Array - value.each(&query) + value.any?(&query) when Hash - value.values.each(&query) + value.values.any?(&query) when Rack::Multipart::UploadedFile - multipart = true + true end } - @params.values.each(&query) - multipart + @params.values.any?(&query) end def flattened_params diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index f661da10..7c38d5f3 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -1,16 +1,24 @@ +# frozen_string_literal: true + require 'rack/utils' +require 'strscan' +require 'rack/core_ext/regexp' module Rack module Multipart class MultipartPartLimitError < Errno::EMFILE; end class Parser - BUFSIZE = 16384 + using ::Rack::RegexpExtensions + + BUFSIZE = 1_048_576 TEXT_PLAIN = "text/plain" TEMPFILE_FACTORY = lambda { |filename, content_type| - Tempfile.new(["RackMultipart", ::File.extname(filename)]) + Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))]) } + BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/ + class BoundedIO # :nodoc: def initialize(io, content_length) @io = io @@ -18,15 +26,15 @@ module Rack @cursor = 0 end - def read(size) + def read(size, outbuf = nil) return if @cursor >= @content_length left = @content_length - @cursor str = if left < size - @io.read left + @io.read left, outbuf else - @io.read size + @io.read size, outbuf end if str @@ -39,8 +47,6 @@ module Rack str end - def eof?; @content_length == @cursor; end - def rewind @io.rewind end @@ -63,13 +69,14 @@ module Rack return EMPTY unless boundary io = BoundedIO.new(io, content_length) if content_length + outbuf = String.new parser = new(boundary, tmpfile, bufsize, qp) - parser.on_read io.read(bufsize), io.eof? + parser.on_read io.read(bufsize, outbuf) loop do break if parser.state == :DONE - parser.on_read io.read(bufsize), io.eof? + parser.on_read io.read(bufsize, outbuf) end io.rewind @@ -92,14 +99,14 @@ module Rack # those which give the lone filename. fn = filename.split(/[\/\\]/).last - data = {:filename => fn, :type => content_type, - :name => name, :tempfile => body, :head => head} + data = { filename: fn, type: content_type, + name: name, tempfile: body, head: head } elsif !filename && content_type && body.is_a?(IO) body.rewind # Generic multipart cases, not coming from a form - data = {:type => content_type, - :name => name, :tempfile => body, :head => head} + data = { type: content_type, + name: name, tempfile: body, head: head } end yield data @@ -135,11 +142,12 @@ module Rack klass = TempfilePart @open_files += 1 else - body = ''.force_encoding(Encoding::ASCII_8BIT) + body = String.new klass = BufferPart end @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name) + check_open_files end @@ -165,25 +173,26 @@ module Rack attr_reader :state def initialize(boundary, tempfile, bufsize, query_parser) - @buf = "".force_encoding(Encoding::ASCII_8BIT) - @query_parser = query_parser @params = query_parser.make_params @boundary = "--#{boundary}" - @boundary_size = @boundary.bytesize + EOL.size @bufsize = bufsize - @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n @full_boundary = @boundary @end_boundary = @boundary + '--' @state = :FAST_FORWARD @mime_index = 0 @collector = Collector.new tempfile + + @sbuf = StringScanner.new("".dup) + @body_regex = /(.*?)(#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/m + @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max + @head_regex = /(.*?#{EOL})#{EOL}/m end - def on_read content, eof - handle_empty_content!(content, eof) - @buf << content + def on_read content + handle_empty_content!(content) + @sbuf.concat content run_parser end @@ -194,7 +203,6 @@ module Rack @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit) end end - MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body) end @@ -221,7 +229,7 @@ module Rack if consume_boundary @state = :MIME_HEAD else - raise EOFError, "bad content body" if @buf.bytesize >= @bufsize + raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize :want_read end end @@ -229,19 +237,16 @@ module Rack def handle_consume_token tok = consume_boundary # break if we're at the end of a buffer, but not if it is the end of a field - if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY) - @state = :DONE + @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY) + :DONE else - @state = :MIME_HEAD + :MIME_HEAD end end def handle_mime_head - if @buf.index(EOL + EOL) - i = @buf.index(EOL+EOL) - head = @buf.slice!(0, i+2) # First \r\n - @buf.slice!(0, 2) # Second \r\n - + if @sbuf.scan_until(@head_regex) + head = @sbuf[1] content_type = head[MULTIPART_CONTENT_TYPE, 1] if name = head[MULTIPART_CONTENT_DISPOSITION, 1] name = Rack::Auth::Digest::Params::dequote(name) @@ -252,7 +257,7 @@ module Rack filename = get_filename(head) if name.nil? || name.empty? - name = filename || "#{content_type || TEXT_PLAIN}[]" + name = filename || "#{content_type || TEXT_PLAIN}[]".dup end @collector.on_mime_head @mime_index, head, filename, content_type, name @@ -263,31 +268,33 @@ module Rack end def handle_mime_body - if @buf =~ rx - # Save the rest. - if i = @buf.index(rx) - @collector.on_mime_body @mime_index, @buf.slice!(0, i) - @buf.slice!(0, 2) # Remove \r\n after the content - end + if @sbuf.check_until(@body_regex) # check but do not advance the pointer yet + body = @sbuf[1] + @collector.on_mime_body @mime_index, body + @sbuf.pos += body.length + 2 # skip \r\n after the content @state = :CONSUME_TOKEN @mime_index += 1 else + # Save what we have so far + if @rx_max_size < @sbuf.rest_size + 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 end def full_boundary; @full_boundary; end - def rx; @rx; end - def consume_boundary - while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '') - read_buffer = $1 + while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX) case read_buffer.strip when full_boundary then return :BOUNDARY when @end_boundary then return :END_BOUNDARY end - return if @buf.empty? + return if @sbuf.eos? end end @@ -308,8 +315,8 @@ module Rack return unless filename - if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ } - filename = Utils.unescape(filename) + if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) } + filename = Utils.unescape_path(filename) end filename.scrub! @@ -325,7 +332,7 @@ module Rack filename end - CHARSET = "charset" + CHARSET = "charset" def tag_multipart_encoding(filename, content_type, name, body) name = name.to_s @@ -342,7 +349,7 @@ module Rack if TEXT_PLAIN == type_subtype rest = list.drop 1 rest.each do |param| - k,v = param.split('=', 2) + k, v = param.split('=', 2) k.strip! v.strip! v = v[1..-2] if v.start_with?('"') && v.end_with?('"') @@ -355,11 +362,9 @@ module Rack body.force_encoding(encoding) end - - def handle_empty_content!(content, eof) + def handle_empty_content!(content) if content.nil? || content.empty? - raise EOFError if eof - return true + raise EOFError end end end diff --git a/lib/rack/multipart/uploaded_file.rb b/lib/rack/multipart/uploaded_file.rb index 924b1f08..d01f2d6f 100644 --- a/lib/rack/multipart/uploaded_file.rb +++ b/lib/rack/multipart/uploaded_file.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Multipart class UploadedFile diff --git a/lib/rack/null_logger.rb b/lib/rack/null_logger.rb index abc61206..3eff73d6 100644 --- a/lib/rack/null_logger.rb +++ b/lib/rack/null_logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack class NullLogger def initialize(app) diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb index 17b77128..fce1ce91 100644 --- a/lib/rack/query_parser.rb +++ b/lib/rack/query_parser.rb @@ -1,5 +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 } @@ -36,7 +42,7 @@ module Rack (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p| next if p.empty? - k, v = p.split('='.freeze, 2).map!(&unescaper) + k, v = p.split('=', 2).map!(&unescaper) if cur = params[k] if cur.class == Array @@ -61,8 +67,8 @@ module Rack return {} if qs.nil? || qs.empty? params = make_params - (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p| - k, v = p.split('='.freeze, 2).map! { |s| unescape(s) } + qs.split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p| + k, v = p.split('=', 2).map! { |s| unescape(s) } normalize_params(params, k, v, param_depth_limit) end @@ -79,22 +85,22 @@ module Rack raise RangeError if depth <= 0 name =~ %r(\A[\[\]]*([^\[\]]+)\]*) - k = $1 || ''.freeze - after = $' || ''.freeze + k = $1 || '' + after = $' || '' if k.empty? - if !v.nil? && name == "[]".freeze + if !v.nil? && name == "[]" return Array(v) else return end end - if after == ''.freeze + if after == '' params[k] = v - elsif after == "[".freeze + elsif after == "[" params[name] = v - elsif after == "[]".freeze + elsif after == "[]" params[k] ||= [] raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) params[k] << v @@ -102,8 +108,7 @@ module Rack child_key = $1 params[k] ||= [] raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) - first_key = child_key.gsub(/[\[\]]/, ' ').split.first - if params_hash_type?(params[k].last) && !params[k].last.key?(first_key) + if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key) normalize_params(params[k].last, child_key, v, depth - 1) else params[k] << normalize_params(make_params, child_key, v, depth - 1) @@ -135,6 +140,18 @@ module Rack obj.kind_of?(@params_class) end + def params_hash_has_key?(hash, key) + return false if /\[\]/.match?(key) + + key.split(/[\[\]]+/).inject(hash) do |h, part| + next h if part == '' + return false unless params_hash_type?(h) && h.key?(part) + h[part] + end + + true + end + def unescape(s) Utils.unescape(s) end diff --git a/lib/rack/recursive.rb b/lib/rack/recursive.rb index 7645d284..6a94ca83 100644 --- a/lib/rack/recursive.rb +++ b/lib/rack/recursive.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'uri' module Rack @@ -10,14 +12,14 @@ module Rack class ForwardRequest < Exception attr_reader :url, :env - def initialize(url, env={}) + def initialize(url, env = {}) @url = URI(url) @env = env - @env[PATH_INFO] = @url.path - @env[QUERY_STRING] = @url.query if @url.query - @env[HTTP_HOST] = @url.host if @url.host - @env["HTTP_PORT"] = @url.port if @url.port + @env[PATH_INFO] = @url.path + @env[QUERY_STRING] = @url.query if @url.query + @env[HTTP_HOST] = @url.host if @url.host + @env["HTTP_PORT"] = @url.port if @url.port @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme super "forwarding to #{url}" diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb index 296dd6a1..e23ed1fb 100644 --- a/lib/rack/reloader.rb +++ b/lib/rack/reloader.rb @@ -1,9 +1,13 @@ -# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com -# Rack::Reloader is subject to the terms of an MIT-style license. -# See COPYING or http://www.opensource.org/licenses/mit-license.php. +# frozen_string_literal: true + +# Copyright (C) 2009-2018 Michael Fellinger <m.fellinger@gmail.com> +# Rack::Reloader is subject to the terms of an MIT-style license. +# See MIT-LICENSE or https://opensource.org/licenses/MIT. require 'pathname' +require_relative 'core_ext/regexp' + module Rack # High performant source reloader @@ -20,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 @@ -69,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 e2d2bb56..54ea86c4 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + 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 @@ -11,6 +15,19 @@ module Rack # req.params["data"] class Request + using ::Rack::RegexpExtensions + + class << self + attr_accessor :ip_filter + end + + 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) + deprecate_constant :SCHEME_WHITELIST + end + def initialize(env) @params = nil super(env) @@ -98,7 +115,7 @@ module Rack module Helpers # The set of form-data media-types. Requests that do not indicate - # one of the media types presents in this list will not be eligible + # one of the media types present in this list will not be eligible # for form-data / param parsing. FORM_DATA_MEDIA_TYPES = [ 'application/x-www-form-urlencoded', @@ -106,7 +123,7 @@ module Rack ] # The set of media-types. Requests that do not indicate - # one of the media types presents in this list will not be eligible + # one of the media types present in this list will not be eligible # for param parsing like soap attachments or generic multiparts PARSEABLE_DATA_MEDIA_TYPES = [ 'multipart/related', @@ -117,11 +134,11 @@ module Rack # to include the port in a generated URI. DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 } - HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze - HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze - HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'.freeze - HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'.freeze - HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'.freeze + HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME' + HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO' + HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST' + HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT' + HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL' def body; get_header(RACK_INPUT) end def script_name; get_header(SCRIPT_NAME).to_s end @@ -157,10 +174,10 @@ module Rack def delete?; request_method == DELETE end # Checks the HTTP request method (or verb) to see if it was of type GET - def get?; request_method == GET end + def get?; request_method == GET end # Checks the HTTP request method (or verb) to see if it was of type HEAD - def head?; request_method == HEAD end + def head?; request_method == HEAD end # Checks the HTTP request method (or verb) to see if it was of type OPTIONS def options?; request_method == OPTIONS end @@ -188,10 +205,8 @@ module Rack 'https' elsif get_header(HTTP_X_FORWARDED_SSL) == 'on' 'https' - elsif get_header(HTTP_X_FORWARDED_SCHEME) - get_header(HTTP_X_FORWARDED_SCHEME) - elsif get_header(HTTP_X_FORWARDED_PROTO) - get_header(HTTP_X_FORWARDED_PROTO).split(',')[0] + elsif forwarded_scheme + forwarded_scheme else get_header(RACK_URL_SCHEME) end @@ -208,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 @@ -232,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 @@ -260,8 +280,9 @@ module Rack return remote_addrs.first if remote_addrs.any? forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR')) + .map { |ip| strip_port(ip) } - return reject_trusted_ip_addresses(forwarded_ips).last || get_header("REMOTE_ADDR") + return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR") end # The media type (type/subtype) portion of the CONTENT_TYPE header @@ -386,12 +407,13 @@ 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 url = "#{scheme}://#{host}" - url << ":#{port}" if port != DEFAULT_PORTS[scheme] + url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme] url end @@ -417,12 +439,12 @@ module Rack end def trusted_proxy?(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 + Rack::Request.ip_filter.call(ip) end # shortcut for <tt>request.params[key]</tt> def [](key) - if $verbose + if $VERBOSE warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead") end @@ -433,7 +455,7 @@ module Rack # # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params. def []=(key, value) - if $verbose + if $VERBOSE warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead") end @@ -464,7 +486,7 @@ module Rack Utils.default_query_parser end - def parse_query(qs, d='&') + def parse_query(qs, d = '&') query_parser.parse_nested_query(qs, d) end @@ -476,9 +498,53 @@ module Rack ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : [] end + def strip_port(ip_address) + # IPv6 format with optional port: "[2001:db8:cafe::17]:47011" + # returns: "2001:db8:cafe::17" + 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" + sep = ip_address.index(':') + if (sep && ip_address.count(':') == 1) + return ip_address[0, sep] + end + + ip_address + end + def reject_trusted_ip_addresses(ip_addresses) ip_addresses.reject { |ip| trusted_proxy?(ip) } end + + def forwarded_scheme + 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 + + def extract_proto_header(header) + if header + if (comma_index = header.index(',')) + header[0, comma_index] + else + header + end + end + end + + def extract_port(uri) + if (colon_index = uri.index(':')) + uri[colon_index + 1, uri.length] + end + end end include Env diff --git a/lib/rack/response.rb b/lib/rack/response.rb index 9ac47aad..2e548cde 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + require 'rack/request' require 'rack/utils' require 'rack/body_proxy' +require 'rack/simple_body_proxy' require 'rack/media_type' require 'time' @@ -23,11 +26,15 @@ module Rack attr_reader :header alias headers header - CHUNKED = 'chunked'.freeze + CHUNKED = 'chunked' + STATUS_WITH_NO_ENTITY_BODY = { + 204 => true, + 304 => true + }.freeze - def initialize(body=[], status=200, header={}) + 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 @@ -48,7 +55,7 @@ module Rack yield self if block_given? end - def redirect(target, status=302) + def redirect(target, status = 302) self.status = status self.location = target end @@ -60,13 +67,17 @@ module Rack def finish(&block) @block = block - if [204, 205, 304].include?(status.to_i) + if STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) delete_header CONTENT_TYPE delete_header CONTENT_LENGTH close [status.to_i, header, []] else - [status.to_i, header, BodyProxy.new(self){}] + if @block.nil? + [status.to_i, header, SimpleBodyProxy.new(@body)] + else + [status.to_i, header, BodyProxy.new(self){}] + end end end alias to_a finish # For *response @@ -184,7 +195,7 @@ module Rack set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value) end - def delete_cookie(key, value={}) + def delete_cookie(key, value = {}) set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value) end diff --git a/lib/rack/rewindable_input.rb b/lib/rack/rewindable_input.rb index dd6b7843..97725091 100644 --- a/lib/rack/rewindable_input.rb +++ b/lib/rack/rewindable_input.rb @@ -1,4 +1,6 @@ # -*- encoding: binary -*- +# frozen_string_literal: true + require 'tempfile' require 'rack/utils' @@ -72,7 +74,7 @@ module Rack @unlinked = true end - buffer = "" + buffer = "".dup while @io.read(1024 * 4, buffer) entire_buffer_written_out = false while !entire_buffer_written_out diff --git a/lib/rack/runtime.rb b/lib/rack/runtime.rb index bb15bdb1..d2bca9e5 100644 --- a/lib/rack/runtime.rb +++ b/lib/rack/runtime.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/utils' module Rack @@ -8,8 +10,8 @@ module Rack # time, or before all the other middlewares to include time for them, # too. class Runtime - FORMAT_STRING = "%0.6f".freeze # :nodoc: - HEADER_NAME = "X-Runtime".freeze # :nodoc: + FORMAT_STRING = "%0.6f" # :nodoc: + HEADER_NAME = "X-Runtime" # :nodoc: def initialize(app, name = nil) @app = app diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb index bdb7cf2f..51ba4db5 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/file' require 'rack/body_proxy' @@ -53,7 +55,7 @@ module Rack # that it maps to. The middleware performs a simple substitution on the # resulting path. # - # See Also: http://wiki.codemongers.com/NginxXSendfile + # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile # # === lighttpd # @@ -99,7 +101,7 @@ module Rack # will be matched with case indifference. class Sendfile - def initialize(app, variation=nil, mappings=[]) + def initialize(app, variation = nil, mappings = []) @app = app @variation = variation @mappings = mappings.map do |internal, external| @@ -115,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) @@ -147,11 +150,15 @@ module Rack end def map_accel_path(env, path) - if mapping = @mappings.find { |internal,_| internal =~ path } + if mapping = @mappings.find { |internal, _| internal =~ path } path.sub(*mapping) elsif mapping = env['HTTP_X_ACCEL_MAPPING'] - internal, external = mapping.split('=', 2).map(&:strip) - path.sub(/^#{internal}/i, external) + mapping.split(',').map(&:strip).each do |m| + internal, external = m.split('=', 2).map(&:strip) + new_path = path.sub(/^#{internal}/i, external) + return new_path unless path == new_path + end + path end end end diff --git a/lib/rack/server.rb b/lib/rack/server.rb index 690f1096..f0bc1500 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -1,9 +1,14 @@ +# frozen_string_literal: true + require 'optparse' +require 'fileutils' +require_relative 'core_ext/regexp' module Rack class Server + using ::Rack::RegexpExtensions class Options def parse!(args) @@ -20,10 +25,6 @@ module Rack lineno += 1 } - opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line| - options[:builder] = line - } - opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") { options[:debug] = true } @@ -46,7 +47,11 @@ module Rack opts.separator "" opts.separator "Rack options:" - opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s| + opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line| + options[:builder] = line + } + + opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick)") { |s| options[:server] = s } @@ -77,6 +82,24 @@ module Rack } opts.separator "" + opts.separator "Profiling options:" + + opts.on("--heap HEAPFILE", "Build the application, then dump the heap to HEAPFILE") do |e| + options[:heapfile] = e + end + + opts.on("--profile PROFILE", "Dump CPU or Memory profile to PROFILE (defaults to a tempfile)") do |e| + options[:profile_file] = e + end + + opts.on("--profile-mode MODE", "Profile mode (cpu|wall|object)") do |e| + { cpu: true, wall: true, object: true }.fetch(e.to_sym) do + raise OptionParser::InvalidOption, "unknown profile mode: #{e}" + end + options[:profile_mode] = e.to_sym + end + + opts.separator "" opts.separator "Common options:" opts.on_tail("-h", "-?", "--help", "Show this message") do @@ -113,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 @@ -151,7 +174,9 @@ module Rack # Options may include: # * :app - # a rack application to run (overrides :config) + # a rack application to run (overrides :config and :builder) + # * :builder + # a string to evaluate a Rack::Builder from # * :config # a rackup configuration file path to load (.ru) # * :environment @@ -181,6 +206,14 @@ module Rack # add given paths to $LOAD_PATH # * :require # require the given libraries + # + # Additional options for profiling app initialization include: + # * :heapfile + # location for ObjectSpace.dump_all to write the output to + # * :profile_file + # location for CPU/Memory (StackProf) profile output (defaults to a tempfile) + # * :profile_mode + # StackProf profile mode (cpu|wall|object) def initialize(options = nil) @ignore_options = [] @@ -205,12 +238,12 @@ module Rack default_host = environment == 'development' ? 'localhost' : '0.0.0.0' { - :environment => environment, - :pid => nil, - :Port => 9292, - :Host => default_host, - :AccessLog => [], - :config => "config.ru" + environment: environment, + pid: nil, + Port: 9292, + Host: default_host, + AccessLog: [], + config: "config.ru" } end @@ -221,12 +254,12 @@ 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 def default_middleware_by_environment - m = Hash.new {|h,k| h[k] = []} + m = Hash.new {|h, k| h[k] = []} m["deployment"] = [ [Rack::ContentLength], [Rack::Chunked], @@ -279,7 +312,9 @@ module Rack # Touch the wrapped app, so that the config.ru is loaded before # daemonization (i.e. before chdir, etc). - wrapped_app + handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do + wrapped_app + end daemonize_app if options[:daemonize] @@ -320,6 +355,44 @@ module Rack app end + def handle_profiling(heapfile, profile_mode, filename) + if heapfile + require "objspace" + ObjectSpace.trace_object_allocations_start + yield + GC.start + ::File.open(heapfile, "w") { |f| ObjectSpace.dump_all(output: f) } + exit + end + + if profile_mode + require "stackprof" + require "tempfile" + + make_profile_name(filename) do |filename| + ::File.open(filename, "w") do |f| + StackProf.run(mode: profile_mode, out: f) do + yield + end + puts "Profile written to: #{filename}" + end + end + exit + end + + yield + end + + def make_profile_name(filename) + if filename + yield filename + else + ::Dir::Tmpname.create("profile.dump") do |tmpname, _, _| + yield tmpname + end + end + end + def build_app_from_string Rack::Builder.new_from_string(self.options[:builder]) end @@ -359,7 +432,7 @@ module Rack def write_pid ::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") } - at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) } + at_exit { ::FileUtils.rm_f(options[:pid]) } rescue Errno::EEXIST check_pid! retry diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index 204bdb34..c9258644 100644 --- a/lib/rack/session/abstract/id.rb +++ b/lib/rack/session/abstract/id.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net # bugrep: Andreas Zehnder @@ -15,9 +17,23 @@ 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) + include Enumerable attr_writer :id + Unspecified = Object.new + def self.find(req) req.get_header RACK_SESSION end @@ -54,7 +70,15 @@ module Rack load_for_read! @data[key.to_s] end - alias :fetch :[] + + def fetch(key, default = Unspecified, &block) + load_for_read! + if default == Unspecified + @data.fetch(key.to_s, &block) + else + @data.fetch(key.to_s, default, &block) + end + end def has_key?(key) load_for_read! @@ -150,11 +174,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 @@ -167,7 +187,7 @@ module Rack # * :key determines the name of the cookie, by default it is # 'rack.session' # * :path, :domain, :expire_after, :secure, and :httponly set the related - # cookie options as by Rack::Response#add_cookie + # cookie options as by Rack::Response#set_cookie # * :skip will not a set a cookie in the response nor update the session state # * :defer will not set a cookie in the response but still update the session # state if it is used with a backend @@ -189,22 +209,22 @@ module Rack class Persisted DEFAULT_OPTIONS = { - :key => RACK_SESSION, - :path => '/', - :domain => nil, - :expire_after => nil, - :secure => false, - :httponly => true, - :defer => false, - :renew => false, - :sidbits => 128, - :cookie_only => true, - :secure_random => ::SecureRandom - } + key: RACK_SESSION, + path: '/', + domain: nil, + expire_after: nil, + secure: false, + httponly: true, + defer: false, + renew: false, + sidbits: 128, + cookie_only: true, + secure_random: ::SecureRandom + }.freeze attr_reader :key, :default_options, :sid_secure - def initialize(app, options={}) + def initialize(app, options = {}) @app = app @default_options = self.class::DEFAULT_OPTIONS.merge(options) @key = @default_options.delete(:key) @@ -216,7 +236,7 @@ module Rack context(env) end - def context(env, app=@app) + def context(env, app = @app) req = make_request env prepare_session(req) status, headers, body = app.call(req.env) @@ -339,7 +359,7 @@ module Rack session.send(:load!) unless loaded_session?(session) session_id ||= session.id - session_data = session.to_hash.delete_if { |k,v| v.nil? } + session_data = session.to_hash.delete_if { |k, v| v.nil? } if not data = write_session(req, session_id, session_data, options) req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.") @@ -398,7 +418,7 @@ module Rack class ID < Persisted def self.inherited(klass) - k = klass.ancestors.find { |kl| kl.superclass == ID } + k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID } unless k.instance_variable_defined?(:"@_rack_warned") warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE k.instance_variable_set(:"@_rack_warned", true) diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index 71bb96f4..3c067d7b 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'openssl' require 'zlib' require 'rack/request' @@ -103,7 +105,7 @@ module Rack attr_reader :coder - def initialize(app, options={}) + def initialize(app, options = {}) @secrets = options.values_at(:secret, :old_secret).compact @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1) @@ -116,8 +118,8 @@ module Rack Called from: #{caller[0]}. MSG - @coder = options[:coder] ||= Base64::Marshal.new - super(app, options.merge!(:cookie_only => true)) + @coder = options[:coder] ||= Base64::Marshal.new + super(app, options.merge!(cookie_only: true)) end private @@ -137,9 +139,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 @@ -147,7 +147,7 @@ module Rack end end - def persistent_session_id!(data, sid=nil) + def persistent_session_id!(data, sid = nil) data ||= {} data["session_id"] ||= sid || generate_sid data diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb index 4cf5ea09..dd587633 100644 --- a/lib/rack/session/memcache.rb +++ b/lib/rack/session/memcache.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net require 'rack/session/abstract/id' require 'memcache' +require 'rack/core_ext/regexp' module Rack module Session @@ -20,18 +23,20 @@ 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 \ - :namespace => 'rack:session', - :memcache_server => 'localhost:11211' + namespace: 'rack:session', + memcache_server: 'localhost:11211' - def initialize(app, options={}) + def initialize(app, options = {}) super @mutex = Mutex.new mserv = @default_options[:memcache_server] - mopts = @default_options.reject{|k,v| !MemCache::DEFAULT_OPTIONS.include? k } + mopts = @default_options.reject{|k, v| !MemCache::DEFAULT_OPTIONS.include? k } @pool = options[:cache] || MemCache.new(mserv, mopts) unless @pool.active? and @pool.servers.any?(&:alive?) @@ -50,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/session/pool.rb b/lib/rack/session/pool.rb index 4c9c25c7..2e1f867f 100644 --- a/lib/rack/session/pool.rb +++ b/lib/rack/session/pool.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net # THANKS: # apeiros, for session id generation, expiry setup, and threadiness @@ -26,9 +28,9 @@ module Rack class Pool < Abstract::Persisted attr_reader :mutex, :pool - DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge :drop => false + DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge drop: false - def initialize(app, options={}) + def initialize(app, options = {}) super @pool = Hash.new @mutex = Mutex.new diff --git a/lib/rack/show_exceptions.rb b/lib/rack/show_exceptions.rb index ca86b2b2..843af607 100644 --- a/lib/rack/show_exceptions.rb +++ b/lib/rack/show_exceptions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'ostruct' require 'erb' require 'rack/request' @@ -46,7 +48,7 @@ module Rack end def prefers_plaintext?(env) - !accepts_html(env) + !accepts_html?(env) end def accepts_html?(env) @@ -55,7 +57,7 @@ module Rack private :accepts_html? def dump_exception(exception) - string = "#{exception.class}: #{exception.message}\n" + string = "#{exception.class}: #{exception.message}\n".dup string << exception.backtrace.map { |l| "\t#{l}" }.join("\n") string end @@ -77,13 +79,13 @@ module Rack frame.function = $4 begin - lineno = frame.lineno-1 + lineno = frame.lineno - 1 lines = ::File.readlines(frame.filename) - frame.pre_context_lineno = [lineno-CONTEXT, 0].max + frame.pre_context_lineno = [lineno - CONTEXT, 0].max frame.pre_context = lines[frame.pre_context_lineno...lineno] frame.context_line = lines[lineno].chomp - frame.post_context_lineno = [lineno+CONTEXT, lines.size].min - frame.post_context = lines[lineno+1..frame.post_context_lineno] + frame.post_context_lineno = [lineno + CONTEXT, lines.size].min + frame.post_context = lines[lineno + 1..frame.post_context_lineno] rescue end @@ -93,7 +95,11 @@ module Rack end }.compact - TEMPLATE.result(binding) + template.result(binding) + end + + def template + TEMPLATE end def h(obj) # :nodoc: @@ -107,8 +113,8 @@ module Rack # :stopdoc: - # adapted from Django <djangoproject.com> - # Copyright (c) 2005, the Lawrence Journal-World + # adapted from Django <www.djangoproject.com> + # Copyright (c) Django Software Foundation and individual contributors. # Used under the modified BSD license: # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 TEMPLATE = ERB.new(<<-'HTML'.gsub(/^ /, '')) @@ -363,7 +369,7 @@ module Rack <% env.sort_by { |k, v| k.to_s }.each { |key, val| %> <tr> <td><%=h key %></td> - <td class="code"><div><%=h val %></div></td> + <td class="code"><div><%=h val.inspect %></div></td> </tr> <% } %> </tbody> diff --git a/lib/rack/show_status.rb b/lib/rack/show_status.rb index 54db8f47..3fdfca5e 100644 --- a/lib/rack/show_status.rb +++ b/lib/rack/show_status.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'erb' require 'rack/request' require 'rack/utils' @@ -52,8 +54,8 @@ module Rack # :stopdoc: -# adapted from Django <djangoproject.com> -# Copyright (c) 2005, the Lawrence Journal-World +# adapted from Django <www.djangoproject.com> +# Copyright (c) Django Software Foundation and individual contributors. # Used under the modified BSD license: # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 TEMPLATE = <<'HTML' diff --git a/lib/rack/simple_body_proxy.rb b/lib/rack/simple_body_proxy.rb new file mode 100644 index 00000000..fe007c4c --- /dev/null +++ b/lib/rack/simple_body_proxy.rb @@ -0,0 +1,13 @@ +# 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 f59a205d..24c40505 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + require "rack/file" require "rack/utils" +require_relative 'core_ext/regexp' + module Rack # The Rack::Static middleware intercepts requests for static files @@ -82,8 +86,9 @@ module Rack # ] # class Static + using ::Rack::RegexpExtensions - def initialize(app, options={}) + def initialize(app, options = {}) @app = app @urls = options[:urls] || ["/favicon.ico"] @index = options[:index] @@ -93,13 +98,13 @@ module Rack # HTTP Headers @header_rules = options[:header_rules] || [] # 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] + @header_rules.unshift([:all, { CACHE_CONTROL => options[:cache_control] }]) if options[:cache_control] @file_server = Rack::File.new(root) end def add_index_root?(path) - @index && path.end_with?('/'.freeze) + @index && route_file(path) && path.end_with?('/'.freeze) end def overwrite_file_path(path) @@ -120,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 && /\bgzip\b/.match?(env['HTTP_ACCEPT_ENCODING']) path = env[PATH_INFO] env[PATH_INFO] += '.gz' response = @file_server.call(env) @@ -157,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/tempfile_reaper.rb b/lib/rack/tempfile_reaper.rb index d8299806..73b6c1c8 100644 --- a/lib/rack/tempfile_reaper.rb +++ b/lib/rack/tempfile_reaper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/body_proxy' module Rack diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb index 510b4b50..c5d9c44f 100644 --- a/lib/rack/urlmap.rb +++ b/lib/rack/urlmap.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'set' + module Rack # Rack::URLMap takes a hash mapping urls or paths to apps, and # dispatches accordingly. Support for HTTP/1.1 host names exists if @@ -20,9 +24,11 @@ module Rack end def remap(map) + @known_hosts = Set[] @mapping = map.map { |location, app| if location =~ %r{\Ahttps?://(.*?)(/.*)} host, location = $1, $2 + @known_hosts << host else host = nil end @@ -50,10 +56,13 @@ module Rack is_same_server = casecmp?(http_host, server_name) || casecmp?(http_host, "#{server_name}:#{server_port}") + is_host_known = @known_hosts.include? http_host + @mapping.each do |host, location, match, app| unless casecmp?(http_host, host) \ || casecmp?(server_name, host) \ - || (!host && is_same_server) + || (!host && is_same_server) \ + || (!host && !is_host_known) # If we don't have a matching host, default to the first without a specified host next end @@ -68,7 +77,7 @@ module Rack return app.call(env) end - [404, {CONTENT_TYPE => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]] + [404, { CONTENT_TYPE => "text/plain", "X-Cascade" => "pass" }, ["Not Found: #{path}"]] ensure env[PATH_INFO] = path diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb index 7b842125..43d70a85 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -1,4 +1,6 @@ # -*- encoding: binary -*- +# frozen_string_literal: true + require 'uri' require 'fileutils' require 'set' @@ -6,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 @@ -118,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 @@ -175,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 @@ -211,7 +216,7 @@ module Rack # precede those with less specific. Ordering with respect to other # attributes (e.g., Domain) is unspecified. cookies = parse_query(header, ';,') { |s| unescape(s) rescue s } - cookies.each_with_object({}) { |(k,v), hash| hash[k] = Array === v ? v.first : v } + cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v } end module_function :parse_cookies_header @@ -221,41 +226,19 @@ module Rack domain = "; domain=#{value[:domain]}" if value[:domain] path = "; path=#{value[:path]}" if value[:path] max_age = "; max-age=#{value[:max_age]}" if value[:max_age] - # There is an RFC mess in the area of date formatting for Cookies. Not - # only are there contradicting RFCs and examples within RFC text, but - # there are also numerous conflicting names of fields and partially - # cross-applicable specifications. - # - # These are best described in RFC 2616 3.3.1. This RFC text also - # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a - # fixed length format with space-date delimited fields. - # - # See also RFC 1123 section 5.2.14. - # - # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined - # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote - # the space delimited format. These formats are compliant with RFC 2822. - # - # For reference, all involved RFCs are: - # RFC 822 - # RFC 1123 - # RFC 2109 - # RFC 2616 - # RFC 2822 - # RFC 2965 - # RFC 6265 - expires = "; expires=" + - rfc2822(value[:expires].clone.gmtime) if value[:expires] + expires = "; expires=#{value[:expires].httpdate}" if value[:expires] secure = "; secure" if value[:secure] httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only]) same_site = case value[:same_site] when false, nil nil + when :none, 'None', :None + '; SameSite=None' when :lax, 'Lax', :Lax - '; SameSite=Lax'.freeze + '; SameSite=Lax' when true, :strict, 'Strict', :Strict - '; SameSite=Strict'.freeze + '; SameSite=Strict' else raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}" end @@ -295,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 @@ -321,9 +304,9 @@ module Rack new_header = make_delete_cookie_header(header, key, value) add_cookie_to_header(new_header, key, - {:value => '', :path => nil, :domain => nil, - :max_age => '0', - :expires => Time.at(0) }.merge(value)) + { value: '', path: nil, domain: nil, + max_age: '0', + expires: Time.at(0) }.merge(value)) end module_function :add_remove_cookie_to_header @@ -364,7 +347,7 @@ module Rack ranges = [] $1.split(/,\s*/).each do |range_spec| return nil unless range_spec =~ /(\d*)-(\d*)/ - r0,r1 = $1, $2 + r0, r1 = $1, $2 if r0.empty? return nil if r1.empty? # suffix-byte-range-spec, represents trailing suffix of file @@ -378,7 +361,7 @@ module Rack else r1 = r1.to_i return nil if r1 < r0 # backwards range is syntactically invalid - r1 = size-1 if r1 >= size + r1 = size - 1 if r1 >= size end end ranges << (r0..r1) if r0 <= r1 @@ -399,7 +382,7 @@ module Rack l = a.unpack("C*") r, i = 0, -1 - b.each_byte { |v| r |= v ^ l[i+=1] } + b.each_byte { |v| r |= v ^ l[i += 1] } r == 0 end module_function :secure_compare @@ -425,19 +408,17 @@ module Rack self.class.new(@for, app) end - def context(env, app=@app) + def context(env, app = @app) recontext(app).call(env) end end # 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 - - def initialize(hash={}) + # + # @api private + class HeaderHash < Hash # :nodoc: + def initialize(hash = {}) super() @names = {} hash.each { |k, v| self[k] = v } @@ -457,7 +438,7 @@ module Rack def to_hash hash = {} - each { |k,v| hash[k] = v } + each { |k, v| hash[k] = v } hash end @@ -510,13 +491,14 @@ module Rack # Every standard HTTP code mapped to the appropriate message. # Generated with: - # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \ - # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \ - # puts "#{m[1]} => \x27#{m[2].strip}\x27,"' + # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \ + # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \ + # puts "#{m[1]} => \x27#{m[2].strip}\x27,"' HTTP_STATUS_CODES = { 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', + 103 => 'Early Hints', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', @@ -557,6 +539,7 @@ module Rack 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', + 425 => 'Too Early', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', @@ -571,12 +554,13 @@ module Rack 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', + 509 => 'Bandwidth Limit Exceeded', 510 => 'Not Extended', 511 => 'Network Authentication Required' } # Responses with HTTP status codes that should not have an entity body - STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304) + STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])] SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message| [message.downcase.gsub(/\s|-|'/, '_').to_sym, code] @@ -584,7 +568,7 @@ module Rack def status_code(status) if status.is_a?(Symbol) - SYMBOL_TO_STATUS_CODE[status] || 500 + SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } else status.to_i end @@ -605,11 +589,11 @@ module Rack clean.unshift '/' if parts.empty? || parts.first.empty? - ::File.join(*clean) + ::File.join clean end module_function :clean_path_info - NULL_BYTE = "\0".freeze + NULL_BYTE = "\0" def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) diff --git a/rack.gemspec b/rack.gemspec index 1b412793..f7b13b17 100644 --- a/rack.gemspec +++ b/rack.gemspec @@ -1,5 +1,7 @@ +# frozen_string_literal: true + Gem::Specification.new do |s| - s.name = "rack" + s.name = "rack" s.version = File.read('lib/rack.rb')[/RELEASE += +([\"\'])([\d][\w\.]+)\1/, 2] s.platform = Gem::Platform::RUBY s.summary = "a modular Ruby webserver interface" @@ -12,26 +14,31 @@ the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call. -Also see http://rack.github.io/. +Also see https://rack.github.io/. EOF - s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] + - %w(COPYING rack.gemspec Rakefile README.rdoc SPEC) + s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*}'] + + %w(MIT-LICENSE rack.gemspec Rakefile README.rdoc SPEC) s.bindir = 'bin' - s.executables << 'rackup' - s.require_path = 'lib' - s.extra_rdoc_files = ['README.rdoc', 'HISTORY.md'] - s.test_files = Dir['test/spec_*.rb'] + s.executables << 'rackup' + s.require_path = 'lib' + s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.md'] - s.author = 'Christian Neukirchen' - s.email = 'chneukirchen@gmail.com' - s.homepage = 'http://rack.github.io/' + s.author = 'Leah Neukirchen' + s.email = 'leah@vuxu.org' + s.homepage = 'https://rack.github.io/' s.required_ruby_version = '>= 2.2.2' - - s.add_dependency 'json' + 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 'concurrent-ruby' + s.add_development_dependency 'minitest-global_expectations' s.add_development_dependency 'rake' end diff --git a/test/builder/an_underscore_app.rb b/test/builder/an_underscore_app.rb index 7ce1a0cc..f58a2be5 100644 --- a/test/builder/an_underscore_app.rb +++ b/test/builder/an_underscore_app.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AnUnderscoreApp def self.call(env) - [200, {'Content-Type' => 'text/plain'}, ['OK']] + [200, { 'Content-Type' => 'text/plain' }, ['OK']] end end diff --git a/test/builder/anything.rb b/test/builder/anything.rb deleted file mode 100644 index c07f82cd..00000000 --- a/test/builder/anything.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Anything - def self.call(env) - [200, {'Content-Type' => 'text/plain'}, ['OK']] - end -end diff --git a/test/builder/bom.ru b/test/builder/bom.ru new file mode 100644 index 00000000..5740f9a1 --- /dev/null +++ b/test/builder/bom.ru @@ -0,0 +1 @@ +run -> (env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } diff --git a/test/builder/comment.ru b/test/builder/comment.ru index 0722f0a0..894ba5d0 100644 --- a/test/builder/comment.ru +++ b/test/builder/comment.ru @@ -1,4 +1,6 @@ +# frozen_string_literal: true + =begin =end -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] } +run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } diff --git a/test/builder/end.ru b/test/builder/end.ru index 7f36d8cb..dd8d45a9 100644 --- a/test/builder/end.ru +++ b/test/builder/end.ru @@ -1,4 +1,6 @@ -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] } +# frozen_string_literal: true + +run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } __END__ Should not be evaluated Neither should diff --git a/test/builder/line.ru b/test/builder/line.ru index f4c84ade..9ad88986 100644 --- a/test/builder/line.ru +++ b/test/builder/line.ru @@ -1 +1,3 @@ -run lambda{ |env| [200, {'Content-Type' => 'text/plain'}, [__LINE__.to_s]] } +# frozen_string_literal: true + +run lambda{ |env| [200, { 'Content-Type' => 'text/plain' }, [__LINE__.to_s]] } diff --git a/test/builder/options.ru b/test/builder/options.ru index 4af32440..dca48fd9 100644 --- a/test/builder/options.ru +++ b/test/builder/options.ru @@ -1,2 +1,4 @@ +# frozen_string_literal: true + #\ -d -p 2929 --env test -run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] } +run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } diff --git a/test/cgi/rackup_stub.rb b/test/cgi/rackup_stub.rb index a216cdc3..5f0e4365 100755 --- a/test/cgi/rackup_stub.rb +++ b/test/cgi/rackup_stub.rb @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# -*- ruby -*- +# frozen_string_literal: true $:.unshift '../../lib' require 'rack' diff --git a/test/cgi/sample_rackup.ru b/test/cgi/sample_rackup.ru index a73df81c..c8e94c9f 100755 --- a/test/cgi/sample_rackup.ru +++ b/test/cgi/sample_rackup.ru @@ -1,4 +1,4 @@ -# -*- ruby -*- +# frozen_string_literal: true require '../testrequest' diff --git a/test/cgi/test b/test/cgi/test index e4837a4e..a1de2fbe 100755 --- a/test/cgi/test +++ b/test/cgi/test @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# -*- ruby -*- +# frozen_string_literal: true $: << File.join(File.dirname(__FILE__), "..", "..", "lib") diff --git a/test/cgi/test.fcgi b/test/cgi/test.fcgi index 31f43399..a67e547a 100755 --- a/test/cgi/test.fcgi +++ b/test/cgi/test.fcgi @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# -*- ruby -*- +# frozen_string_literal: true require 'uri' $:.unshift '../../lib' diff --git a/test/cgi/test.gz b/test/cgi/test.gz Binary files differindex 312c60e2..a23c856c 100644 --- a/test/cgi/test.gz +++ b/test/cgi/test.gz diff --git a/test/cgi/test.ru b/test/cgi/test.ru index 7913ef78..1263778d 100755 --- a/test/cgi/test.ru +++ b/test/cgi/test.ru @@ -1,5 +1,5 @@ #!../../bin/rackup -# -*- ruby -*- +# frozen_string_literal: true require '../testrequest' run Rack::Lint.new(TestRequest.new) diff --git a/test/gemloader.rb b/test/gemloader.rb index 22be6975..f38c8036 100644 --- a/test/gemloader.rb +++ b/test/gemloader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rubygems' project = 'rack' gemspec = File.expand_path("#{project}.gemspec", Dir.pwd) diff --git a/test/helper.rb b/test/helper.rb index aa9c0e0a..dff89558 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' module Rack class TestCase < Minitest::Test diff --git a/test/multipart/filename_with_null_byte b/test/multipart/filename_with_null_byte new file mode 100644 index 00000000..961d44c4 --- /dev/null +++ b/test/multipart/filename_with_null_byte @@ -0,0 +1,7 @@ +--AaB03x
+Content-Type: image/jpeg
+Content-Disposition: attachment; name="files"; filename="flowers.exe%00.jpg"
+Content-Description: a complete map of the human genome
+
+contents
+--AaB03x--
diff --git a/test/multipart/filename_with_plus b/test/multipart/filename_with_plus new file mode 100644 index 00000000..aa75022b --- /dev/null +++ b/test/multipart/filename_with_plus @@ -0,0 +1,6 @@ +--AaB03x
+Content-Disposition: form-data; name="files"; filename="foo+bar"
+Content-Type: application/octet-stream
+
+contents
+--AaB03x--
diff --git a/test/rackup/config.ru b/test/rackup/config.ru index f1e2e1f3..fa9b6eca 100644 --- a/test/rackup/config.ru +++ b/test/rackup/config.ru @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "#{File.dirname(__FILE__)}/../testrequest" $stderr = File.open("#{File.dirname(__FILE__)}/log_output", "w") diff --git a/test/registering_handler/rack/handler/registering_myself.rb b/test/registering_handler/rack/handler/registering_myself.rb index 4964953b..21b60516 100644 --- a/test/registering_handler/rack/handler/registering_myself.rb +++ b/test/registering_handler/rack/handler/registering_myself.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Handler class RegisteringMyself diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb index 1e19bf66..3e479ace 100644 --- a/test/spec_auth_basic.rb +++ b/test/spec_auth_basic.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/auth/basic' require 'rack/lint' require 'rack/mock' @@ -10,7 +12,7 @@ describe Rack::Auth::Basic do def unprotected_app Rack::Lint.new lambda { |env| - [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ] + [ 200, { 'Content-Type' => 'text/plain' }, ["Hi #{env['REMOTE_USER']}"] ] } end @@ -75,6 +77,13 @@ describe Rack::Auth::Basic do end end + it 'return 401 Bad Request for a nil authorization header' do + request 'HTTP_AUTHORIZATION' => nil do |response| + response.must_be :client_error? + response.status.must_equal 401 + end + end + it 'takes realm as optional constructor arg' do app = Rack::Auth::Basic.new(unprotected_app, realm) { true } realm.must_equal app.realm diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb index 7230bb68..cc205aa9 100644 --- a/test/spec_auth_digest.rb +++ b/test/spec_auth_digest.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/auth/digest/md5' require 'rack/lint' require 'rack/mock' @@ -11,12 +13,12 @@ describe Rack::Auth::Digest::MD5 do def unprotected_app Rack::Lint.new lambda { |env| friend = Rack::Utils.parse_query(env["QUERY_STRING"])["friend"] - [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}#{friend ? " and #{friend}" : ''}"] ] + [ 200, { 'Content-Type' => 'text/plain' }, ["Hi #{env['REMOTE_USER']}#{friend ? " and #{friend}" : ''}"] ] } end def protected_app - Rack::Auth::Digest::MD5.new(unprotected_app, :realm => realm, :opaque => 'this-should-be-secret') do |username| + Rack::Auth::Digest::MD5.new(unprotected_app, realm: realm, opaque: 'this-should-be-secret') do |username| { 'Alice' => 'correct-password' }[username] end end @@ -158,7 +160,7 @@ describe Rack::Auth::Digest::MD5 do begin Rack::Auth::Digest::Nonce.time_limit = 10 - request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 1 do |response| + request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', wait: 1 do |response| response.status.must_equal 200 response.body.to_s.must_equal 'Hi Alice' response.headers['WWW-Authenticate'].wont_match(/\bstale=true\b/) @@ -172,7 +174,7 @@ describe Rack::Auth::Digest::MD5 do begin Rack::Auth::Digest::Nonce.time_limit = 1 - request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 2 do |response| + request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', wait: 2 do |response| assert_digest_auth_challenge response response.headers['WWW-Authenticate'].must_match(/\bstale=true\b/) end @@ -247,7 +249,7 @@ describe Rack::Auth::Digest::MD5 do it 'return application output if correct credentials given for PUT (using method override of POST)' do @request = Rack::MockRequest.new(protected_app_with_method_override) - request_with_digest_auth 'POST', '/', 'Alice', 'correct-password', :input => "_method=put" do |response| + request_with_digest_auth 'POST', '/', 'Alice', 'correct-password', input: "_method=put" do |response| response.status.must_equal 200 response.body.to_s.must_equal 'Hi Alice' end diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb index 4db447a0..6be79f8b 100644 --- a/test/spec_body_proxy.rb +++ b/test/spec_body_proxy.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/body_proxy' require 'stringio' @@ -40,7 +42,7 @@ describe Rack::BodyProxy do called = false begin - proxy = Rack::BodyProxy.new(object) { called = true } + proxy = Rack::BodyProxy.new(object) { called = true } called.must_equal false proxy.close rescue RuntimeError => e diff --git a/test/spec_builder.rb b/test/spec_builder.rb index ae1c4006..853fb7b1 100644 --- a/test/spec_builder.rb +++ b/test/spec_builder.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/builder' require 'rack/lint' require 'rack/mock' @@ -31,10 +33,10 @@ describe Rack::Builder do it "supports mapping" do app = builder_to_app do map '/' do |outer_env| - run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['root']] } + run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['root']] } end map '/sub' do - run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['sub']] } + run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['sub']] } end end Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root' @@ -47,7 +49,7 @@ describe Rack::Builder do map '/' do |outer_env| run lambda { |inner_env| inner_env['new_key'] = 'new_value' - [200, {"Content-Type" => "text/plain"}, ['root']] + [200, { "Content-Type" => "text/plain" }, ['root']] } end end @@ -55,6 +57,19 @@ describe Rack::Builder do NothingMiddleware.env['new_key'].must_equal 'new_value' end + it "dupe #to_app when mapping so Rack::Reloader can reload the application on each request" do + app = builder do + map '/' do |outer_env| + run lambda { |env| [200, { "Content-Type" => "text/plain" }, [object_id.to_s]] } + end + end + + builder_app1_id = Rack::MockRequest.new(app).get("/").body.to_s + builder_app2_id = Rack::MockRequest.new(app).get("/").body.to_s + + builder_app2_id.wont_equal builder_app1_id + end + it "chains apps by default" do app = builder_to_app do use Rack::ShowExceptions @@ -84,7 +99,7 @@ describe Rack::Builder do 'secret' == password end - run lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hi Boss']] } + run lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hi Boss']] } end response = Rack::MockRequest.new(app).get("/") @@ -112,9 +127,9 @@ describe Rack::Builder do it "can mix map and run for endpoints" do app = builder do map '/sub' do - run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['sub']] } + run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['sub']] } end - run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['root']] } + run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['root']] } end Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root' @@ -151,7 +166,7 @@ describe Rack::Builder do def call(env) raise "bzzzt" if @called > 0 @called += 1 - [200, {'Content-Type' => 'text/plain'}, ['OK']] + [200, { 'Content-Type' => 'text/plain' }, ['OK']] end end @@ -174,6 +189,27 @@ describe Rack::Builder do Rack::MockRequest.new(app).get("/").must_be :server_error? end + it "supports #freeze_app for freezing app and middleware" do + app = builder do + freeze_app + use Rack::ShowExceptions + use(Class.new do + def initialize(app) @app = app end + def call(env) @a = 1 if env['PATH_INFO'] == '/a'; @app.call(env) end + end) + o = Object.new + def o.call(env) + @a = 1 if env['PATH_INFO'] == '/b'; + [200, {}, []] + end + run o + end + + Rack::MockRequest.new(app).get("/a").must_be :server_error? + Rack::MockRequest.new(app).get("/b").must_be :server_error? + Rack::MockRequest.new(app).get("/c").status.must_equal 200 + end + it 'complains about a missing run' do proc do Rack::Lint.new Rack::Builder.app { use Rack::ShowExceptions } @@ -204,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' @@ -220,7 +249,12 @@ describe Rack::Builder do it "sets __LINE__ correctly" do app, _ = Rack::Builder.parse_file config_file('line.ru') - Rack::MockRequest.new(app).get("/").body.to_s.must_equal '1' + Rack::MockRequest.new(app).get("/").body.to_s.must_equal '3' + end + + it "strips leading unicode byte order mark when present" do + app, _ = Rack::Builder.parse_file config_file('bom.ru') + Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK' end end diff --git a/test/spec_cascade.rb b/test/spec_cascade.rb index 180ce46e..a06aefeb 100644 --- a/test/spec_cascade.rb +++ b/test/spec_cascade.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/cascade' require 'rack/file' @@ -17,7 +19,7 @@ describe Rack::Cascade do app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" }) app3 = Rack::URLMap.new("/foo" => lambda { |env| - [200, { "Content-Type" => "text/plain"}, [""]]}) + [200, { "Content-Type" => "text/plain" }, [""]]}) it "dispatch onward on 404 and 405 by default" do cascade = cascade([app1, app2, app3]) diff --git a/test/spec_cgi.rb b/test/spec_cgi.rb index 77020c2f..4e8111a6 100644 --- a/test/spec_cgi.rb +++ b/test/spec_cgi.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'helper' if defined? LIGHTTPD_PID @@ -35,7 +37,7 @@ describe Rack::Handler::CGI do it "have rack headers" do GET("/test") - response["rack.version"].must_equal [1,3] + response["rack.version"].must_equal [1, 3] assert_equal false, response["rack.multithread"] assert_equal true, response["rack.multiprocess"] assert_equal true, response["rack.run_once"] @@ -59,7 +61,7 @@ describe Rack::Handler::CGI do end it "have CGI headers on POST" do - POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'}) + POST("/test", { "rack-form-data" => "23" }, { 'X-test-header' => '42' }) status.must_equal 200 response["REQUEST_METHOD"].must_equal "POST" response["SCRIPT_NAME"].must_equal "/test" @@ -70,7 +72,7 @@ describe Rack::Handler::CGI do end it "support HTTP auth" do - GET("/test", {:user => "ruth", :passwd => "secret"}) + GET("/test", { user: "ruth", passwd: "secret" }) response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" end diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb index 7bbcfd92..daa36cad 100644 --- a/test/spec_chunked.rb +++ b/test/spec_chunked.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/chunked' require 'rack/lint' require 'rack/mock' @@ -19,8 +21,28 @@ describe Rack::Chunked do env_for('/', 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => 'GET') end + class TrailerBody + def each(&block) + ['Hello', ' ', 'World!'].each(&block) + end + + def trailers + { "Expires" => "tomorrow" } + end + end + + it 'yields trailer headers after the response' do + app = lambda { |env| + [200, { "Content-Type" => "text/plain", "Trailer" => "Expires" }, TrailerBody.new] + } + response = Rack::MockResponse.new(*chunked(app).call(@env)) + response.headers.wont_include 'Content-Length' + response.headers['Transfer-Encoding'].must_equal 'chunked' + response.body.must_equal "5\r\nHello\r\n1\r\n \r\n6\r\nWorld!\r\n0\r\nExpires: tomorrow\r\n\r\n" + end + it 'chunk responses with no Content-Length' do - app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] } + app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] } response = Rack::MockResponse.new(*chunked(app).call(@env)) response.headers.wont_include 'Content-Length' response.headers['Transfer-Encoding'].must_equal 'chunked' @@ -28,7 +50,7 @@ describe Rack::Chunked do end it 'chunks empty bodies properly' do - app = lambda { |env| [200, {"Content-Type" => "text/plain"}, []] } + app = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] } response = Rack::MockResponse.new(*chunked(app).call(@env)) response.headers.wont_include 'Content-Length' response.headers['Transfer-Encoding'].must_equal 'chunked' @@ -37,18 +59,18 @@ describe Rack::Chunked do it 'chunks encoded bodies properly' do body = ["\uFFFEHello", " ", "World"].map {|t| t.encode("UTF-16LE") } - app = lambda { |env| [200, {"Content-Type" => "text/plain"}, body] } + app = lambda { |env| [200, { "Content-Type" => "text/plain" }, body] } response = Rack::MockResponse.new(*chunked(app).call(@env)) response.headers.wont_include 'Content-Length' response.headers['Transfer-Encoding'].must_equal 'chunked' response.body.encoding.to_s.must_equal "ASCII-8BIT" - response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".force_encoding("BINARY") - response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".force_encoding(Encoding::BINARY) + response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".dup.force_encoding("BINARY") + response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".dup.force_encoding(Encoding::BINARY) end it 'not modify response when Content-Length header present' do app = lambda { |env| - [200, {"Content-Type" => "text/plain", 'Content-Length'=>'12'}, ['Hello', ' ', 'World!']] + [200, { "Content-Type" => "text/plain", 'Content-Length' => '12' }, ['Hello', ' ', 'World!']] } status, headers, body = chunked(app).call(@env) status.must_equal 200 @@ -58,7 +80,7 @@ describe Rack::Chunked do end it 'not modify response when client is HTTP/1.0' do - app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] } + app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] } @env['HTTP_VERSION'] = 'HTTP/1.0' status, headers, body = chunked(app).call(@env) status.must_equal 200 @@ -67,7 +89,7 @@ describe Rack::Chunked do end it 'not modify response when client is ancient, pre-HTTP/1.0' do - app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] } + app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] } check = lambda do status, headers, body = chunked(app).call(@env.dup) status.must_equal 200 @@ -84,7 +106,7 @@ describe Rack::Chunked do it 'not modify response when Transfer-Encoding header already present' do app = lambda { |env| - [200, {"Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity'}, ['Hello', ' ', 'World!']] + [200, { "Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity' }, ['Hello', ' ', 'World!']] } status, headers, body = chunked(app).call(@env) status.must_equal 200 @@ -92,7 +114,7 @@ describe Rack::Chunked do body.join.must_equal 'Hello World!' end - [100, 204, 205, 304].each do |status_code| + [100, 204, 304].each do |status_code| it "not modify response when status code is #{status_code}" do app = lambda { |env| [status_code, {}, []] } status, headers, _ = chunked(app).call(@env) diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb index 3589576b..330a6480 100644 --- a/test/spec_common_logger.rb +++ b/test/spec_common_logger.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/common_logger' require 'rack/lint' require 'rack/mock' @@ -11,15 +13,15 @@ describe Rack::CommonLogger do app = Rack::Lint.new lambda { |env| [200, - {"Content-Type" => "text/html", "Content-Length" => length.to_s}, + { "Content-Type" => "text/html", "Content-Length" => length.to_s }, [obj]]} app_without_length = Rack::Lint.new lambda { |env| [200, - {"Content-Type" => "text/html"}, + { "Content-Type" => "text/html" }, []]} app_with_zero_length = Rack::Lint.new lambda { |env| [200, - {"Content-Type" => "text/html", "Content-Length" => "0"}, + { "Content-Type" => "text/html", "Content-Length" => "0" }, []]} it "log to rack.errors by default" do diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb index 58f37ad5..8402f04e 100644 --- a/test/spec_conditional_get.rb +++ b/test/spec_conditional_get.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'time' require 'rack/conditional_get' require 'rack/mock' @@ -11,7 +13,7 @@ describe Rack::ConditionalGet do it "set a 304 status and truncate body when If-Modified-Since hits" do timestamp = Time.now.httpdate app = conditional_get(lambda { |env| - [200, {'Last-Modified'=>timestamp}, ['TEST']] }) + [200, { 'Last-Modified' => timestamp }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp) @@ -22,7 +24,7 @@ describe Rack::ConditionalGet do it "set a 304 status and truncate body when If-Modified-Since hits and is higher than current time" do app = conditional_get(lambda { |env| - [200, {'Last-Modified'=>(Time.now - 3600).httpdate}, ['TEST']] }) + [200, { 'Last-Modified' => (Time.now - 3600).httpdate }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate) @@ -33,7 +35,7 @@ describe Rack::ConditionalGet do it "set a 304 status and truncate body when If-None-Match hits" do app = conditional_get(lambda { |env| - [200, {'ETag'=>'1234'}, ['TEST']] }) + [200, { 'ETag' => '1234' }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_NONE_MATCH' => '1234') @@ -45,7 +47,7 @@ describe Rack::ConditionalGet do it "not set a 304 status if If-Modified-Since hits but Etag does not" do timestamp = Time.now.httpdate app = conditional_get(lambda { |env| - [200, {'Last-Modified'=>timestamp, 'Etag'=>'1234', 'Content-Type' => 'text/plain'}, ['TEST']] }) + [200, { 'Last-Modified' => timestamp, 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '4321') @@ -57,7 +59,7 @@ describe Rack::ConditionalGet do it "set a 304 status and truncate body when both If-None-Match and If-Modified-Since hits" do timestamp = Time.now.httpdate app = conditional_get(lambda { |env| - [200, {'Last-Modified'=>timestamp, 'ETag'=>'1234'}, ['TEST']] }) + [200, { 'Last-Modified' => timestamp, 'ETag' => '1234' }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '1234') @@ -68,7 +70,7 @@ describe Rack::ConditionalGet do it "not affect non-GET/HEAD requests" do app = conditional_get(lambda { |env| - [200, {'Etag'=>'1234', 'Content-Type' => 'text/plain'}, ['TEST']] }) + [200, { 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] }) response = Rack::MockRequest.new(app). post("/", 'HTTP_IF_NONE_MATCH' => '1234') @@ -79,7 +81,7 @@ describe Rack::ConditionalGet do it "not affect non-200 requests" do app = conditional_get(lambda { |env| - [302, {'Etag'=>'1234', 'Content-Type' => 'text/plain'}, ['TEST']] }) + [302, { 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_NONE_MATCH' => '1234') @@ -91,7 +93,7 @@ describe Rack::ConditionalGet do it "not affect requests with malformed HTTP_IF_NONE_MATCH" do bad_timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S %z') app = conditional_get(lambda { |env| - [200,{'Last-Modified'=>(Time.now - 3600).httpdate, 'Content-Type' => 'text/plain'}, ['TEST']] }) + [200, { 'Last-Modified' => (Time.now - 3600).httpdate, 'Content-Type' => 'text/plain' }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => bad_timestamp) diff --git a/test/spec_config.rb b/test/spec_config.rb index 16f0a664..d97107b6 100644 --- a/test/spec_config.rb +++ b/test/spec_config.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/builder' require 'rack/config' require 'rack/content_length' @@ -13,7 +15,7 @@ describe Rack::Config do env['greeting'] = 'hello' end run lambda { |env| - [200, {'Content-Type' => 'text/plain'}, [env['greeting'] || '']] + [200, { 'Content-Type' => 'text/plain' }, [env['greeting'] || '']] } end diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb index 261e4505..2e7a8581 100644 --- a/test/spec_content_length.rb +++ b/test/spec_content_length.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/content_length' require 'rack/lint' require 'rack/mock' @@ -13,7 +15,7 @@ describe Rack::ContentLength do end it "set Content-Length on Array bodies if none is set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } response = content_length(app).call(request) response[1]['Content-Length'].must_equal '13' end @@ -22,13 +24,13 @@ describe Rack::ContentLength do body = lambda { "Hello World!" } def body.each ; yield call ; end - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] } response = content_length(app).call(request) response[1]['Content-Length'].must_be_nil end it "not change Content-Length if it is already set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '1'}, "Hello, World!"] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Content-Length' => '1' }, "Hello, World!"] } response = content_length(app).call(request) response[1]['Content-Length'].must_equal '1' end @@ -36,13 +38,13 @@ describe Rack::ContentLength do it "not set Content-Length on 304 responses" do app = lambda { |env| [304, {}, []] } response = content_length(app).call(request) - response[1]['Content-Length'].must_equal nil + response[1]['Content-Length'].must_be_nil end it "not set Content-Length when Transfer-Encoding is chunked" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked'}, []] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked' }, []] } response = content_length(app).call(request) - response[1]['Content-Length'].must_equal nil + response[1]['Content-Length'].must_be_nil end # Using "Connection: close" for this is fairly contended. It might be useful @@ -51,7 +53,7 @@ describe Rack::ContentLength do # should "not force a Content-Length when Connection:close" do # app = lambda { |env| [200, {'Connection' => 'close'}, []] } # response = content_length(app).call({}) - # response[1]['Content-Length'].must_equal nil + # response[1]['Content-Length'].must_be_nil # end it "close bodies that need to be closed" do @@ -62,9 +64,9 @@ describe Rack::ContentLength do def to_ary; end end.new(%w[one two three]) - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] } response = content_length(app).call(request) - body.closed.must_equal nil + body.closed.must_be_nil response[2].close body.closed.must_equal true end @@ -77,7 +79,7 @@ describe Rack::ContentLength do def to_ary; end end.new(%w[one two three]) - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] } response = content_length(app).call(request) expected = %w[one two three] response[1]['Content-Length'].must_equal expected.join.size.to_s diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb index 281879f3..53f1d172 100644 --- a/test/spec_content_type.rb +++ b/test/spec_content_type.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/content_type' require 'rack/lint' require 'rack/mock' @@ -26,21 +28,31 @@ describe Rack::ContentType do end it "not change Content-Type if it is already set" do - app = lambda { |env| [200, {'Content-Type' => 'foo/bar'}, "Hello, World!"] } + app = lambda { |env| [200, { 'Content-Type' => 'foo/bar' }, "Hello, World!"] } headers = content_type(app).call(request)[1] headers['Content-Type'].must_equal 'foo/bar' end it "detect Content-Type case insensitive" do - app = lambda { |env| [200, {'CONTENT-Type' => 'foo/bar'}, "Hello, World!"] } + app = lambda { |env| [200, { 'CONTENT-Type' => 'foo/bar' }, "Hello, World!"] } headers = content_type(app).call(request)[1] - headers.to_a.select { |k,v| k.downcase == "content-type" }. - must_equal [["CONTENT-Type","foo/bar"]] + headers.to_a.select { |k, v| k.downcase == "content-type" }. + must_equal [["CONTENT-Type", "foo/bar"]] + end + + [100, 204, 304].each do |code| + it "not set Content-Type on #{code} responses" do + app = lambda { |env| [code, {}, []] } + response = content_type(app, "text/html").call(request) + response[1]['Content-Type'].must_be_nil + end end - it "not set Content-Type on 304 responses" do - app = lambda { |env| [304, {}, []] } - response = content_type(app, "text/html").call(request) - response[1]['Content-Type'].must_equal nil + ['100', '204', '304'].each do |code| + it "not set Content-Type on #{code} responses if status is a string" do + app = lambda { |env| [code, {}, []] } + response = content_type(app, "text/html").call(request) + response[1]['Content-Type'].must_be_nil + end end end diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb index ba7ec5d3..75244dcc 100644 --- a/test/spec_deflater.rb +++ b/test/spec_deflater.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'stringio' require 'time' # for Time#httpdate require 'rack/deflater' @@ -44,6 +46,8 @@ describe Rack::Deflater do [accept_encoding, accept_encoding.dup] end + start = Time.now.to_i + # build response status, headers, body = build_response( options['app_status'] || expected_status, @@ -57,7 +61,7 @@ describe Rack::Deflater do # verify body unless options['skip_body_verify'] - body_text = '' + body_text = ''.dup body.each { |part| body_text << part } deflated_body = case expected_encoding @@ -67,6 +71,13 @@ describe Rack::Deflater do when 'gzip' io = StringIO.new(body_text) gz = Zlib::GzipReader.new(io) + mtime = gz.mtime.to_i + if last_mod = headers['Last-Modified'] + Time.httpdate(last_mod).to_i.must_equal mtime + else + mtime.must_be(:<=, Time.now.to_i) + mtime.must_be(:>=, start.to_i) + end tmp = gz.read gz.close tmp @@ -81,13 +92,35 @@ describe Rack::Deflater do yield(status, headers, body) if block_given? end + # automatic gzip detection (streamable) + def auto_inflater + Zlib::Inflate.new(32 + Zlib::MAX_WBITS) + end + + def deflate_or_gzip + { 'deflate, gzip' => 'gzip' } + end + it 'be able to deflate bodies that respond to each' do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end - verify(200, 'foobar', 'deflate', { 'app_body' => app_body }) do |status, headers, body| + 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 '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' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) @@ -98,15 +131,15 @@ describe Rack::Deflater do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end - verify(200, app_body, 'deflate', { 'skip_body_verify' => true }) do |status, headers, body| + verify(200, app_body, deflate_or_gzip, { 'skip_body_verify' => true }) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) buf = [] - inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + inflater = auto_inflater body.each { |part| buf << inflater.inflate(part) } buf << inflater.finish @@ -118,32 +151,33 @@ describe Rack::Deflater do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end opts = { 'skip_body_verify' => true } - verify(200, app_body, 'deflate', opts) do |status, headers, body| + verify(200, app_body, 'gzip', opts) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) buf = [] - inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) + inflater = auto_inflater FakeDisconnect = Class.new(RuntimeError) assert_raises(FakeDisconnect, "not Zlib::DataError not raised") do body.each do |part| - buf << inflater.inflate(part) + tmp = inflater.inflate(part) + buf << tmp if tmp.bytesize > 0 raise FakeDisconnect end end - assert_raises(Zlib::BufError) { inflater.finish } + inflater.finish buf.must_equal(%w(foo)) end end # TODO: This is really just a special case of the above... it 'be able to deflate String bodies' do - verify(200, 'Hello world!', 'deflate') do |status, headers, body| + verify(200, 'Hello world!', deflate_or_gzip) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'deflate', + 'Content-Encoding' => 'gzip', 'Vary' => 'Accept-Encoding', 'Content-Type' => 'text/plain' }) @@ -280,7 +314,7 @@ describe Rack::Deflater do 'Content-Encoding' => 'identity' } } - verify(200, 'Hello World!', 'deflate', options) + verify(200, 'Hello World!', deflate_or_gzip, options) end it "deflate if content-type matches :include" do @@ -289,7 +323,7 @@ describe Rack::Deflater do 'Content-Type' => 'text/plain' }, 'deflater_options' => { - :include => %w(text/plain) + include: %w(text/plain) } } verify(200, 'Hello World!', 'gzip', options) @@ -301,7 +335,7 @@ describe Rack::Deflater do 'Content-Type' => 'text/plain; charset=us-ascii' }, 'deflater_options' => { - :include => %w(text/plain) + include: %w(text/plain) } } verify(200, 'Hello World!', 'gzip', options) @@ -310,7 +344,7 @@ describe Rack::Deflater do it "not deflate if content-type is not set but given in :include" do options = { 'deflater_options' => { - :include => %w(text/plain) + include: %w(text/plain) } } verify(304, 'Hello World!', { 'gzip' => nil }, options) @@ -322,7 +356,7 @@ describe Rack::Deflater do 'Content-Type' => 'text/plain' }, 'deflater_options' => { - :include => %w(text/json) + include: %w(text/json) } } verify(200, 'Hello World!', { 'gzip' => nil }, options) @@ -331,16 +365,16 @@ describe Rack::Deflater do it "deflate response if :if lambda evaluates to true" do options = { 'deflater_options' => { - :if => lambda { |env, status, headers, body| true } + if: lambda { |env, status, headers, body| true } } } - verify(200, 'Hello World!', 'deflate', options) + verify(200, 'Hello World!', deflate_or_gzip, options) end it "not deflate if :if lambda evaluates to false" do options = { 'deflater_options' => { - :if => lambda { |env, status, headers, body| false } + if: lambda { |env, status, headers, body| false } } } verify(200, 'Hello World!', { 'gzip' => nil }, options) @@ -354,7 +388,7 @@ describe Rack::Deflater do 'Content-Length' => response_len.to_s }, 'deflater_options' => { - :if => lambda { |env, status, headers, body| + if: lambda { |env, status, headers, body| headers['Content-Length'].to_i >= response_len } } @@ -362,4 +396,38 @@ describe Rack::Deflater do verify(200, response, 'gzip', options) end + + it 'will honor sync: false to avoid unnecessary flushing' do + app_body = Object.new + class << app_body + def each + (0..20).each { |i| yield "hello\n" } + end + end + + options = { + 'deflater_options' => { sync: false }, + 'app_body' => app_body, + 'skip_body_verify' => true, + } + verify(200, app_body, deflate_or_gzip, options) do |status, headers, body| + headers.must_equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + + buf = ''.dup + raw_bytes = 0 + inflater = auto_inflater + body.each do |part| + raw_bytes += part.bytesize + buf << inflater.inflate(part) + end + buf << inflater.finish + expect = "hello\n" * 21 + buf.must_equal expect + raw_bytes.must_be(:<, expect.bytesize) + end + end end diff --git a/test/spec_directory.rb b/test/spec_directory.rb index 42bdea9f..8635ec90 100644 --- a/test/spec_directory.rb +++ b/test/spec_directory.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/directory' require 'rack/lint' require 'rack/mock' @@ -7,7 +9,7 @@ require 'fileutils' describe Rack::Directory do DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT - FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, ['passed!']] } + FILE_CATCH = proc{|env| [200, { 'Content-Type' => 'text/plain', "Content-Length" => "7" }, ['passed!']] } attr_reader :app @@ -23,11 +25,11 @@ describe Rack::Directory do FileUtils.touch File.join(full_dir, "omg.txt") app = Rack::Directory.new(dir, FILE_CATCH) env = Rack::MockRequest.env_for("/#{plus_dir}/") - status,_,body = app.call env + status, _, body = app.call env assert_equal 200, status - str = '' + str = ''.dup body.each { |x| str << x } assert_match "foo+bar", str end @@ -109,11 +111,11 @@ describe Rack::Directory do FileUtils.touch File.join(full_dir, "omg omg.txt") app = Rack::Directory.new(dir, FILE_CATCH) env = Rack::MockRequest.env_for(Rack::Utils.escape_path("/#{space_dir}/")) - status,_,body = app.call env + status, _, body = app.call env assert_equal 200, status - str = '' + str = ''.dup body.each { |x| str << x } assert_match "/foo%20bar/omg%20omg.txt", str end diff --git a/test/spec_etag.rb b/test/spec_etag.rb index 10ee2bd0..750ceaac 100644 --- a/test/spec_etag.rb +++ b/test/spec_etag.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/etag' require 'rack/lint' require 'rack/mock' @@ -20,79 +22,79 @@ describe Rack::ETag do end it "set ETag if none is set if status is 200" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app).call(request) response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" end it "set ETag if none is set if status is 201" do - app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } + app = lambda { |env| [201, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app).call(request) response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" end it "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do - app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } + app = lambda { |env| [201, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app).call(request) response[1]['Cache-Control'].must_equal 'max-age=0, private, must-revalidate' end it "set Cache-Control to chosen one if none is set" do - app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } + app = lambda { |env| [201, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app, nil, 'public').call(request) response[1]['Cache-Control'].must_equal 'public' end it "set a given Cache-Control even if digest could not be calculated" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, []] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } response = etag(app, 'no-cache').call(request) response[1]['Cache-Control'].must_equal 'no-cache' end it "not set Cache-Control if it is already set" do - app = lambda { |env| [201, {'Content-Type' => 'text/plain', 'Cache-Control' => 'public'}, ["Hello, World!"]] } + app = lambda { |env| [201, { 'Content-Type' => 'text/plain', 'Cache-Control' => 'public' }, ["Hello, World!"]] } response = etag(app).call(request) response[1]['Cache-Control'].must_equal 'public' end it "not set Cache-Control if directive isn't present" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app, nil, nil).call(request) - response[1]['Cache-Control'].must_equal nil + response[1]['Cache-Control'].must_be_nil end it "not change ETag if it is already set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'ETag' => '"abc"'}, ["Hello, World!"]] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'ETag' => '"abc"' }, ["Hello, World!"]] } response = etag(app).call(request) response[1]['ETag'].must_equal "\"abc\"" end it "not set ETag if body is empty" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate}, []] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate }, []] } response = etag(app).call(request) response[1]['ETag'].must_be_nil end it "not set ETag if Last-Modified is set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate}, ["Hello, World!"]] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate }, ["Hello, World!"]] } response = etag(app).call(request) response[1]['ETag'].must_be_nil end it "not set ETag if a sendfile_body is given" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, sendfile_body] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, sendfile_body] } response = etag(app).call(request) response[1]['ETag'].must_be_nil end it "not set ETag if a status is not 200 or 201" do - app = lambda { |env| [401, {'Content-Type' => 'text/plain'}, ['Access denied.']] } + app = lambda { |env| [401, { 'Content-Type' => 'text/plain' }, ['Access denied.']] } response = etag(app).call(request) response[1]['ETag'].must_be_nil end it "not set ETag if no-cache is given" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Cache-Control' => 'no-cache, must-revalidate'}, ['Hello, World!']] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Cache-Control' => 'no-cache, must-revalidate' }, ['Hello, World!']] } response = etag(app).call(request) response[1]['ETag'].must_be_nil end diff --git a/test/spec_events.rb b/test/spec_events.rb index 7fc7b055..8c079361 100644 --- a/test/spec_events.rb +++ b/test/spec_events.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'helper' require 'rack/events' diff --git a/test/spec_fastcgi.rb b/test/spec_fastcgi.rb index 5a48327b..db662a57 100644 --- a/test/spec_fastcgi.rb +++ b/test/spec_fastcgi.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'helper' if defined? LIGHTTPD_PID @@ -36,7 +38,7 @@ describe Rack::Handler::FastCGI do it "have rack headers" do GET("/test.fcgi") - response["rack.version"].must_equal [1,3] + response["rack.version"].must_equal [1, 3] assert_equal false, response["rack.multithread"] assert_equal true, response["rack.multiprocess"] assert_equal false, response["rack.run_once"] @@ -60,7 +62,7 @@ describe Rack::Handler::FastCGI do end it "have CGI headers on POST" do - POST("/test.fcgi", {"rack-form-data" => "23"}, {'X-test-header' => '42'}) + POST("/test.fcgi", { "rack-form-data" => "23" }, { 'X-test-header' => '42' }) status.must_equal 200 response["REQUEST_METHOD"].must_equal "POST" response["SCRIPT_NAME"].must_equal "/test.fcgi" @@ -71,7 +73,7 @@ describe Rack::Handler::FastCGI do end it "support HTTP auth" do - GET("/test.fcgi", {:user => "ruth", :passwd => "secret"}) + GET("/test.fcgi", { user: "ruth", passwd: "secret" }) response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" end diff --git a/test/spec_file.rb b/test/spec_file.rb index 3106e629..ba713c61 100644 --- a/test/spec_file.rb +++ b/test/spec_file.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/file' require 'rack/lint' require 'rack/mock' @@ -15,11 +17,11 @@ describe Rack::File do File.write File.join(dir, "you+me.txt"), "hello world" app = file(dir) env = Rack::MockRequest.env_for("/you+me.txt") - status,_,body = app.call env + status, _, body = app.call env assert_equal 200, status - str = '' + str = ''.dup body.each { |x| str << x } assert_match "hello world", str end @@ -156,8 +158,8 @@ describe Rack::File do res.status.must_equal 206 res["Content-Length"].must_equal "12" - res["Content-Range"].must_equal "bytes 22-33/193" - res.body.must_equal "-*- ruby -*-" + res["Content-Range"].must_equal "bytes 22-33/208" + res.body.must_equal "frozen_strin" end it "return error for unsatisfiable byte range" do @@ -166,7 +168,7 @@ describe Rack::File do res = Rack::MockResponse.new(*file(DOCROOT).call(env)) res.status.must_equal 416 - res["Content-Range"].must_equal "bytes */193" + res["Content-Range"].must_equal "bytes */208" end it "support custom http headers" do @@ -184,8 +186,8 @@ describe Rack::File do status, heads, _ = file(DOCROOT).call(env) status.must_equal 200 - heads['Cache-Control'].must_equal nil - heads['Access-Control-Allow-Origin'].must_equal nil + heads['Cache-Control'].must_be_nil + heads['Access-Control-Allow-Origin'].must_be_nil end it "only support GET, HEAD, and OPTIONS requests" do @@ -218,7 +220,7 @@ describe Rack::File do req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) res = req.head "/cgi/test" res.must_be :successful? - res['Content-Length'].must_equal "193" + res['Content-Length'].must_equal "208" end it "default to a mime type of text/plain" do @@ -239,7 +241,7 @@ describe Rack::File do req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil))) res = req.get "/cgi/test" res.must_be :successful? - res['Content-Type'].must_equal nil + res['Content-Type'].must_be_nil end it "return error when file not found for head request" do @@ -248,4 +250,17 @@ describe Rack::File do res.body.must_be :empty? end + class MyFile < Rack::File + def response_body + "hello world" + end + end + + it "behaves gracefully if response_body is present" do + file = Rack::Lint.new MyFile.new(DOCROOT) + res = Rack::MockRequest.new(file).get("/cgi/test") + + res.must_be :ok? + end + end diff --git a/test/spec_handler.rb b/test/spec_handler.rb index dff474c9..5746dc22 100644 --- a/test/spec_handler.rb +++ b/test/spec_handler.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +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 17b4a349..f6f41a5d 100644 --- a/test/spec_head.rb +++ b/test/spec_head.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/head' require 'rack/lint' require 'rack/mock' @@ -8,7 +10,7 @@ describe Rack::Head do def test_response(headers = {}) body = StringIO.new "foo" app = lambda do |env| - [200, {"Content-type" => "test/plain", "Content-length" => "3"}, body] + [200, { "Content-type" => "test/plain", "Content-length" => "3" }, body] end request = Rack::MockRequest.env_for("/", headers) response = Rack::Lint.new(Rack::Head.new(app)).call(request) diff --git a/test/spec_lint.rb b/test/spec_lint.rb index 6d1c2c45..192f260f 100644 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'stringio' require 'tempfile' require 'rack/lint' @@ -11,7 +13,7 @@ describe Rack::Lint do it "pass valid request" do Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]] + [200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]] }).call(env({})).first.must_equal 200 end @@ -187,14 +189,14 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| - [200, {true=>false}, []] + [200, { true => false }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_equal "header key must be a string, was TrueClass" lambda { Rack::Lint.new(lambda { |env| - [200, {"Status" => "404"}, []] + [200, { "Status" => "404" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/must not contain Status/) @@ -216,7 +218,7 @@ describe Rack::Lint do invalid_headers.each do |invalid_header| lambda { Rack::Lint.new(lambda { |env| - [200, {invalid_header => "text/plain"}, []] + [200, { invalid_header => "text/plain" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}"). message.must_equal("invalid header name: #{invalid_header}") @@ -224,20 +226,20 @@ describe Rack::Lint do valid_headers = 0.upto(127).map(&:chr) - invalid_headers valid_headers.each do |valid_header| Rack::Lint.new(lambda { |env| - [200, {valid_header => "text/plain"}, []] + [200, { valid_header => "text/plain" }, []] }).call(env({})).first.must_equal 200 end lambda { Rack::Lint.new(lambda { |env| - [200, {"Foo" => Object.new}, []] + [200, { "Foo" => Object.new }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_equal "a header value must be a String, but the value of 'Foo' is a Object" lambda { Rack::Lint.new(lambda { |env| - [200, {"Foo" => [1, 2, 3]}, []] + [200, { "Foo" => [1, 2, 3] }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_equal "a header value must be a String, but the value of 'Foo' is a Array" @@ -245,14 +247,14 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| - [200, {"Foo-Bar" => "text\000plain"}, []] + [200, { "Foo-Bar" => "text\000plain" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/invalid header/) # line ends (010).must_be :allowed in header values.? Rack::Lint.new(lambda { |env| - [200, {"Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []] + [200, { "Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []] }).call(env({})).first.must_equal 200 # non-Hash header responses.must_be :allowed? @@ -269,10 +271,10 @@ describe Rack::Lint do # }.must_raise(Rack::Lint::LintError). # message.must_match(/No Content-Type/) - [100, 101, 204, 205, 304].each do |status| + [100, 101, 204, 304].each do |status| lambda { Rack::Lint.new(lambda { |env| - [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [status, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/Content-Type header found/) @@ -280,10 +282,10 @@ describe Rack::Lint do end it "notice content-length errors" do - [100, 101, 204, 205, 304].each do |status| + [100, 101, 204, 304].each do |status| lambda { Rack::Lint.new(lambda { |env| - [status, {"Content-length" => "0"}, []] + [status, { "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/Content-Length header found/) @@ -291,7 +293,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, []] + [200, { "Content-type" => "text/plain", "Content-Length" => "1" }, []] }).call(env({}))[2].each { } }.must_raise(Rack::Lint::LintError). message.must_match(/Content-Length header was 1, but should be 0/) @@ -300,7 +302,7 @@ describe Rack::Lint do it "notice body errors" do lambda { body = Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]] + [200, { "Content-type" => "text/plain", "Content-length" => "3" }, [1, 2, 3]] }).call(env({}))[2] body.each { |part| } }.must_raise(Rack::Lint::LintError). @@ -311,7 +313,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].gets("\r\n") - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/gets called with arguments/) @@ -319,7 +321,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(1, 2, 3) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/read called with too many arguments/) @@ -327,7 +329,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read("foo") - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/read called with non-integer and non-nil length/) @@ -335,7 +337,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(-1) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/read called with a negative length/) @@ -343,7 +345,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(nil, nil) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/read called with non-String buffer/) @@ -351,7 +353,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read(nil, 1) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/read called with non-String buffer/) @@ -359,7 +361,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].rewind(0) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/rewind called with arguments/) @@ -404,7 +406,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].gets - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env("rack.input" => weirdio)) }.must_raise(Rack::Lint::LintError). message.must_match(/gets didn't return a String/) @@ -412,7 +414,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].each { |x| } - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env("rack.input" => weirdio)) }.must_raise(Rack::Lint::LintError). message.must_match(/each didn't yield a String/) @@ -420,7 +422,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env("rack.input" => weirdio)) }.must_raise(Rack::Lint::LintError). message.must_match(/read didn't return nil or a String/) @@ -428,7 +430,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].read - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env("rack.input" => eof_weirdio)) }.must_raise(Rack::Lint::LintError). message.must_match(/read\(nil\) returned nil on EOF/) @@ -436,7 +438,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].rewind - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env("rack.input" => weirdio)) }.must_raise(Rack::Lint::LintError). message.must_match(/rewind raised Errno::ESPIPE/) @@ -445,7 +447,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.input"].close - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/close must not be called/) @@ -455,7 +457,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.errors"].write(42) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/write not called with a String/) @@ -463,7 +465,7 @@ describe Rack::Lint do lambda { Rack::Lint.new(lambda { |env| env["rack.errors"].close - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] }).call(env({})) }.must_raise(Rack::Lint::LintError). message.must_match(/close must not be called/) @@ -471,25 +473,25 @@ describe Rack::Lint do it "notice HEAD errors" do Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "test/plain", "Content-length" => "3"}, []] - }).call(env({"REQUEST_METHOD" => "HEAD"})).first.must_equal 200 + [200, { "Content-type" => "test/plain", "Content-length" => "3" }, []] + }).call(env({ "REQUEST_METHOD" => "HEAD" })).first.must_equal 200 lambda { Rack::Lint.new(lambda { |env| - [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]] - }).call(env({"REQUEST_METHOD" => "HEAD"}))[2].each { } + [200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]] + }).call(env({ "REQUEST_METHOD" => "HEAD" }))[2].each { } }.must_raise(Rack::Lint::LintError). message.must_match(/body was given for HEAD/) end def assert_lint(*args) - hello_str = "hello world" + hello_str = "hello world".dup hello_str.force_encoding(Encoding::ASCII_8BIT) Rack::Lint.new(lambda { |env| env["rack.input"].send(:read, *args) - [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []] - }).call(env({"rack.input" => StringIO.new(hello_str)})). + [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] + }).call(env({ "rack.input" => StringIO.new(hello_str) })). first.must_equal 201 end @@ -498,8 +500,8 @@ describe Rack::Lint do assert_lint 0 assert_lint 1 assert_lint nil - assert_lint nil, '' - assert_lint 1, '' + assert_lint nil, ''.dup + assert_lint 1, ''.dup end end diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb index fd4e7082..9f3b9a89 100644 --- a/test/spec_lobster.rb +++ b/test/spec_lobster.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +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 aa3efa54..cd9e1230 100644 --- a/test/spec_lock.rb +++ b/test/spec_lock.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/lock' require 'rack/mock' @@ -44,7 +46,7 @@ describe Rack::Lock do def each; %w{ hi mom }.each { |x| yield x }; end }.new - app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] }) + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, response] }) response = app.call(env)[2] list = [] response.each { |x| list << x } @@ -58,7 +60,7 @@ describe Rack::Lock do res = ['Hello World'] def res.to_path ; "/tmp/hello.txt" ; end - app = Rack::Lock.new(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] }, lock) + app = Rack::Lock.new(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, res] }, lock) body = app.call(env)[2] body.must_respond_to :to_path @@ -66,11 +68,11 @@ describe Rack::Lock do end it 'not delegate to_path if body does not implement it' do - env = Rack::MockRequest.env_for("/") + env = Rack::MockRequest.env_for("/") res = ['Hello World'] - app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] }) + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, res] }) body = app.call(env)[2] body.wont_respond_to :to_path @@ -85,7 +87,7 @@ describe Rack::Lock do def close; @close_called = true; end }.new - app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] }) + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, response] }) app.call(env) response.close_called.must_equal false response.close @@ -96,7 +98,7 @@ describe Rack::Lock do lock = Lock.new env = Rack::MockRequest.env_for("/") response = Object.new - app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] }, lock) + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, response] }, lock) lock.synchronized.must_equal false response = app.call(env)[2] lock.synchronized.must_equal true @@ -106,7 +108,7 @@ describe Rack::Lock do it "return value from app" do env = Rack::MockRequest.env_for("/") - body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }] + body = [200, { "Content-Type" => "text/plain" }, %w{ hi mom }] app = lock_app(lambda { |inner_env| body }) res = app.call(env) @@ -118,7 +120,7 @@ describe Rack::Lock do it "call synchronize on lock" do lock = Lock.new env = Rack::MockRequest.env_for("/") - app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] }, lock) + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, %w{ a b c }] }, lock) lock.synchronized.must_equal false app.call(env) lock.synchronized.must_equal true @@ -143,11 +145,12 @@ describe Rack::Lock do it "set multithread flag to false" do app = lock_app(lambda { |env| env['rack.multithread'].must_equal false - [200, {"Content-Type" => "text/plain"}, %w{ a b c }] + [200, { "Content-Type" => "text/plain" }, %w{ a b c }] }, false) env = Rack::MockRequest.env_for("/") env['rack.multithread'].must_equal true - app.call(env) + _, _, body = app.call(env) + body.close env['rack.multithread'].must_equal true end @@ -157,7 +160,7 @@ describe Rack::Lock do env['rack.multithread'].must_equal true super end - }.new(lambda { |env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] }) + }.new(lambda { |env| [200, { "Content-Type" => "text/plain" }, %w{ a b c }] }) Rack::Lint.new(app).call(Rack::MockRequest.env_for("/")) end @@ -169,7 +172,7 @@ describe Rack::Lock do def unlock() @unlocked = true end end.new env = Rack::MockRequest.env_for("/") - app = lock_app(proc { [200, {"Content-Type" => "text/plain"}, []] }, lock) + app = lock_app(proc { [200, { "Content-Type" => "text/plain" }, []] }, lock) lambda { app.call(env) }.must_raise Exception lock.unlocked?.must_equal false end @@ -179,7 +182,7 @@ describe Rack::Lock do attr_reader :env def initialize(env) @env = env end end - app = Rack::Lock.new lambda { |env| [200, {"Content-Type" => "text/plain"}, proxy.new(env)] } + app = Rack::Lock.new lambda { |env| [200, { "Content-Type" => "text/plain" }, proxy.new(env)] } response = app.call(Rack::MockRequest.env_for("/"))[2] response.env['rack.multithread'].must_equal false end @@ -191,4 +194,13 @@ describe Rack::Lock do lambda { app.call(env) }.must_raise Exception lock.synchronized.must_equal false end + + it "not replace the environment" do + env = Rack::MockRequest.env_for("/") + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, [inner_env.object_id.to_s]] }) + + _, _, body = app.call(env) + + body.to_enum.to_a.must_equal [env.object_id.to_s] + end end diff --git a/test/spec_logger.rb b/test/spec_logger.rb index ea503e1d..f453b14d 100644 --- a/test/spec_logger.rb +++ b/test/spec_logger.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'stringio' require 'rack/lint' require 'rack/logger' @@ -11,7 +13,7 @@ describe Rack::Logger do log.info("Program started") log.warn("Nothing to do!") - [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] + [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } it "conform to Rack::Lint" do diff --git a/test/spec_media_type.rb b/test/spec_media_type.rb index ef054364..7d52b4d4 100644 --- a/test/spec_media_type.rb +++ b/test/spec_media_type.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/media_type' describe Rack::MediaType do @@ -8,7 +10,7 @@ describe Rack::MediaType do before { @content_type = nil } it '#type is nil' do - Rack::MediaType.type(@content_type).must_equal nil + Rack::MediaType.type(@content_type).must_be_nil end it '#params is empty' do @@ -23,7 +25,7 @@ describe Rack::MediaType do Rack::MediaType.type(@content_type).must_equal 'application/text' end - it '#params is empty' do + it '#params is empty' do Rack::MediaType.params(@content_type).must_equal @empty_hash end end diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb index 14ace0b1..6b01f7c9 100644 --- a/test/spec_method_override.rb +++ b/test/spec_method_override.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'stringio' require 'rack/method_override' require 'rack/mock' @@ -6,19 +8,33 @@ require 'rack/mock' describe Rack::MethodOverride do def app Rack::Lint.new(Rack::MethodOverride.new(lambda {|e| - [200, {"Content-Type" => "text/plain"}, []] + [200, { "Content-Type" => "text/plain" }, []] })) end it "not affect GET requests" do - env = Rack::MockRequest.env_for("/?_method=delete", :method => "GET") + env = Rack::MockRequest.env_for("/?_method=delete", method: "GET") app.call env env["REQUEST_METHOD"].must_equal "GET" end + it "sets rack.errors for invalid UTF8 _method values" do + errors = StringIO.new + env = Rack::MockRequest.env_for("/", + :method => "POST", + :input => "_method=\xBF".b, + Rack::RACK_ERRORS => errors) + + app.call env + + errors.rewind + errors.read.must_equal "Invalid string for method\n" + env["REQUEST_METHOD"].must_equal "POST" + end + it "modify REQUEST_METHOD for POST requests when _method parameter is set" do - env = Rack::MockRequest.env_for("/", :method => "POST", :input => "_method=put") + env = Rack::MockRequest.env_for("/", method: "POST", input: "_method=put") app.call env env["REQUEST_METHOD"].must_equal "PUT" @@ -35,14 +51,14 @@ describe Rack::MethodOverride do end it "not modify REQUEST_METHOD if the method is unknown" do - env = Rack::MockRequest.env_for("/", :method => "POST", :input => "_method=foo") + env = Rack::MockRequest.env_for("/", method: "POST", input: "_method=foo") app.call env env["REQUEST_METHOD"].must_equal "POST" end it "not modify REQUEST_METHOD when _method is nil" do - env = Rack::MockRequest.env_for("/", :method => "POST", :input => "foo=bar") + env = Rack::MockRequest.env_for("/", method: "POST", input: "foo=bar") app.call env env["REQUEST_METHOD"].must_equal "POST" @@ -50,8 +66,8 @@ describe Rack::MethodOverride do it "store the original REQUEST_METHOD prior to overriding" do env = Rack::MockRequest.env_for("/", - :method => "POST", - :input => "_method=options") + method: "POST", + input: "_method=options") app.call env env["rack.methodoverride.original_method"].must_equal "POST" @@ -66,16 +82,29 @@ EOF "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", "CONTENT_LENGTH" => input.size.to_s, :method => "POST", :input => input) - begin - app.call env - rescue EOFError - end + app.call env env["REQUEST_METHOD"].must_equal "POST" end + it "writes error to RACK_ERRORS when given invalid multipart form data" do + input = <<EOF +--AaB03x\r +content-disposition: form-data; name="huge"; filename="huge"\r +EOF + env = Rack::MockRequest.env_for("/", + "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x", + "CONTENT_LENGTH" => input.size.to_s, + Rack::RACK_ERRORS => StringIO.new, + :method => "POST", :input => input) + Rack::MethodOverride.new(proc { [200, { "Content-Type" => "text/plain" }, []] }).call env + + env[Rack::RACK_ERRORS].rewind + env[Rack::RACK_ERRORS].read.must_match /Bad request content body/ + 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%)") + env = Rack::MockRequest.env_for("/", method: "POST", input: "(%bad-params%)") app.call env env["REQUEST_METHOD"].must_equal "POST" diff --git a/test/spec_mime.rb b/test/spec_mime.rb index cd40b4b5..8d1ca256 100644 --- a/test/spec_mime.rb +++ b/test/spec_mime.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/mime' describe Rack::Mime do @@ -19,7 +21,7 @@ describe Rack::Mime do end it "should support null fallbacks" do - Rack::Mime.mime_type('.nothing', nil).must_equal nil + Rack::Mime.mime_type('.nothing', nil).must_be_nil end it "should match exact mimes" do diff --git a/test/spec_mock.rb b/test/spec_mock.rb index a4d4e5a5..d7246d3f 100644 --- a/test/spec_mock.rb +++ b/test/spec_mock.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'yaml' require 'rack/lint' require 'rack/mock' @@ -14,9 +16,15 @@ app = Rack::Lint.new(lambda { |env| end body = req.head? ? "" : env.to_yaml - Rack::Response.new(body, - req.GET["status"] || 200, - "Content-Type" => "text/yaml").finish + response = Rack::Response.new( + body, + 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("persistent_test", { value: "persistent_test", max_age: 15552000, path: "/" }) + response.finish }) describe Rack::MockRequest do @@ -54,44 +62,53 @@ describe Rack::MockRequest do end it "allow GET/POST/PUT/DELETE/HEAD" do - res = Rack::MockRequest.new(app).get("", :input => "foo") + res = Rack::MockRequest.new(app).get("", input: "foo") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "GET" - res = Rack::MockRequest.new(app).post("", :input => "foo") + res = Rack::MockRequest.new(app).post("", input: "foo") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "POST" - res = Rack::MockRequest.new(app).put("", :input => "foo") + res = Rack::MockRequest.new(app).put("", input: "foo") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "PUT" - res = Rack::MockRequest.new(app).patch("", :input => "foo") + res = Rack::MockRequest.new(app).patch("", input: "foo") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "PATCH" - res = Rack::MockRequest.new(app).delete("", :input => "foo") + res = Rack::MockRequest.new(app).delete("", input: "foo") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "DELETE" - Rack::MockRequest.env_for("/", :method => "HEAD")["REQUEST_METHOD"] - .must_equal "HEAD" + Rack::MockRequest.env_for("/", method: "HEAD")["REQUEST_METHOD"] + .must_equal "HEAD" - Rack::MockRequest.env_for("/", :method => "OPTIONS")["REQUEST_METHOD"] - .must_equal "OPTIONS" + Rack::MockRequest.env_for("/", method: "OPTIONS")["REQUEST_METHOD"] + .must_equal "OPTIONS" end it "set content length" do - env = Rack::MockRequest.env_for("/", :input => "foo") + env = Rack::MockRequest.env_for("/", input: "foo") + env["CONTENT_LENGTH"].must_equal "3" + + env = Rack::MockRequest.env_for("/", input: StringIO.new("foo")) + env["CONTENT_LENGTH"].must_equal "3" + + env = Rack::MockRequest.env_for("/", input: Tempfile.new("name").tap { |t| t << "foo" }) env["CONTENT_LENGTH"].must_equal "3" + + env = Rack::MockRequest.env_for("/", input: IO.pipe.first) + env["CONTENT_LENGTH"].must_be_nil end it "allow posting" do - res = Rack::MockRequest.new(app).get("", :input => "foo") + res = Rack::MockRequest.new(app).get("", input: "foo") env = YAML.load(res.body) env["mock.postdata"].must_equal "foo" - res = Rack::MockRequest.new(app).post("", :input => StringIO.new("foo")) + res = Rack::MockRequest.new(app).post("", input: StringIO.new("foo")) env = YAML.load(res.body) env["mock.postdata"].must_equal "foo" end @@ -146,7 +163,7 @@ describe Rack::MockRequest do end it "accept params and build query string 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: { bar: "1" } }) env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "GET" env["QUERY_STRING"].must_include "baz=2" @@ -156,7 +173,7 @@ describe Rack::MockRequest do 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[bar]=1") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "GET" env["QUERY_STRING"].must_include "baz=2" @@ -166,7 +183,7 @@ describe Rack::MockRequest do end it "accept params and build url encoded 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: { bar: "1" } }) env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "POST" env["QUERY_STRING"].must_equal "" @@ -176,7 +193,7 @@ describe Rack::MockRequest do 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[bar]=1") env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "POST" env["QUERY_STRING"].must_equal "" @@ -187,7 +204,7 @@ describe Rack::MockRequest do it "accept params and build multipart encoded params for POST requests" do files = Rack::Multipart::UploadedFile.new(File.join(File.dirname(__FILE__), "multipart", "file1.txt")) - res = Rack::MockRequest.new(app).post("/foo", :params => { "submit-name" => "Larry", "files" => files }) + res = Rack::MockRequest.new(app).post("/foo", params: { "submit-name" => "Larry", "files" => files }) env = YAML.load(res.body) env["REQUEST_METHOD"].must_equal "POST" env["QUERY_STRING"].must_equal "" @@ -199,18 +216,35 @@ describe Rack::MockRequest do it "behave valid according to the Rack spec" do url = "https://bla.example.org:9292/meh/foo?bar" - Rack::MockRequest.new(app).get(url, :lint => true). + Rack::MockRequest.new(app).get(url, lint: true). must_be_kind_of Rack::MockResponse end it "call close on the original body object" do called = false body = Rack::BodyProxy.new(['hi']) { called = true } - capp = proc { |e| [200, {'Content-Type' => 'text/plain'}, body] } + capp = proc { |e| [200, { 'Content-Type' => 'text/plain' }, body] } called.must_equal false - Rack::MockRequest.new(capp).get('/', :lint => true) + Rack::MockRequest.new(capp).get('/', lint: true) called.must_equal true end + + it "defaults encoding to ASCII 8BIT" do + req = Rack::MockRequest.env_for("/foo") + + keys = [ + Rack::REQUEST_METHOD, + Rack::SERVER_NAME, + Rack::SERVER_PORT, + Rack::QUERY_STRING, + Rack::PATH_INFO, + Rack::HTTPS, + Rack::RACK_URL_SCHEME + ] + keys.each do |k| + assert_equal Encoding::ASCII_8BIT, req[k].encoding + end + end end describe Rack::MockResponse do @@ -231,7 +265,7 @@ describe Rack::MockResponse do res = Rack::MockRequest.new(app).get("/?status=307") res.must_be :redirect? - res = Rack::MockRequest.new(app).get("/?status=201", :lint => true) + res = Rack::MockRequest.new(app).get("/?status=201", lint: true) res.must_be :empty? end @@ -246,6 +280,42 @@ describe Rack::MockResponse do res.location.must_be_nil end + it "provide access to session cookies" do + 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.path.must_equal "/" + session_cookie.secure.must_equal false + session_cookie.expires.must_be_nil + end + + it "provide access to persistent cookies" do + res = Rack::MockRequest.new(app).get("") + persistent_cookie = res.cookie("persistent_test") + persistent_cookie.value[0].must_equal "persistent_test" + persistent_cookie.domain.must_be_nil + persistent_cookie.path.must_equal "/" + persistent_cookie.secure.must_equal false + persistent_cookie.expires.wont_be_nil + persistent_cookie.expires.must_be :<, (Time.now + 15552000) + end + + it "provide access to secure cookies" do + 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.path.must_equal "/" + secure_cookie.secure.must_equal true + secure_cookie.expires.must_be_nil + end + + it "return nil if a non existent cookie is requested" do + res = Rack::MockRequest.new(app).get("") + res.cookie("i_dont_exist").must_be_nil + end + it "provide access to the HTTP body" do res = Rack::MockRequest.new(app).get("") res.body.must_match(/rack/) @@ -253,7 +323,7 @@ describe Rack::MockResponse do end it "provide access to the Rack errors" do - res = Rack::MockRequest.new(app).get("/?error=foo", :lint => true) + res = Rack::MockRequest.new(app).get("/?error=foo", lint: true) res.must_be :ok? res.errors.wont_be :empty? res.errors.must_include "foo" @@ -269,7 +339,7 @@ describe Rack::MockResponse do it "optionally make Rack errors fatal" do lambda { - Rack::MockRequest.new(app).get("/?error=foo", :fatal => true) + Rack::MockRequest.new(app).get("/?error=foo", fatal: true) }.must_raise Rack::MockRequest::FatalWarning end end diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index 80e49ccb..b029048e 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -1,6 +1,6 @@ -# coding: utf-8 +# frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/multipart' require 'rack/multipart/parser' @@ -27,7 +27,7 @@ describe Rack::Multipart do it "return nil if content type is not multipart" do env = Rack::MockRequest.env_for("/", "CONTENT_TYPE" => 'application/x-www-form-urlencoded') - Rack::Multipart.parse_multipart(env).must_equal nil + Rack::Multipart.parse_multipart(env).must_be_nil end it "parse multipart content when content type present but filename is not" do @@ -107,11 +107,6 @@ describe Rack::Multipart do def rd.rewind; end wr.sync = true - # mock out length to make this pipe look like a Tempfile - def rd.length - 1024 * 1024 * 8 - end - # write to a pipe in a background thread, this will write a lot # unless Rack (properly) shuts down the read end thr = Thread.new do @@ -136,7 +131,7 @@ describe Rack::Multipart do fixture = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", - "CONTENT_LENGTH" => rd.length.to_s, + "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s, :input => rd, } @@ -177,7 +172,7 @@ describe Rack::Multipart do c = Class.new(Rack::QueryParser::Params) do def initialize(*) super - @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)} + @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end query_parser = Rack::QueryParser.new c, 65536, 100 @@ -206,7 +201,7 @@ describe Rack::Multipart do it "parse multipart upload file using custom tempfile class" do env = Rack::MockRequest.env_for("/", multipart_fixture(:text)) - my_tempfile = "" + my_tempfile = "".dup env['rack.multipart.tempfile_factory'] = lambda { |filename, content_type| my_tempfile } params = Rack::Multipart.parse_multipart(env) params["files"][:tempfile].object_id.must_equal my_tempfile.object_id @@ -305,11 +300,17 @@ describe Rack::Multipart do params["files"][:filename].must_equal "bob's flowers.jpg" end + it "parse multipart form with a null byte in the filename" do + env = Rack::MockRequest.env_for '/', multipart_fixture(:filename_with_null_byte) + params = Rack::Multipart.parse_multipart(env) + params["files"][:filename].must_equal "flowers.exe\u0000.jpg" + end + it "not include file params if no file was selected" do env = Rack::MockRequest.env_for("/", multipart_fixture(:none)) params = Rack::Multipart.parse_multipart(env) params["submit-name"].must_equal "Larry" - params["files"].must_equal nil + params["files"].must_be_nil params.keys.wont_include "files" end @@ -362,6 +363,19 @@ describe Rack::Multipart do params["files"][:tempfile].read.must_equal "contents" end + it "parse filename with plus character" do + env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_plus)) + params = Rack::Multipart.parse_multipart(env) + params["files"][:type].must_equal "application/octet-stream" + params["files"][:filename].must_equal "foo+bar" + params["files"][:head].must_equal "Content-Disposition: form-data; " + + "name=\"files\"; " + + "filename=\"foo+bar\"\r\n" + + "Content-Type: application/octet-stream\r\n" + params["files"][:name].must_equal "files" + params["files"][:tempfile].read.must_equal "contents" + end + it "parse filename with percent escaped quotes" do env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_percent_escaped_quotes)) params = Rack::Multipart.parse_multipart(env) @@ -476,7 +490,7 @@ Content-Type: image/jpeg\r it "builds nested multipart body" do files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt")) - data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => files}]) + data = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => files }]) options = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", @@ -556,8 +570,8 @@ Content-Type: image/jpeg\r end it "return nil if no UploadedFiles were used" do - data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => "contents"}]) - data.must_equal nil + data = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => "contents" }]) + data.must_be_nil end it "raise ArgumentError if params is not a Hash" do @@ -583,7 +597,7 @@ EOF env = Rack::MockRequest.env_for("/", options) params = Rack::Multipart.parse_multipart(env) - params.must_equal "description"=>"Very very blue" + params.must_equal "description" => "Very very blue" end it "parse multipart upload with no content-length header" do @@ -682,7 +696,7 @@ true\r it "fallback to content-type for name" do rack_logo = File.read(multipart_file("rack-logo.png")) - data = <<-EOF + data = <<-EOF.dup --AaB03x\r Content-Type: text/plain\r \r @@ -701,7 +715,7 @@ Content-Type: image/png\r options = { "CONTENT_TYPE" => "multipart/related; boundary=AaB03x", "CONTENT_LENGTH" => data.bytesize.to_s, - :input => StringIO.new(data) + :input => StringIO.new(data.dup) } env = Rack::MockRequest.env_for("/", options) params = Rack::Multipart.parse_multipart(env) diff --git a/test/spec_null_logger.rb b/test/spec_null_logger.rb index 3002d97f..1037c9fa 100644 --- a/test/spec_null_logger.rb +++ b/test/spec_null_logger.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/mock' require 'rack/null_logger' @@ -7,14 +9,14 @@ describe Rack::NullLogger do it "act as a noop logger" do app = lambda { |env| env['rack.logger'].warn "b00m" - [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] + [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } logger = Rack::Lint.new(Rack::NullLogger.new(app)) res = logger.call(Rack::MockRequest.env_for) res[0..1].must_equal [ - 200, {'Content-Type' => 'text/plain'} + 200, { 'Content-Type' => 'text/plain' } ] res[2].to_enum.to_a.must_equal ["Hello, World!"] end diff --git a/test/spec_recursive.rb b/test/spec_recursive.rb index 4a60e600..e77d966d 100644 --- a/test/spec_recursive.rb +++ b/test/spec_recursive.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +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 74f2fa87..583a367e 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'stringio' require 'cgi' require 'rack/request' @@ -53,7 +55,7 @@ class RackRequestTest < Minitest::Spec req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) req.set_header 'foo', 'bar' hash = {} - req.each_header do |k,v| + req.each_header do |k, v| hash[k] = v end assert_equal 'bar', hash['foo'] @@ -230,7 +232,7 @@ class RackRequestTest < Minitest::Spec c = Class.new(Rack::QueryParser::Params) do def initialize(*) super - @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)} + @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end parser = Rack::QueryParser.new(c, 65536, 100) @@ -272,7 +274,7 @@ class RackRequestTest < Minitest::Spec old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3 begin - exp = {"foo"=>{"bar"=>{"baz"=>{"qux"=>"1"}}}} + exp = { "foo" => { "bar" => { "baz" => { "qux" => "1" } } } } make_request(nested_query).GET.must_equal exp lambda { make_request(plain_query).GET }.must_raise RangeError ensure @@ -298,7 +300,7 @@ class RackRequestTest < Minitest::Spec c = Class.new(Rack::QueryParser::Params) do def initialize(*) super - @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)} + @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end parser = Rack::QueryParser.new(c, 65536, 100) @@ -476,7 +478,7 @@ class RackRequestTest < Minitest::Spec req = make_request \ Rack::MockRequest.env_for("/") - req.referer.must_equal nil + req.referer.must_be_nil end it "extract user agent correctly" do @@ -486,25 +488,25 @@ class RackRequestTest < Minitest::Spec req = make_request \ Rack::MockRequest.env_for("/") - req.user_agent.must_equal nil + req.user_agent.must_be_nil end it "treat missing content type as nil" do req = make_request \ Rack::MockRequest.env_for("/") - req.content_type.must_equal nil + req.content_type.must_be_nil end it "treat empty content type as nil" do req = make_request \ Rack::MockRequest.env_for("/", "CONTENT_TYPE" => "") - req.content_type.must_equal nil + req.content_type.must_be_nil end it "return nil media type for empty content type" do req = make_request \ Rack::MockRequest.env_for("/", "CONTENT_TYPE" => "") - req.media_type.must_equal nil + req.media_type.must_be_nil end it "cache, but invalidates the cache" do @@ -572,6 +574,11 @@ class RackRequestTest < Minitest::Spec request.must_be :ssl? end + it "prevents scheme abuse" do + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_SCHEME' => 'a."><script>alert(1)</script>')) + request.scheme.must_equal 'http' + end + it "parse cookies" do req = make_request \ Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m") @@ -722,7 +729,7 @@ class RackRequestTest < Minitest::Spec end it "provide setters" do - req = make_request(e=Rack::MockRequest.env_for("")) + req = make_request(e = Rack::MockRequest.env_for("")) req.script_name.must_equal "" req.script_name = "/foo" req.script_name.must_equal "/foo" @@ -953,7 +960,7 @@ EOF --AaB03x\r content-disposition: form-data; name="huge"; filename="huge"\r \r -#{"x"*32768}\r +#{"x" * 32768}\r --AaB03x\r content-disposition: form-data; name="mean"; filename="mean"\r \r @@ -1072,7 +1079,7 @@ EOF content-disposition: form-data; name="fileupload"; filename="junk.a"\r content-type: application/octet-stream\r \r -#{[0x36,0xCF,0x0A,0xF8].pack('c*')}\r +#{[0x36, 0xCF, 0x0A, 0xF8].pack('c*')}\r --AaB03x--\r EOF @@ -1092,7 +1099,7 @@ EOF rack_input.rewind req = make_request Rack::MockRequest.env_for("/", - "rack.request.form_hash" => {'foo' => 'bar'}, + "rack.request.form_hash" => { 'foo' => 'bar' }, "rack.request.form_input" => rack_input, :input => rack_input) @@ -1103,10 +1110,10 @@ EOF app = lambda { |env| content = make_request(env).POST["file"].inspect size = content.bytesize - [200, {"Content-Type" => "text/html", "Content-Length" => size.to_s}, [content]] + [200, { "Content-Type" => "text/html", "Content-Length" => size.to_s }, [content]] } - input = <<EOF + input = <<EOF.dup --AaB03x\r content-disposition: form-data; name="reply"\r \r @@ -1216,6 +1223,20 @@ EOF res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '127.0.0.1, 3.4.5.6' res.body.must_equal '3.4.5.6' + # IPv6 format with optional port: "[2001:db8:cafe::17]:47011" + res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '[2001:db8:cafe::17]:47011' + res.body.must_equal '2001:db8:cafe::17' + + res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, [2001:db8:cafe::17]:47011' + res.body.must_equal '2001:db8:cafe::17' + + # IPv4 format with optional port: "192.0.2.43:47011" + res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '192.0.2.43:47011' + res.body.must_equal '192.0.2.43' + + res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, 192.0.2.43:47011' + res.body.must_equal '192.0.2.43' + res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => 'unknown,192.168.0.1' res.body.must_equal 'unknown' @@ -1281,28 +1302,45 @@ EOF res.body.must_equal '2.2.2.3' end - it "regard local addresses as proxies" do + it "preserves ip for trusted proxy chain" do + mock = Rack::MockRequest.new(Rack::Lint.new(ip_app)) + res = mock.get '/', + 'HTTP_X_FORWARDED_FOR' => '192.168.0.11, 192.168.0.7', + 'HTTP_CLIENT_IP' => '127.0.0.1' + res.body.must_equal '192.168.0.11' + + end + + it "uses a custom trusted proxy filter" do + old_ip = Rack::Request.ip_filter + Rack::Request.ip_filter = lambda { |ip| ip == 'foo' } + req = make_request(Rack::MockRequest.env_for("/")) + assert req.trusted_proxy?('foo') + Rack::Request.ip_filter = old_ip + end + + 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_equal nil - req.trusted_proxy?("example.org\n127.0.0.1").must_equal nil - req.trusted_proxy?("127.0.0.1\nexample.org").must_equal nil - req.trusted_proxy?("11.0.0.1").must_equal nil - req.trusted_proxy?("172.15.0.1").must_equal nil - req.trusted_proxy?("172.32.0.1").must_equal nil - req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal 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 @@ -1312,7 +1350,7 @@ EOF class MyRequest < Rack::Request def params - {:foo => "bar"} + { foo: "bar" } end end @@ -1325,7 +1363,7 @@ EOF req2 = MyRequest.new(env) req2.GET.must_equal "foo" => "bar" - req2.params.must_equal :foo => "bar" + req2.params.must_equal foo: "bar" end it "allow parent request to be instantiated after subclass request" do @@ -1333,7 +1371,7 @@ EOF req1 = MyRequest.new(env) req1.GET.must_equal "foo" => "bar" - req1.params.must_equal :foo => "bar" + req1.params.must_equal foo: "bar" req2 = make_request(env) req2.GET.must_equal "foo" => "bar" diff --git a/test/spec_response.rb b/test/spec_response.rb index 02e51435..3cd56664 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/response' require 'stringio' @@ -9,7 +11,7 @@ describe Rack::Response do cc = 'foo' response.cache_control = cc assert_equal cc, response.cache_control - assert_equal cc, response.to_a[2]['Cache-Control'] + assert_equal cc, response.to_a[1]['Cache-Control'] end it 'has an etag method' do @@ -17,7 +19,7 @@ describe Rack::Response do etag = 'foo' response.etag = etag assert_equal etag, response.etag - assert_equal etag, response.to_a[2]['ETag'] + assert_equal etag, response.to_a[1]['ETag'] end it "have sensible default values" do @@ -55,11 +57,21 @@ describe Rack::Response do it "can set and read headers" do response = Rack::Response.new - response["Content-Type"].must_equal nil + response["Content-Type"].must_be_nil response["Content-Type"] = "text/plain" 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" @@ -78,103 +90,121 @@ describe Rack::Response do it "can set cookies with the same name for multiple domains" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :domain => "sample.example.com"} - response.set_cookie "foo", {:value => "bar", :domain => ".example.com"} + response.set_cookie "foo", { value: "bar", domain: "sample.example.com" } + response.set_cookie "foo", { value: "bar", domain: ".example.com" } response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n") end it "formats the Cookie expiration date accordingly to RFC 6265" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :expires => Time.now+10} + response.set_cookie "foo", { value: "bar", expires: Time.now + 10 } response["Set-Cookie"].must_match( /expires=..., \d\d ... \d\d\d\d \d\d:\d\d:\d\d .../) end it "can set secure cookies" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :secure => true} + response.set_cookie "foo", { value: "bar", secure: true } response["Set-Cookie"].must_equal "foo=bar; secure" end it "can set http only cookies" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :httponly => true} + response.set_cookie "foo", { value: "bar", httponly: true } response["Set-Cookie"].must_equal "foo=bar; HttpOnly" end it "can set http only cookies with :http_only" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :http_only => true} + response.set_cookie "foo", { value: "bar", http_only: true } response["Set-Cookie"].must_equal "foo=bar; HttpOnly" end it "can set prefers :httponly for http only cookie setting when :httponly and :http_only provided" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :httponly => false, :http_only => true} + response.set_cookie "foo", { value: "bar", httponly: false, http_only: true } 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} + response.set_cookie "foo", { value: "bar", same_site: :lax } response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax" end it "can set SameSite cookies with symbol value :Lax" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => :lax} + response.set_cookie "foo", { value: "bar", same_site: :lax } response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax" end it "can set SameSite cookies with string value 'Lax'" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => "Lax"} + response.set_cookie "foo", { value: "bar", same_site: "Lax" } response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax" end it "can set SameSite cookies with boolean value true" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => true} + response.set_cookie "foo", { value: "bar", same_site: true } response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" end it "can set SameSite cookies with symbol value :strict" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => :strict} + response.set_cookie "foo", { value: "bar", same_site: :strict } response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" end it "can set SameSite cookies with symbol value :Strict" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => :Strict} + response.set_cookie "foo", { value: "bar", same_site: :Strict } response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" end it "can set SameSite cookies with string value 'Strict'" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => "Strict"} + response.set_cookie "foo", { value: "bar", same_site: "Strict" } response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" end it "validates the SameSite option value" do response = Rack::Response.new lambda { - response.set_cookie "foo", {:value => "bar", :same_site => "Foo"} + response.set_cookie "foo", { value: "bar", same_site: "Foo" } }.must_raise(ArgumentError). message.must_match(/Invalid SameSite value: "Foo"/) end it "can set SameSite cookies with symbol value" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => :Strict} + response.set_cookie "foo", { value: "bar", same_site: :Strict } response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict" end [ nil, false ].each do |non_truthy| it "omits SameSite attribute given a #{non_truthy.inspect} value" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :same_site => non_truthy} + response.set_cookie "foo", { value: "bar", same_site: non_truthy } response["Set-Cookie"].must_equal "foo=bar" end end @@ -186,32 +216,32 @@ describe Rack::Response do response.delete_cookie "foo" response["Set-Cookie"].must_equal [ "foo2=bar2", - "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000" + "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" ].join("\n") end it "can delete cookies with the same name from multiple domains" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :domain => "sample.example.com"} - response.set_cookie "foo", {:value => "bar", :domain => ".example.com"} + response.set_cookie "foo", { value: "bar", domain: "sample.example.com" } + response.set_cookie "foo", { value: "bar", domain: ".example.com" } response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n") - response.delete_cookie "foo", :domain => ".example.com" - response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n") - response.delete_cookie "foo", :domain => "sample.example.com" - response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000", - "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n") + response.delete_cookie "foo", domain: ".example.com" + response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") + response.delete_cookie "foo", domain: "sample.example.com" + response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", + "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") end it "can delete cookies with the same name with different paths" do response = Rack::Response.new - response.set_cookie "foo", {:value => "bar", :path => "/"} - response.set_cookie "foo", {:value => "bar", :path => "/path"} + response.set_cookie "foo", { value: "bar", path: "/" } + response.set_cookie "foo", { value: "bar", path: "/path" } response["Set-Cookie"].must_equal ["foo=bar; path=/", "foo=bar; path=/path"].join("\n") - response.delete_cookie "foo", :path => "/path" + response.delete_cookie "foo", path: "/path" response["Set-Cookie"].must_equal ["foo=bar; path=/", - "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n") + "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") end it "can do redirects" do @@ -231,12 +261,12 @@ describe Rack::Response do it "has a useful constructor" do r = Rack::Response.new("foo") status, header, body = r.finish - str = ""; body.each { |part| str << part } + str = "".dup; body.each { |part| str << part } str.must_equal "foo" r = Rack::Response.new(["foo", "bar"]) status, header, body = r.finish - str = ""; body.each { |part| str << part } + str = "".dup; body.each { |part| str << part } str.must_equal "foobar" object_with_each = Object.new @@ -247,7 +277,7 @@ describe Rack::Response do r = Rack::Response.new(object_with_each) r.write "foo" status, header, body = r.finish - str = ""; body.each { |part| str << part } + str = "".dup; body.each { |part| str << part } str.must_equal "foobarfoo" r = Rack::Response.new([], 500) @@ -263,7 +293,7 @@ describe Rack::Response do res.write "foo" } status, _, body = r.finish - str = ""; body.each { |part| str << part } + str = "".dup; body.each { |part| str << part } str.must_equal "foo" status.must_equal 404 end @@ -271,10 +301,10 @@ describe Rack::Response do it "doesn't return invalid responses" do r = Rack::Response.new(["foo", "bar"], 204) _, header, body = r.finish - str = ""; body.each { |part| str << part } + str = "".dup; body.each { |part| str << part } str.must_be :empty? - header["Content-Type"].must_equal nil - header['Content-Length'].must_equal nil + header["Content-Type"].must_be_nil + header['Content-Length'].must_be_nil lambda { Rack::Response.new(Object.new) @@ -410,7 +440,7 @@ describe Rack::Response do res.body.must_be :closed? end - it "calls close on #body when 204, 205, or 304" do + it "calls close on #body when 204 or 304" do res = Rack::Response.new res.body = StringIO.new res.finish @@ -424,7 +454,7 @@ describe Rack::Response do res.body = StringIO.new res.status = 205 _, _, b = res.finish - res.body.must_be :closed? + res.body.wont_be :closed? b.wont_equal res.body res.body = StringIO.new diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb index 5adfba1d..6bb5f5cf 100644 --- a/test/spec_rewindable_input.rb +++ b/test/spec_rewindable_input.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'stringio' require 'rack/rewindable_input' @@ -26,14 +28,14 @@ module RewindableTest end it "be able to handle to read(length, buffer)" do - buffer = "" + buffer = "".dup result = @rio.read(1, buffer) result.must_equal "h" result.object_id.must_equal buffer.object_id end it "be able to handle to read(nil, buffer)" do - buffer = "" + buffer = "".dup result = @rio.read(nil, buffer) result.must_equal "hello world" result.object_id.must_equal buffer.object_id @@ -95,7 +97,7 @@ end describe Rack::RewindableInput do describe "given an IO object that is already rewindable" do def setup - @io = StringIO.new("hello world") + @io = StringIO.new("hello world".dup) super end @@ -104,7 +106,7 @@ describe Rack::RewindableInput do describe "given an IO object that is not rewindable" do def setup - @io = StringIO.new("hello world") + @io = StringIO.new("hello world".dup) @io.instance_eval do undef :rewind end @@ -116,7 +118,7 @@ describe Rack::RewindableInput do describe "given an IO object whose rewind method raises Errno::ESPIPE" do def setup - @io = StringIO.new("hello world") + @io = StringIO.new("hello world".dup) def @io.rewind raise Errno::ESPIPE, "You can't rewind this!" end diff --git a/test/spec_runtime.rb b/test/spec_runtime.rb index f7f52ad9..10e561de 100644 --- a/test/spec_runtime.rb +++ b/test/spec_runtime.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/lint' require 'rack/mock' require 'rack/runtime' @@ -13,25 +15,25 @@ describe Rack::Runtime do end it "sets X-Runtime is none is set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] } response = runtime_app(app).call(request) response[1]['X-Runtime'].must_match(/[\d\.]+/) end it "doesn't set the X-Runtime if it is already set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain', "X-Runtime" => "foobar"}, "Hello, World!"] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain', "X-Runtime" => "foobar" }, "Hello, World!"] } response = runtime_app(app).call(request) response[1]['X-Runtime'].must_equal "foobar" end it "allow a suffix to be set" do - app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] } + app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] } response = runtime_app(app, "Test").call(request) response[1]['X-Runtime-Test'].must_match(/[\d\.]+/) end it "allow multiple timers to be set" do - app = lambda { |env| sleep 0.1; [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] } + app = lambda { |env| sleep 0.1; [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] } runtime = runtime_app(app, "App") # wrap many times to guarantee a measurable difference diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb index 18689857..cbed8db3 100644 --- a/test/spec_sendfile.rb +++ b/test/spec_sendfile.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'fileutils' require 'rack/lint' require 'rack/sendfile' @@ -6,22 +8,22 @@ 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 - def simple_app(body=sendfile_body) - lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] } + def simple_app(body = sendfile_body) + lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] } end def sendfile_app(body, mappings = []) Rack::Lint.new Rack::Sendfile.new(simple_app(body), nil, mappings) end - def request(headers={}, body=sendfile_body, mappings=[]) + def request(headers = {}, body = sendfile_body, mappings = []) yield Rack::MockRequest.new(sendfile_app(body, mappings)).get('/', headers) end @@ -72,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? @@ -82,7 +97,7 @@ describe Rack::Sendfile do end it 'does nothing when body does not respond to #to_path' do - request({'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile'}, ['Not a file...']) do |response| + request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }, ['Not a file...']) do |response| response.body.must_equal 'Not a file...' response.headers.wont_include 'X-Sendfile' end @@ -104,14 +119,49 @@ describe Rack::Sendfile do ["#{dir2}/", '/wibble/'] ] - request({'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect'}, first_body, mappings) do |response| + request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' }, first_body, mappings) 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/rack_sendfile' + end + + request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' }, second_body, mappings) 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 '/wibble/rack_sendfile' + end + ensure + FileUtils.remove_entry_secure dir1 + FileUtils.remove_entry_secure dir2 + end + end + + it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings via header" do + begin + dir1 = Dir.mktmpdir + dir2 = Dir.mktmpdir + + first_body = open_file(File.join(dir1, 'rack_sendfile')) + first_body.puts 'hello world' + + second_body = open_file(File.join(dir2, 'rack_sendfile')) + second_body.puts 'goodbye world' + + headers = { + 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', + 'HTTP_X_ACCEL_MAPPING' => "#{dir1}/=/foo/bar/, #{dir2}/=/wibble/" + } + + request(headers, first_body) 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/rack_sendfile' end - request({'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect'}, second_body, mappings) do |response| + request(headers, second_body) do |response| response.must_be :ok? response.body.must_be :empty? response.headers['Content-Length'].must_equal '0' diff --git a/test/spec_server.rb b/test/spec_server.rb index a3690bce..b09caf03 100644 --- a/test/spec_server.rb +++ b/test/spec_server.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/server' require 'tempfile' @@ -15,7 +17,7 @@ describe Rack::Server do before { SPEC_ARGV[0..-1] = [] } def app - lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] } + lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['success']] } end def with_stderr @@ -26,12 +28,12 @@ describe Rack::Server do end it "overrides :config if :app is passed in" do - server = Rack::Server.new(:app => "FOO") + server = Rack::Server.new(app: "FOO") server.app.must_equal "FOO" end it "prefer to use :builder when it is passed in" do - server = Rack::Server.new(:builder => "run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] }") + server = Rack::Server.new(builder: "run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] }") server.app.class.must_equal Proc Rack::MockRequest.new(server.app).get("/").body.to_s.must_equal 'success' end @@ -39,13 +41,13 @@ describe Rack::Server do it "allow subclasses to override middleware" do server = Class.new(Rack::Server).class_eval { def middleware; Hash.new [] end; self } server.middleware['deployment'].wont_equal [] - server.new(:app => 'foo').middleware['deployment'].must_equal [] + server.new(app: 'foo').middleware['deployment'].must_equal [] end it "allow subclasses to override default middleware" do server = Class.new(Rack::Server).instance_eval { def default_middleware_by_environment; Hash.new [] end; self } server.middleware['deployment'].must_equal [] - server.new(:app => 'foo').middleware['deployment'].must_equal [] + server.new(app: 'foo').middleware['deployment'].must_equal [] end it "only provide default middleware for development and deployment environments" do @@ -53,39 +55,39 @@ describe Rack::Server do end it "always return an empty array for unknown environments" do - server = Rack::Server.new(:app => 'foo') + server = Rack::Server.new(app: 'foo') server.middleware['production'].must_equal [] end it "not include Rack::Lint in deployment environment" do - server = Rack::Server.new(:app => 'foo') + server = Rack::Server.new(app: 'foo') server.middleware['deployment'].flatten.wont_include Rack::Lint end it "not include Rack::ShowExceptions in deployment environment" do - server = Rack::Server.new(:app => 'foo') + server = Rack::Server.new(app: 'foo') server.middleware['deployment'].flatten.wont_include Rack::ShowExceptions end it "include Rack::TempfileReaper in deployment environment" do - server = Rack::Server.new(:app => 'foo') + server = Rack::Server.new(app: 'foo') server.middleware['deployment'].flatten.must_include Rack::TempfileReaper end it "support CGI" do begin o, ENV["REQUEST_METHOD"] = ENV["REQUEST_METHOD"], 'foo' - server = Rack::Server.new(:app => 'foo') + server = Rack::Server.new(app: 'foo') server.server.name =~ /CGI/ - Rack::Server.logging_middleware.call(server).must_equal nil + Rack::Server.logging_middleware.call(server).must_be_nil ensure ENV['REQUEST_METHOD'] = o end end it "be quiet if said so" do - server = Rack::Server.new(:app => "FOO", :quiet => true) - Rack::Server.logging_middleware.call(server).must_equal nil + server = Rack::Server.new(app: "FOO", quiet: true) + Rack::Server.logging_middleware.call(server).must_be_nil end it "use a full path to the pidfile" do @@ -118,15 +120,15 @@ describe Rack::Server do pidfile = Tempfile.open('pidfile') { |f| break f } FileUtils.rm pidfile.path server = Rack::Server.new( - :app => app, - :environment => 'none', - :pid => pidfile.path, - :Port => TCPServer.open('127.0.0.1', 0){|s| s.addr[1] }, - :Host => '127.0.0.1', - :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - :AccessLog => [], - :daemonize => false, - :server => 'webrick' + app: app, + environment: 'none', + pid: pidfile.path, + Port: TCPServer.open('127.0.0.1', 0){|s| s.addr[1] }, + Host: '127.0.0.1', + Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), + AccessLog: [], + daemonize: false, + server: 'webrick' ) t = Thread.new { server.start { |s| Thread.current[:server] = s } } t.join(0.01) until t[:server] && t[:server].status != :Stop @@ -140,34 +142,36 @@ describe Rack::Server do it "check pid file presence and running process" do pidfile = Tempfile.open('pidfile') { |f| f.write($$); break f }.path - server = Rack::Server.new(:pid => pidfile) + server = Rack::Server.new(pid: pidfile) server.send(:pidfile_process_status).must_equal :running end it "check pid file presence and dead process" do dead_pid = `echo $$`.to_i pidfile = Tempfile.open('pidfile') { |f| f.write(dead_pid); break f }.path - server = Rack::Server.new(:pid => pidfile) + server = Rack::Server.new(pid: pidfile) server.send(:pidfile_process_status).must_equal :dead end it "check pid file presence and exited process" do pidfile = Tempfile.open('pidfile') { |f| break f }.path ::File.delete(pidfile) - server = Rack::Server.new(:pid => pidfile) + server = Rack::Server.new(pid: pidfile) server.send(:pidfile_process_status).must_equal :exited 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 = Rack::Server.new(pid: pidfile) server.send(:pidfile_process_status).must_equal :not_owned end it "not write pid file when it is created after check" do pidfile = Tempfile.open('pidfile') { |f| break f }.path ::File.delete(pidfile) - server = Rack::Server.new(:pid => pidfile) + server = Rack::Server.new(pid: pidfile) ::File.open(pidfile, 'w') { |f| f.write(1) } with_stderr do |err| lambda { server.send(:write_pid) }.must_raise SystemExit @@ -180,7 +184,7 @@ describe Rack::Server do it "inform the user about existing pidfiles with running processes" do pidfile = Tempfile.open('pidfile') { |f| f.write(1); break f }.path - server = Rack::Server.new(:pid => pidfile) + server = Rack::Server.new(pid: pidfile) with_stderr do |err| lambda { server.start }.must_raise SystemExit err.rewind diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb index a6568f19..3591a3de 100644 --- a/test/spec_session_abstract_id.rb +++ b/test/spec_session_abstract_id.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' ### WARNING: there be hax in this file. require 'rack/session/abstract/id' @@ -24,7 +26,7 @@ describe Rack::Session::Abstract::ID do 'fake_hex' end end - id = Rack::Session::Abstract::ID.new nil, :secure_random => secure_random.new + id = Rack::Session::Abstract::ID.new nil, secure_random: secure_random.new id.send(:generate_sid).must_equal 'fake_hex' end diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb index 6d73a80a..206e2c1b 100644 --- a/test/spec_session_abstract_session_hash.rb +++ b/test/spec_session_abstract_session_hash.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/session/abstract/id' describe Rack::Session::Abstract::SessionHash do @@ -8,7 +10,7 @@ describe Rack::Session::Abstract::SessionHash do super store = Class.new do def load_session(req) - ["id", {foo: :bar, baz: :qux}] + ["id", { foo: :bar, baz: :qux }] end def session_exists?(req) true @@ -25,4 +27,21 @@ describe Rack::Session::Abstract::SessionHash do assert_equal [:bar, :qux], hash.values end + describe "#fetch" do + it "returns value for a matching key" do + assert_equal :bar, hash.fetch(:foo) + end + + it "works with a default value" do + assert_equal :default, hash.fetch(:unknown, :default) + end + + it "works with a block" do + assert_equal :default, hash.fetch(:unkown) { :default } + end + + it "it raises when fetching unknown keys without defaults" do + lambda { hash.fetch(:unknown) }.must_raise KeyError + end + end end diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb index 5b47b37e..9b4442dd 100644 --- a/test/spec_session_cookie.rb +++ b/test/spec_session_cookie.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/session/cookie' require 'rack/lint' require 'rack/mock' @@ -45,7 +47,7 @@ describe Rack::Session::Cookie do Rack::Response.new("Nothing").to_a end - def response_for(options={}) + def response_for(options = {}) request_options = options.fetch(:request, {}) cookie = if options[:cookie].is_a?(Rack::Response) options[:cookie]["Set-Cookie"] @@ -98,18 +100,18 @@ describe Rack::Session::Cookie do it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::Marshal.new - coder.decode('lulz').must_equal nil + coder.decode('lulz').must_be_nil end end describe 'JSON' do - it 'marshals and base64 encodes' do + it 'JSON and base64 encodes' do coder = Rack::Session::Cookie::Base64::JSON.new obj = %w[fuuuuu] coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m') end - it 'marshals and base64 decodes' do + it 'JSON and base64 decodes' do coder = Rack::Session::Cookie::Base64::JSON.new str = [::JSON.dump(%w[fuuuuu])].pack('m') coder.decode(str).must_equal ::JSON.parse(str.unpack('m').first) @@ -117,7 +119,7 @@ describe Rack::Session::Cookie do it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::JSON.new - coder.decode('lulz').must_equal nil + coder.decode('lulz').must_be_nil end end @@ -139,7 +141,7 @@ describe Rack::Session::Cookie do it 'rescues failures on decode' do coder = Rack::Session::Cookie::Base64::ZipJSON.new - coder.decode('lulz').must_equal nil + coder.decode('lulz').must_be_nil end end end @@ -148,22 +150,22 @@ describe Rack::Session::Cookie do Rack::Session::Cookie.new(incrementor) @warnings.first.must_match(/no secret/i) @warnings.clear - Rack::Session::Cookie.new(incrementor, :secret => 'abc') + Rack::Session::Cookie.new(incrementor, secret: 'abc') @warnings.must_be :empty? end it "doesn't warn if coder is configured to handle encoding" do Rack::Session::Cookie.new( incrementor, - :coder => Object.new, - :let_coder_handle_secure_encoding => true) + coder: Object.new, + let_coder_handle_secure_encoding: true) @warnings.must_be :empty? end it "still warns if coder is not set" do Rack::Session::Cookie.new( incrementor, - :let_coder_handle_secure_encoding => true) + let_coder_handle_secure_encoding: true) @warnings.first.must_match(/no secret/i) end @@ -178,7 +180,7 @@ describe Rack::Session::Cookie do def encode(str); @calls << :encode; str; end def decode(str); @calls << :decode; str; end }.new - response = response_for(:app => [incrementor, { :coder => identity }]) + response = response_for(app: [incrementor, { coder: identity }]) response["Set-Cookie"].must_include "rack.session=" response.body.must_equal '{"counter"=>1}' @@ -186,47 +188,47 @@ describe Rack::Session::Cookie do end it "creates a new cookie" do - response = response_for(:app => incrementor) + response = response_for(app: incrementor) response["Set-Cookie"].must_include "rack.session=" response.body.must_equal '{"counter"=>1}' end it "loads from a cookie" do - response = response_for(:app => incrementor) + response = response_for(app: incrementor) - response = response_for(:app => incrementor, :cookie => response) + response = response_for(app: incrementor, cookie: response) response.body.must_equal '{"counter"=>2}' - response = response_for(:app => incrementor, :cookie => response) + response = response_for(app: incrementor, cookie: response) response.body.must_equal '{"counter"=>3}' end it "renew session id" do - response = response_for(:app => incrementor) + response = response_for(app: incrementor) cookie = response['Set-Cookie'] - response = response_for(:app => only_session_id, :cookie => cookie) + response = response_for(app: only_session_id, cookie: cookie) cookie = response['Set-Cookie'] if response['Set-Cookie'] response.body.wont_equal "" old_session_id = response.body - response = response_for(:app => renewer, :cookie => cookie) + response = response_for(app: renewer, cookie: cookie) cookie = response['Set-Cookie'] if response['Set-Cookie'] - response = response_for(:app => only_session_id, :cookie => cookie) + response = response_for(app: only_session_id, cookie: cookie) response.body.wont_equal "" response.body.wont_equal old_session_id end it "destroys session" do - response = response_for(:app => incrementor) - response = response_for(:app => only_session_id, :cookie => response) + response = response_for(app: incrementor) + response = response_for(app: only_session_id, cookie: response) response.body.wont_equal "" old_session_id = response.body - response = response_for(:app => destroy_session, :cookie => response) - response = response_for(:app => only_session_id, :cookie => response) + response = response_for(app: destroy_session, cookie: response) + response = response_for(app: only_session_id, cookie: response) response.body.wont_equal "" response.body.wont_equal old_session_id @@ -234,104 +236,104 @@ describe Rack::Session::Cookie do it "survives broken cookies" do response = response_for( - :app => incrementor, - :cookie => "rack.session=blarghfasel" + app: incrementor, + cookie: "rack.session=blarghfasel" ) response.body.must_equal '{"counter"=>1}' response = response_for( - :app => [incrementor, { :secret => "test" }], - :cookie => "rack.session=" + app: [incrementor, { secret: "test" }], + cookie: "rack.session=" ) response.body.must_equal '{"counter"=>1}' end it "barks on too big cookies" do lambda{ - response_for(:app => bigcookie, :request => { :fatal => true }) + response_for(app: bigcookie, request: { fatal: true }) }.must_raise Rack::MockRequest::FatalWarning end it "loads from a cookie with integrity hash" do - app = [incrementor, { :secret => "test" }] + app = [incrementor, { secret: "test" }] - response = response_for(:app => app) - response = response_for(:app => app, :cookie => response) + response = response_for(app: app) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>2}' - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>3}' - app = [incrementor, { :secret => "other" }] + app = [incrementor, { secret: "other" }] - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>1}' end it "loads from a cookie with accept-only integrity hash for graceful key rotation" do - response = response_for(:app => [incrementor, { :secret => "test" }]) + response = response_for(app: [incrementor, { secret: "test" }]) - app = [incrementor, { :secret => "test2", :old_secret => "test" }] - response = response_for(:app => app, :cookie => response) + app = [incrementor, { secret: "test2", old_secret: "test" }] + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>2}' - app = [incrementor, { :secret => "test3", :old_secret => "test2" }] - response = response_for(:app => app, :cookie => response) + app = [incrementor, { secret: "test3", old_secret: "test2" }] + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>3}' end it "ignores tampered with session cookies" do - app = [incrementor, { :secret => "test" }] - response = response_for(:app => app) + app = [incrementor, { secret: "test" }] + response = response_for(app: app) response.body.must_equal '{"counter"=>1}' - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>2}' _, digest = response["Set-Cookie"].split("--") tampered_with_cookie = "hackerman-was-here" + "--" + digest - response = response_for(:app => app, :cookie => tampered_with_cookie) + response = response_for(app: app, cookie: tampered_with_cookie) response.body.must_equal '{"counter"=>1}' end it "supports either of secret or old_secret" do - app = [incrementor, { :secret => "test" }] - response = response_for(:app => app) + app = [incrementor, { secret: "test" }] + response = response_for(app: app) response.body.must_equal '{"counter"=>1}' - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>2}' - app = [incrementor, { :old_secret => "test" }] - response = response_for(:app => app) + app = [incrementor, { old_secret: "test" }] + response = response_for(app: app) response.body.must_equal '{"counter"=>1}' - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>2}' end it "supports custom digest class" do - app = [incrementor, { :secret => "test", hmac: OpenSSL::Digest::SHA256 }] + app = [incrementor, { secret: "test", hmac: OpenSSL::Digest::SHA256 }] - response = response_for(:app => app) - response = response_for(:app => app, :cookie => response) + response = response_for(app: app) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>2}' - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>3}' - app = [incrementor, { :secret => "other" }] + app = [incrementor, { secret: "other" }] - response = response_for(:app => app, :cookie => response) + response = response_for(app: app, cookie: response) response.body.must_equal '{"counter"=>1}' end it "can handle Rack::Lint middleware" do - response = response_for(:app => incrementor) + response = response_for(app: incrementor) lint = Rack::Lint.new(session_id) - response = response_for(:app => lint, :cookie => response) + response = response_for(app: lint, cookie: response) response.body.wont_be :nil? end @@ -346,75 +348,75 @@ describe Rack::Session::Cookie do end end - response = response_for(:app => incrementor) + response = response_for(app: incrementor) inspector = TestEnvInspector.new(session_id) - response = response_for(:app => inspector, :cookie => response) + response = response_for(app: inspector, cookie: response) response.body.wont_be :nil? end it "returns the session id in the session hash" do - response = response_for(:app => incrementor) + response = response_for(app: incrementor) response.body.must_equal '{"counter"=>1}' - response = response_for(:app => session_id, :cookie => response) + response = response_for(app: session_id, cookie: response) response.body.must_match(/"session_id"=>/) response.body.must_match(/"counter"=>1/) end it "does not return a cookie if set to secure but not using ssl" do - app = [incrementor, { :secure => true }] + app = [incrementor, { secure: true }] - response = response_for(:app => app) + response = response_for(app: app) response["Set-Cookie"].must_be_nil - response = response_for(:app => app, :request => { "HTTPS" => "on" }) + response = response_for(app: app, request: { "HTTPS" => "on" }) response["Set-Cookie"].wont_be :nil? response["Set-Cookie"].must_match(/secure/) end it "does not return a cookie if cookie was not read/written" do - response = response_for(:app => nothing) + response = response_for(app: nothing) response["Set-Cookie"].must_be_nil end it "does not return a cookie if cookie was not written (only read)" do - response = response_for(:app => session_id) + response = response_for(app: session_id) response["Set-Cookie"].must_be_nil end it "returns even if not read/written if :expire_after is set" do - app = [nothing, { :expire_after => 3600 }] - request = { "rack.session" => { "not" => "empty" }} - response = response_for(:app => app, :request => request) + app = [nothing, { expire_after: 3600 }] + request = { "rack.session" => { "not" => "empty" } } + response = response_for(app: app, request: request) response["Set-Cookie"].wont_be :nil? end it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do - app = [nothing, { :expire_after => 3600 }] - response = response_for(:app => app) + app = [nothing, { expire_after: 3600 }] + response = response_for(app: app) response["Set-Cookie"].must_be_nil end it "exposes :secret in env['rack.session.option']" do - response = response_for(:app => [session_option[:secret], { :secret => "foo" }]) + response = response_for(app: [session_option[:secret], { secret: "foo" }]) response.body.must_equal '"foo"' end it "exposes :coder in env['rack.session.option']" do - response = response_for(:app => session_option[:coder]) + response = response_for(app: session_option[:coder]) response.body.must_match(/Base64::Marshal/) end it "allows passing in a hash with session data from middleware in front" do - request = { 'rack.session' => { :foo => 'bar' }} - response = response_for(:app => session_id, :request => request) + request = { 'rack.session' => { foo: 'bar' } } + response = response_for(app: session_id, request: request) response.body.must_match(/foo/) end it "allows modifying session data with session data from middleware in front" do - request = { 'rack.session' => { :foo => 'bar' }} - response = response_for(:app => incrementor, :request => request) + request = { 'rack.session' => { foo: 'bar' } } + response = response_for(app: incrementor, request: request) response.body.must_match(/counter/) response.body.must_match(/foo/) end @@ -423,7 +425,7 @@ describe Rack::Session::Cookie do @counter = 0 app = lambda do |env| env["rack.session"]["message"] ||= "" - env["rack.session"]["message"] << "#{(@counter += 1).to_s}--" + env["rack.session"]["message"] += "#{(@counter += 1).to_s}--" hash = env["rack.session"].dup hash.delete("session_id") Rack::Response.new(hash["message"]).to_a @@ -433,10 +435,10 @@ describe Rack::Session::Cookie do def encode(hash); hash.inspect end def decode(str); eval(str) if str; end }.new - _app = [ app, { :secret => "test", :coder => unsafe_coder } ] - response = response_for(:app => _app) + _app = [ app, { secret: "test", coder: unsafe_coder } ] + response = response_for(app: _app) response.body.must_equal "1--" - response = response_for(:app => _app, :cookie => response) + response = response_for(app: _app, cookie: response) response.body.must_equal "1--2--" end end diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb index 66903d69..a015cee6 100644 --- a/test/spec_session_memcache.rb +++ b/test/spec_session_memcache.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' begin require 'rack/session/memcache' require 'rack/lint' @@ -36,17 +38,17 @@ begin it "faults on no connection" do lambda { - Rack::Session::Memcache.new(incrementor, :memcache_server => 'nosuchserver') + Rack::Session::Memcache.new(incrementor, memcache_server: 'nosuchserver') }.must_raise(RuntimeError).message.must_equal 'No memcache servers' end it "connects to existing server" do - test_pool = MemCache.new(incrementor, :namespace => 'test:rack:session') + test_pool = MemCache.new(incrementor, namespace: 'test:rack:session') test_pool.namespace.must_equal 'test:rack:session' end it "passes options to MemCache" do - pool = Rack::Session::Memcache.new(incrementor, :namespace => 'test:rack:session') + pool = Rack::Session::Memcache.new(incrementor, namespace: 'test:rack:session') pool.pool.namespace.must_equal 'test:rack:session' end @@ -80,7 +82,7 @@ begin end it "determines session from params" do - pool = Rack::Session::Memcache.new(incrementor, :cookie_only => false) + pool = Rack::Session::Memcache.new(incrementor, cookie_only: false) req = Rack::MockRequest.new(pool) res = req.get("/") sid = res["Set-Cookie"][session_match, 1] @@ -101,7 +103,7 @@ begin end it "maintains freshness" do - pool = Rack::Session::Memcache.new(incrementor, :expire_after => 3) + pool = Rack::Session::Memcache.new(incrementor, expire_after: 3) res = Rack::MockRequest.new(pool).get('/') res.body.must_include '"counter"=>1' cookie = res["Set-Cookie"] @@ -143,7 +145,7 @@ begin res1.body.must_equal '{"counter"=>1}' res2 = dreq.get("/", "HTTP_COOKIE" => cookie) - res2["Set-Cookie"].must_equal nil + res2["Set-Cookie"].must_be_nil res2.body.must_equal '{"counter"=>2}' res3 = req.get("/", "HTTP_COOKIE" => cookie) @@ -183,7 +185,7 @@ begin creq = Rack::MockRequest.new(count) res0 = dreq.get("/") - res0["Set-Cookie"].must_equal nil + res0["Set-Cookie"].must_be_nil res0.body.must_equal '{"counter"=>1}' res0 = creq.get("/") @@ -201,7 +203,7 @@ begin creq = Rack::MockRequest.new(count) res0 = sreq.get("/") - res0["Set-Cookie"].must_equal nil + res0["Set-Cookie"].must_be_nil res0.body.must_equal '{"counter"=>1}' res0 = creq.get("/") @@ -215,8 +217,8 @@ begin hash_check = proc do |env| session = env['rack.session'] unless session.include? 'test' - session.update :a => :b, :c => { :d => :e }, - :f => { :g => { :h => :i} }, 'test' => true + session.update :a => :b, :c => { d: :e }, + :f => { g: { h: :i } }, 'test' => true else session[:f][:g][:h] = :j end @@ -252,12 +254,12 @@ begin # emulate disconjoinment of threading env['rack.session'] = env['rack.session'].dup Thread.stop - env['rack.session'][(Time.now.usec*rand).to_i] = true + env['rack.session'][(Time.now.usec * rand).to_i] = true incrementor.call(env) end tses = Rack::Utils::Context.new pool, delta_incrementor treq = Rack::MockRequest.new(tses) - tnum = rand(7).to_i+5 + tnum = rand(7).to_i + 5 r = Array.new(tnum) do Thread.new(treq) do |run| run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) @@ -269,10 +271,10 @@ begin end session = pool.pool.get(session_id) - session.size.must_equal tnum+1 # counter + session.size.must_equal tnum + 1 # counter session['counter'].must_equal 2 # meeeh - tnum = rand(7).to_i+5 + tnum = rand(7).to_i + 5 r = Array.new(tnum) do app = Rack::Utils::Context.new pool, time_delta req = Rack::MockRequest.new app @@ -286,17 +288,17 @@ begin end session = pool.pool.get(session_id) - session.size.must_equal tnum+1 + session.size.must_equal tnum + 1 session['counter'].must_equal 3 drop_counter = proc do |env| env['rack.session'].delete 'counter' env['rack.session']['foo'] = 'bar' - [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect] + [200, { 'Content-Type' => 'text/plain' }, env['rack.session'].inspect] end tses = Rack::Utils::Context.new pool, drop_counter treq = Rack::MockRequest.new(tses) - tnum = rand(7).to_i+5 + tnum = rand(7).to_i + 5 r = Array.new(tnum) do Thread.new(treq) do |run| run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) @@ -308,7 +310,7 @@ begin end session = pool.pool.get(session_id) - session.size.must_equal r.size+1 + session.size.must_equal r.size + 1 session['counter'].must_be_nil? session['foo'].must_equal 'bar' end diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb index 5eaadfff..fda5f56e 100644 --- a/test/spec_session_pool.rb +++ b/test/spec_session_pool.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'thread' require 'rack/lint' require 'rack/mock' @@ -138,7 +140,7 @@ describe Rack::Session::Pool do dreq = Rack::MockRequest.new(defer) res1 = dreq.get("/") - res1["Set-Cookie"].must_equal nil + res1["Set-Cookie"].must_be_nil res1.body.must_equal '{"counter"=>1}' pool.pool.size.must_equal 1 end @@ -157,18 +159,18 @@ describe Rack::Session::Pool do res = req.get('/') res.body.must_equal '{"counter"=>1}' cookie = res["Set-Cookie"] - sess_id = cookie[/#{pool.key}=([^,;]+)/,1] + sess_id = cookie[/#{pool.key}=([^,;]+)/, 1] delta_incrementor = lambda do |env| # emulate disconjoinment of threading env['rack.session'] = env['rack.session'].dup Thread.stop - env['rack.session'][(Time.now.usec*rand).to_i] = true + env['rack.session'][(Time.now.usec * rand).to_i] = true incrementor.call(env) end tses = Rack::Utils::Context.new pool, delta_incrementor treq = Rack::MockRequest.new(tses) - tnum = rand(7).to_i+5 + tnum = rand(7).to_i + 5 r = Array.new(tnum) do Thread.new(treq) do |run| run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) @@ -180,7 +182,7 @@ describe Rack::Session::Pool do end session = pool.pool[sess_id] - session.size.must_equal tnum+1 # counter + session.size.must_equal tnum + 1 # counter session['counter'].must_equal 2 # meeeh end @@ -197,13 +199,13 @@ describe Rack::Session::Pool do end it "returns even if not read/written if :expire_after is set" do - app = Rack::Session::Pool.new(nothing, :expire_after => 3600) - res = Rack::MockRequest.new(app).get("/", 'rack.session' => {'not' => 'empty'}) + app = Rack::Session::Pool.new(nothing, expire_after: 3600) + res = Rack::MockRequest.new(app).get("/", 'rack.session' => { 'not' => 'empty' }) res["Set-Cookie"].wont_be :nil? end it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do - app = Rack::Session::Pool.new(nothing, :expire_after => 3600) + app = Rack::Session::Pool.new(nothing, expire_after: 3600) res = Rack::MockRequest.new(app).get("/") res["Set-Cookie"].must_be_nil end diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb index cd44c816..a4ade121 100644 --- a/test/spec_show_exceptions.rb +++ b/test/spec_show_exceptions.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/show_exceptions' require 'rack/lint' require 'rack/mock' @@ -25,6 +27,24 @@ describe Rack::ShowExceptions do assert_match(res, /ShowExceptions/) end + it "works with binary data in the Rack environment" do + res = nil + + # "\xCC" is not a valid UTF-8 string + req = Rack::MockRequest.new( + show_exceptions( + lambda{|env| env['foo'] = "\xCC"; raise RuntimeError } + )) + + res = req.get("/", "HTTP_ACCEPT" => "text/html") + + res.must_be :server_error? + res.status.must_equal 500 + + assert_match(res, /RuntimeError/) + assert_match(res, /ShowExceptions/) + end + it "responds with HTML only to requests accepting HTML" do res = nil @@ -35,11 +55,11 @@ describe Rack::ShowExceptions do [ # Serve text/html when the client accepts text/html - ["text/html", ["/", {"HTTP_ACCEPT" => "text/html"}]], - ["text/html", ["/", {"HTTP_ACCEPT" => "*/*"}]], + ["text/html", ["/", { "HTTP_ACCEPT" => "text/html" }]], + ["text/html", ["/", { "HTTP_ACCEPT" => "*/*" }]], # Serve text/plain when the client does not accept text/html ["text/plain", ["/"]], - ["text/plain", ["/", {"HTTP_ACCEPT" => "application/json"}]] + ["text/plain", ["/", { "HTTP_ACCEPT" => "application/json" }]] ].each do |exmime, rargs| res = req.get(*rargs) @@ -77,4 +97,39 @@ describe Rack::ShowExceptions do assert_match(res, /ShowExceptions/) assert_match(res, /unknown location/) end + + it "allows subclasses to override template" do + c = Class.new(Rack::ShowExceptions) do + TEMPLATE = ERB.new("foo") + + def template + TEMPLATE + end + end + + app = lambda { |env| raise RuntimeError, "", [] } + + req = Rack::MockRequest.new( + Rack::Lint.new c.new(app) + ) + + res = req.get("/", "HTTP_ACCEPT" => "text/html") + + res.must_be :server_error? + res.status.must_equal 500 + res.body.must_equal "foo" + end + + it "knows to prefer plaintext for non-html" do + # We don't need an app for this + exc = Rack::ShowExceptions.new(nil) + + [ + [{ "HTTP_ACCEPT" => "text/plain" }, true], + [{ "HTTP_ACCEPT" => "text/foo" }, true], + [{ "HTTP_ACCEPT" => "text/html" }, false] + ].each do |env, expected| + assert_equal(expected, exc.prefers_plaintext?(env)) + end + end end diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb index d32dc7cb..ca23134e 100644 --- a/test/spec_show_status.rb +++ b/test/spec_show_status.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/show_status' require 'rack/lint' require 'rack/mock' @@ -12,10 +14,10 @@ describe Rack::ShowStatus do it "provide a default status message" do req = Rack::MockRequest.new( show_status(lambda{|env| - [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []] + [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []] })) - res = req.get("/", :lint => true) + res = req.get("/", lint: true) res.must_be :not_found? res.wont_be_empty @@ -29,10 +31,10 @@ describe Rack::ShowStatus do show_status( lambda{|env| env["rack.showstatus.detail"] = "gone too meta." - [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []] + [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []] })) - res = req.get("/", :lint => true) + res = req.get("/", lint: true) res.must_be :not_found? res.wont_be_empty @@ -48,10 +50,10 @@ describe Rack::ShowStatus do show_status( lambda{|env| env["rack.showstatus.detail"] = detail - [500, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []] + [500, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []] })) - res = req.get("/", :lint => true) + res = req.get("/", lint: true) res.wont_be_empty res["Content-Type"].must_equal "text/html" @@ -64,21 +66,21 @@ describe Rack::ShowStatus do req = Rack::MockRequest.new( show_status( lambda{|env| - [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]] + [404, { "Content-Type" => "text/plain", "Content-Length" => "4" }, ["foo!"]] })) - res = req.get("/", :lint => true) + res = req.get("/", lint: true) res.must_be :not_found? res.body.must_equal "foo!" end it "pass on original headers" do - headers = {"WWW-Authenticate" => "Basic blah"} + headers = { "WWW-Authenticate" => "Basic blah" } req = Rack::MockRequest.new( show_status(lambda{|env| [401, headers, []] })) - res = req.get("/", :lint => true) + res = req.get("/", lint: true) res["WWW-Authenticate"].must_equal "Basic blah" end @@ -88,10 +90,10 @@ describe Rack::ShowStatus do show_status( lambda{|env| env["rack.showstatus.detail"] = "gone too meta." - [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]] + [404, { "Content-Type" => "text/plain", "Content-Length" => "4" }, ["foo!"]] })) - res = req.get("/", :lint => true) + res = req.get("/", lint: true) res.must_be :not_found? res.wont_be_empty diff --git a/test/spec_static.rb b/test/spec_static.rb index f0a47171..d33e8edc 100644 --- a/test/spec_static.rb +++ b/test/spec_static.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/static' require 'rack/lint' require 'rack/mock' @@ -7,7 +9,7 @@ require 'stringio' class DummyApp def call(env) - [200, {"Content-Type" => "text/plain"}, ["Hello World"]] + [200, { "Content-Type" => "text/plain" }, ["Hello World"]] end end @@ -18,15 +20,17 @@ describe Rack::Static do root = File.expand_path(File.dirname(__FILE__)) - OPTIONS = {:urls => ["/cgi"], :root => root} - STATIC_OPTIONS = {:urls => [""], :root => "#{root}/static", :index => 'index.html'} - HASH_OPTIONS = {:urls => {"/cgi/sekret" => 'cgi/test'}, :root => root} - HASH_ROOT_OPTIONS = {:urls => {"/" => "static/foo.html"}, :root => root} - GZIP_OPTIONS = {:urls => ["/cgi"], :root => root, :gzip=>true} + OPTIONS = { urls: ["/cgi"], root: root } + STATIC_OPTIONS = { urls: [""], root: "#{root}/static", index: 'index.html' } + STATIC_URLS_OPTIONS = { urls: ["/static"], root: "#{root}", index: 'index.html' } + HASH_OPTIONS = { urls: { "/cgi/sekret" => 'cgi/test' }, root: root } + HASH_ROOT_OPTIONS = { urls: { "/" => "static/foo.html" }, root: root } + GZIP_OPTIONS = { urls: ["/cgi"], root: root, gzip: true } before do @request = Rack::MockRequest.new(static(DummyApp.new, OPTIONS)) @static_request = Rack::MockRequest.new(static(DummyApp.new, STATIC_OPTIONS)) + @static_urls_request = Rack::MockRequest.new(static(DummyApp.new, STATIC_URLS_OPTIONS)) @hash_request = Rack::MockRequest.new(static(DummyApp.new, HASH_OPTIONS)) @hash_root_request = Rack::MockRequest.new(static(DummyApp.new, HASH_ROOT_OPTIONS)) @gzip_request = Rack::MockRequest.new(static(DummyApp.new, GZIP_OPTIONS)) @@ -63,6 +67,16 @@ describe Rack::Static do res.body.must_match(/another index!/) end + it "does not call index file when requesting folder with unknown prefix" do + res = @static_urls_request.get("/static/another/") + res.must_be :ok? + res.body.must_match(/index!/) + + res = @static_urls_request.get("/something/else/") + res.must_be :ok? + res.body.must_equal "Hello World" + end + it "doesn't call index file if :index option was omitted" do res = @request.get("/") res.body.must_equal "Hello World" @@ -87,7 +101,7 @@ describe Rack::Static do end it "serves gzipped files if client accepts gzip encoding and gzip files are present" do - res = @gzip_request.get("/cgi/test", 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip') + res = @gzip_request.get("/cgi/test", 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip') res.must_be :ok? res.headers['Content-Encoding'].must_equal 'gzip' res.headers['Content-Type'].must_equal 'text/plain' @@ -95,9 +109,9 @@ describe Rack::Static do end it "serves regular files if client accepts gzip encoding and gzip files are not present" do - res = @gzip_request.get("/cgi/rackup_stub.rb", 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip') + res = @gzip_request.get("/cgi/rackup_stub.rb", 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip') res.must_be :ok? - res.headers['Content-Encoding'].must_equal nil + res.headers['Content-Encoding'].must_be_nil res.headers['Content-Type'].must_equal 'text/x-script.ruby' res.body.must_match(/ruby/) end @@ -105,27 +119,27 @@ describe Rack::Static do it "serves regular files if client does not accept gzip encoding" do res = @gzip_request.get("/cgi/test") res.must_be :ok? - res.headers['Content-Encoding'].must_equal nil + res.headers['Content-Encoding'].must_be_nil res.headers['Content-Type'].must_equal 'text/plain' res.body.must_match(/ruby/) end it "supports serving fixed cache-control (legacy option)" do - opts = OPTIONS.merge(:cache_control => 'public') + opts = OPTIONS.merge(cache_control: 'public') request = Rack::MockRequest.new(static(DummyApp.new, opts)) res = request.get("/cgi/test") res.must_be :ok? res.headers['Cache-Control'].must_equal 'public' end - HEADER_OPTIONS = {:urls => ["/cgi"], :root => root, :header_rules => [ - [:all, {'Cache-Control' => 'public, max-age=100'}], - [:fonts, {'Cache-Control' => 'public, max-age=200'}], - [%w(png jpg), {'Cache-Control' => 'public, max-age=300'}], - ['/cgi/assets/folder/', {'Cache-Control' => 'public, max-age=400'}], - ['cgi/assets/javascripts', {'Cache-Control' => 'public, max-age=500'}], - [/\.(css|erb)\z/, {'Cache-Control' => 'public, max-age=600'}] - ]} + HEADER_OPTIONS = { urls: ["/cgi"], root: root, header_rules: [ + [:all, { 'Cache-Control' => 'public, max-age=100' }], + [:fonts, { 'Cache-Control' => 'public, max-age=200' }], + [%w(png jpg), { 'Cache-Control' => 'public, max-age=300' }], + ['/cgi/assets/folder/', { 'Cache-Control' => 'public, max-age=400' }], + ['cgi/assets/javascripts', { 'Cache-Control' => 'public, max-age=500' }], + [/\.(css|erb)\z/, { 'Cache-Control' => 'public, max-age=600' }] + ] } it "supports header rule :all" do # Headers for all files via :all shortcut @@ -170,9 +184,9 @@ describe Rack::Static do it "prioritizes header rules over fixed cache-control setting (legacy option)" do opts = OPTIONS.merge( - :cache_control => 'public, max-age=24', - :header_rules => [ - [:all, {'Cache-Control' => 'public, max-age=42'}] + cache_control: 'public, max-age=24', + header_rules: [ + [:all, { 'Cache-Control' => 'public, max-age=42' }] ]) request = Rack::MockRequest.new(static(DummyApp.new, opts)) @@ -181,4 +195,14 @@ describe Rack::Static do res.headers['Cache-Control'].must_equal 'public, max-age=42' end + it "expands the root path upon the middleware initialization" do + relative_path = STATIC_OPTIONS[:root].sub("#{Dir.pwd}/", '') + opts = { urls: [""], root: relative_path, index: 'index.html' } + request = Rack::MockRequest.new(static(DummyApp.new, opts)) + Dir.chdir '..' do + res = request.get("") + res.must_be :ok? + res.body.must_match(/index!/) + end + end end diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb index b7c62563..0e7de841 100644 --- a/test/spec_tempfile_reaper.rb +++ b/test/spec_tempfile_reaper.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +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 85b225ed..0729c3f3 100644 --- a/test/spec_thin.rb +++ b/test/spec_thin.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' begin require 'rack/handler/thin' require File.expand_path('../testrequest', __FILE__) @@ -13,7 +15,7 @@ describe Rack::Handler::Thin do Thin::Logging.silent = true @thread = Thread.new do - Rack::Handler::Thin.run(@app, :Host => @host='127.0.0.1', :Port => @port=9204, :tag => "tag") do |server| + Rack::Handler::Thin.run(@app, Host: @host = '127.0.0.1', Port: @port = 9204, tag: "tag") do |server| @server = server end end @@ -44,7 +46,7 @@ describe Rack::Handler::Thin do it "have rack headers" do GET("/") - response["rack.version"].must_equal [1,0] + response["rack.version"].must_equal [1, 0] response["rack.multithread"].must_equal false response["rack.multiprocess"].must_equal false response["rack.run_once"].must_equal false @@ -66,7 +68,7 @@ describe Rack::Handler::Thin do end it "have CGI headers on POST" do - POST("/", {"rack-form-data" => "23"}, {'X-test-header' => '42'}) + POST("/", { "rack-form-data" => "23" }, { 'X-test-header' => '42' }) status.must_equal 200 response["REQUEST_METHOD"].must_equal "POST" response["REQUEST_PATH"].must_equal "/" @@ -76,7 +78,7 @@ describe Rack::Handler::Thin do end it "support HTTP auth" do - GET("/test", {:user => "ruth", :passwd => "secret"}) + GET("/test", { user: "ruth", passwd: "secret" }) response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" end diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb index 9d655c22..9ce38298 100644 --- a/test/spec_urlmap.rb +++ b/test/spec_urlmap.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/urlmap' require 'rack/mock' @@ -117,6 +119,14 @@ describe Rack::URLMap do res.must_be :ok? res["X-Position"].must_equal "default.org" + res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "any-host.org") + res.must_be :ok? + res["X-Position"].must_equal "default.org" + + res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "any-host.org", "HTTP_X_FORWARDED_HOST" => "any-host.org") + res.must_be :ok? + res["X-Position"].must_equal "default.org" + res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "example.org:9292", "SERVER_PORT" => "9292") @@ -127,7 +137,7 @@ describe Rack::URLMap do it "be nestable" do map = Rack::Lint.new(Rack::URLMap.new("/foo" => Rack::URLMap.new("/bar" => - Rack::URLMap.new("/quux" => lambda { |env| + Rack::URLMap.new("/quux" => lambda { |env| [200, { "Content-Type" => "text/plain", "X-Position" => "/foo/bar/quux", diff --git a/test/spec_utils.rb b/test/spec_utils.rb index b24762c9..6210fd73 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -1,5 +1,6 @@ -# -*- encoding: utf-8 -*- -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/utils' require 'rack/mock' require 'timeout' @@ -72,7 +73,7 @@ describe Rack::Utils do end it "escape path spaces with %20" do - Rack::Utils.escape_path("foo bar").must_equal "foo%20bar" + Rack::Utils.escape_path("foo bar").must_equal "foo%20bar" end it "unescape correctly" do @@ -181,38 +182,38 @@ describe Rack::Utils do must_equal "foo" => ["bar"], "baz" => ["1", "2", "3"] Rack::Utils.parse_nested_query("x[y][z]=1"). - must_equal "x" => {"y" => {"z" => "1"}} + must_equal "x" => { "y" => { "z" => "1" } } Rack::Utils.parse_nested_query("x[y][z][]=1"). - must_equal "x" => {"y" => {"z" => ["1"]}} + must_equal "x" => { "y" => { "z" => ["1"] } } Rack::Utils.parse_nested_query("x[y][z]=1&x[y][z]=2"). - must_equal "x" => {"y" => {"z" => "2"}} + must_equal "x" => { "y" => { "z" => "2" } } Rack::Utils.parse_nested_query("x[y][z][]=1&x[y][z][]=2"). - must_equal "x" => {"y" => {"z" => ["1", "2"]}} + must_equal "x" => { "y" => { "z" => ["1", "2"] } } Rack::Utils.parse_nested_query("x[y][][z]=1"). - must_equal "x" => {"y" => [{"z" => "1"}]} + must_equal "x" => { "y" => [{ "z" => "1" }] } Rack::Utils.parse_nested_query("x[y][][z][]=1"). - must_equal "x" => {"y" => [{"z" => ["1"]}]} + must_equal "x" => { "y" => [{ "z" => ["1"] }] } Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=2"). - must_equal "x" => {"y" => [{"z" => "1", "w" => "2"}]} + must_equal "x" => { "y" => [{ "z" => "1", "w" => "2" }] } Rack::Utils.parse_nested_query("x[y][][v][w]=1"). - must_equal "x" => {"y" => [{"v" => {"w" => "1"}}]} + must_equal "x" => { "y" => [{ "v" => { "w" => "1" } }] } Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][v][w]=2"). - must_equal "x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]} + must_equal "x" => { "y" => [{ "z" => "1", "v" => { "w" => "2" } }] } Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][z]=2"). - must_equal "x" => {"y" => [{"z" => "1"}, {"z" => "2"}]} + must_equal "x" => { "y" => [{ "z" => "1" }, { "z" => "2" }] } Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3"). - must_equal "x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]} + must_equal "x" => { "y" => [{ "z" => "1", "w" => "a" }, { "z" => "2", "w" => "3" }] } Rack::Utils.parse_nested_query("x[][y]=1&x[][z][w]=a&x[][y]=2&x[][z][w]=b"). - must_equal "x" => [{"y" => "1", "z" => {"w" => "a"}}, {"y" => "2", "z" => {"w" => "b"}}] + must_equal "x" => [{ "y" => "1", "z" => { "w" => "a" } }, { "y" => "2", "z" => { "w" => "b" } }] Rack::Utils.parse_nested_query("x[][z][w]=a&x[][y]=1&x[][z][w]=b&x[][y]=2"). - must_equal "x" => [{"y" => "1", "z" => {"w" => "a"}}, {"y" => "2", "z" => {"w" => "b"}}] + must_equal "x" => [{ "y" => "1", "z" => { "w" => "a" } }, { "y" => "2", "z" => { "w" => "b" } }] Rack::Utils.parse_nested_query("data[books][][data][page]=1&data[books][][data][page]=2"). - must_equal "data" => { "books" => [{ "data" => { "page" => "1"}}, { "data" => { "page" => "2"}}] } + must_equal "data" => { "books" => [{ "data" => { "page" => "1" } }, { "data" => { "page" => "2" } }] } lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y]z=2") }. must_raise(Rack::Utils::ParameterTypeError). @@ -231,13 +232,25 @@ describe Rack::Utils do message.must_equal "invalid byte sequence in UTF-8" end + it "only moves to a new array when the full key has been seen" do + Rack::Utils.parse_nested_query("x[][y][][z]=1&x[][y][][w]=2"). + must_equal "x" => [{ "y" => [{ "z" => "1", "w" => "2" }] }] + + Rack::Utils.parse_nested_query( + "x[][id]=1&x[][y][a]=5&x[][y][b]=7&x[][z][id]=3&x[][z][w]=0&x[][id]=2&x[][y][a]=6&x[][y][b]=8&x[][z][id]=4&x[][z][w]=0" + ).must_equal "x" => [ + { "id" => "1", "y" => { "a" => "5", "b" => "7" }, "z" => { "id" => "3", "w" => "0" } }, + { "id" => "2", "y" => { "a" => "6", "b" => "8" }, "z" => { "id" => "4", "w" => "0" } }, + ] + end + it "allow setting the params hash class to use for parsing query strings" do begin default_parser = Rack::Utils.default_query_parser param_parser_class = Class.new(Rack::QueryParser::Params) do def initialize(*) super - @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)} + @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end Rack::Utils.default_query_parser = Rack::QueryParser.new(param_parser_class, 65536, 100) @@ -308,7 +321,7 @@ describe Rack::Utils do must_equal 'x[y][][z]=1&x[y][][z]=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' - Rack::Utils.build_nested_query({"foo" => ["1", ["2"]]}). + Rack::Utils.build_nested_query({ "foo" => ["1", ["2"]] }). must_equal 'foo[]=1&foo[][]=2' lambda { Rack::Utils.build_nested_query("foo=bar") }. @@ -317,24 +330,24 @@ describe Rack::Utils do end it 'performs the inverse function of #parse_nested_query' do - [{"foo" => nil, "bar" => ""}, - {"foo" => "bar", "baz" => ""}, - {"foo" => ["1", "2"]}, - {"foo" => "bar", "baz" => ["1", "2", "3"]}, - {"foo" => ["bar"], "baz" => ["1", "2", "3"]}, - {"foo" => ["1", "2"]}, - {"foo" => "bar", "baz" => ["1", "2", "3"]}, - {"x" => {"y" => {"z" => "1"}}}, - {"x" => {"y" => {"z" => ["1"]}}}, - {"x" => {"y" => {"z" => ["1", "2"]}}}, - {"x" => {"y" => [{"z" => "1"}]}}, - {"x" => {"y" => [{"z" => ["1"]}]}}, - {"x" => {"y" => [{"z" => "1", "w" => "2"}]}}, - {"x" => {"y" => [{"v" => {"w" => "1"}}]}}, - {"x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]}}, - {"x" => {"y" => [{"z" => "1"}, {"z" => "2"}]}}, - {"x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}}, - {"foo" => ["1", ["2"]]}, + [{ "foo" => nil, "bar" => "" }, + { "foo" => "bar", "baz" => "" }, + { "foo" => ["1", "2"] }, + { "foo" => "bar", "baz" => ["1", "2", "3"] }, + { "foo" => ["bar"], "baz" => ["1", "2", "3"] }, + { "foo" => ["1", "2"] }, + { "foo" => "bar", "baz" => ["1", "2", "3"] }, + { "x" => { "y" => { "z" => "1" } } }, + { "x" => { "y" => { "z" => ["1"] } } }, + { "x" => { "y" => { "z" => ["1", "2"] } } }, + { "x" => { "y" => [{ "z" => "1" }] } }, + { "x" => { "y" => [{ "z" => ["1"] }] } }, + { "x" => { "y" => [{ "z" => "1", "w" => "2" }] } }, + { "x" => { "y" => [{ "v" => { "w" => "1" } }] } }, + { "x" => { "y" => [{ "z" => "1", "v" => { "w" => "2" } }] } }, + { "x" => { "y" => [{ "z" => "1" }, { "z" => "2" }] } }, + { "x" => { "y" => [{ "z" => "1", "w" => "a" }, { "z" => "2", "w" => "3" }] } }, + { "foo" => ["1", ["2"]] }, ].each { |params| qs = Rack::Utils.build_nested_query(params) Rack::Utils.parse_nested_query(qs).must_equal params @@ -382,7 +395,7 @@ describe Rack::Utils do Rack::Utils.best_q_match("text/plain,text/html", %w[text/html text/plain]).must_equal "text/html" # When there are no matches, return nil: - Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).must_equal nil + Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).must_be_nil end it "escape html entities [&><'\"/]" do @@ -415,9 +428,9 @@ describe Rack::Utils do Rack::Utils.select_best_encoding(a, b) end - helper.call(%w(), [["x", 1]]).must_equal nil - helper.call(%w(identity), [["identity", 0.0]]).must_equal nil - helper.call(%w(identity), [["*", 0.0]]).must_equal nil + helper.call(%w(), [["x", 1]]).must_be_nil + helper.call(%w(identity), [["identity", 0.0]]).must_be_nil + helper.call(%w(identity), [["*", 0.0]]).must_be_nil helper.call(%w(identity), [["compress", 1.0], ["gzip", 1.0]]).must_equal "identity" @@ -449,6 +462,12 @@ describe Rack::Utils do Rack::Utils.status_code(:ok).must_equal 200 end + it "raise an error for an invalid symbol" do + assert_raises(ArgumentError, "Unrecognized status code :foobar") do + Rack::Utils.status_code(:foobar) + end + end + it "return rfc2822 format from rfc2822 helper" do Rack::Utils.rfc2822(Time.at(0).gmtime).must_equal "Thu, 01 Jan 1970 00:00:00 -0000" end @@ -480,19 +499,19 @@ end describe Rack::Utils, "cookies" do it "parses cookies" do env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m") - Rack::Utils.parse_cookies(env).must_equal({"zoo" => "m"}) + Rack::Utils.parse_cookies(env).must_equal({ "zoo" => "m" }) env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=%") - Rack::Utils.parse_cookies(env).must_equal({"foo" => "%"}) + Rack::Utils.parse_cookies(env).must_equal({ "foo" => "%" }) env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;foo=car") - Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"}) + Rack::Utils.parse_cookies(env).must_equal({ "foo" => "bar" }) env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m") - Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar", "quux" => "h&m"}) + Rack::Utils.parse_cookies(env).must_equal({ "foo" => "bar", "quux" => "h&m" }) env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar").freeze - Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"}) + Rack::Utils.parse_cookies(env).must_equal({ "foo" => "bar" }) end it "adds new cookies to nil header" do @@ -526,48 +545,48 @@ end describe Rack::Utils, "byte_range" do it "ignore missing or syntactically invalid byte ranges" do - Rack::Utils.byte_ranges({},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "foobar"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "furlongs=123-456"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes="},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123,456"},500).must_equal nil + Rack::Utils.byte_ranges({}, 500).must_be_nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "foobar" }, 500).must_be_nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "furlongs=123-456" }, 500).must_be_nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=" }, 500).must_be_nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-" }, 500).must_be_nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123,456" }, 500).must_be_nil # A range of non-positive length is syntactically invalid and ignored: - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-123"},500).must_equal nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_equal nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=456-123" }, 500).must_be_nil + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=456-455" }, 500).must_be_nil end it "parse simple byte ranges" do - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-456"},500).must_equal [(123..456)] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-"},500).must_equal [(123..499)] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-100"},500).must_equal [(400..499)] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=0-0"},500).must_equal [(0..0)] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=499-499"},500).must_equal [(499..499)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 500).must_equal [(123..456)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-" }, 500).must_equal [(123..499)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-100" }, 500).must_equal [(400..499)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-0" }, 500).must_equal [(0..0)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=499-499" }, 500).must_equal [(499..499)] end it "parse several byte ranges" do - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-600,601-999"},1000).must_equal [(500..600),(601..999)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=500-600,601-999" }, 1000).must_equal [(500..600), (601..999)] end it "truncate byte ranges" do - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-999"},500).must_equal [(123..499)] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=600-999"},500).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-999"},500).must_equal [(0..499)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-999" }, 500).must_equal [(123..499)] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=600-999" }, 500).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-999" }, 500).must_equal [(0..499)] end it "ignore unsatisfiable byte ranges" do - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-501"},500).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-"},500).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=999-"},500).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-0"},500).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=500-501" }, 500).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=500-" }, 500).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=999-" }, 500).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-0" }, 500).must_equal [] end it "handle byte ranges of empty files" do - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-456"},0).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=0-"},0).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-100"},0).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=0-0"},0).must_equal [] - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-0"},0).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 0).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-" }, 0).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-100" }, 0).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-0" }, 0).must_equal [] + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-0" }, 0).must_equal [] end end @@ -600,7 +619,7 @@ describe Rack::Utils::HeaderHash do it "merge case-insensitively" do h = Rack::Utils::HeaderHash.new("ETag" => 'HELLO', "content-length" => '123') merged = h.merge("Etag" => 'WORLD', 'Content-Length' => '321', "Foo" => 'BAR') - merged.must_equal "Etag"=>'WORLD', "Content-Length"=>'321', "Foo"=>'BAR' + merged.must_equal "Etag" => 'WORLD', "Content-Length" => '321', "Foo" => 'BAR' end it "overwrite case insensitively and assume the new key's case" do @@ -623,7 +642,7 @@ describe Rack::Utils::HeaderHash do it "replace hashes correctly" do h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz") - j = {"foo" => "bar"} + j = { "foo" => "bar" } h.replace(j) h["foo"].must_equal "bar" end @@ -652,16 +671,16 @@ 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 it "convert Array values to Strings when responding to #each" do h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"]) - h.each do |k,v| + h.each do |k, v| k.must_equal "foo" v.must_equal "bar\nbaz" end @@ -678,14 +697,14 @@ end describe Rack::Utils::Context do class ContextTest attr_reader :app - def initialize app; @app=app; end + def initialize app; @app = app; end def call env; context env; end - def context env, app=@app; app.call(env); end + def context env, app = @app; app.call(env); end end - test_target1 = proc{|e| e.to_s+' world' } - test_target2 = proc{|e| e.to_i+2 } + test_target1 = proc{|e| e.to_s + ' world' } + test_target2 = proc{|e| e.to_i + 2 } test_target3 = proc{|e| nil } - test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain', 'Content-Length'=>'0'},['']] } + test_target4 = proc{|e| [200, { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }, ['']] } test_app = ContextTest.new test_target4 it "set context correctly" do diff --git a/test/spec_version.rb b/test/spec_version.rb index 6ab0a74c..d4191aa4 100644 --- a/test/spec_version.rb +++ b/test/spec_version.rb @@ -1,5 +1,6 @@ -# -*- encoding: utf-8 -*- -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack' describe Rack do diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb index 9ae6103d..0d0aa8f7 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -1,6 +1,8 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/mock' -require 'concurrent/atomic/count_down_latch' +require 'thread' require File.expand_path('../testrequest', __FILE__) Thread.abort_on_exception = true @@ -9,10 +11,10 @@ describe Rack::Handler::WEBrick do include TestRequest::Helpers before do - @server = WEBrick::HTTPServer.new(:Host => @host='127.0.0.1', - :Port => @port=9202, - :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - :AccessLog => []) + @server = WEBrick::HTTPServer.new(Host: @host = '127.0.0.1', + Port: @port = 9202, + Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), + AccessLog: []) @server.mount "/test", Rack::Handler::WEBrick, Rack::Lint.new(TestRequest.new) @thread = Thread.new { @server.start } @@ -49,7 +51,7 @@ describe Rack::Handler::WEBrick do it "have rack headers" do GET("/test") - response["rack.version"].must_equal [1,3] + response["rack.version"].must_equal [1, 3] response["rack.multithread"].must_equal true assert_equal false, response["rack.multiprocess"] assert_equal false, response["rack.run_once"] @@ -80,7 +82,7 @@ describe Rack::Handler::WEBrick do end it "have CGI headers on POST" do - POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'}) + POST("/test", { "rack-form-data" => "23" }, { 'X-test-header' => '42' }) status.must_equal 200 response["REQUEST_METHOD"].must_equal "POST" response["SCRIPT_NAME"].must_equal "/test" @@ -92,7 +94,7 @@ describe Rack::Handler::WEBrick do end it "support HTTP auth" do - GET("/test", {:user => "ruth", :passwd => "secret"}) + GET("/test", { user: "ruth", passwd: "secret" }) response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" end @@ -119,25 +121,33 @@ describe Rack::Handler::WEBrick do end it "provide a .run" do - block_ran = false - latch = Concurrent::CountDownLatch.new 1 + queue = Queue.new t = Thread.new do Rack::Handler::WEBrick.run(lambda {}, { - :Host => '127.0.0.1', - :Port => 9210, - :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - :AccessLog => []}) { |server| - block_ran = true + Host: '127.0.0.1', + Port: 9210, + Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), + AccessLog: [] }) { |server| assert_kind_of WEBrick::HTTPServer, server - @s = server - latch.count_down + queue.push(server) } end - latch.wait - @s.shutdown + server = queue.pop + + # The server may not yet have started: wait for it + seconds = 10 + wait_time = 0.1 + until server.status == :Running || seconds <= 0 + seconds -= wait_time + sleep wait_time + end + + raise "Server never reached status 'Running'" unless server.status == :Running + + server.shutdown t.join end @@ -171,7 +181,7 @@ describe Rack::Handler::WEBrick do Rack::Lint.new(lambda{ |req| [ 200, - {"rack.hijack" => io_lambda}, + [ [ "rack.hijack", io_lambda ] ], [""] ] }) @@ -187,7 +197,7 @@ describe Rack::Handler::WEBrick do Rack::Lint.new(lambda{ |req| [ 200, - {"Transfer-Encoding" => "chunked"}, + { "Transfer-Encoding" => "chunked" }, ["7\r\nchunked\r\n0\r\n\r\n"] ] }) @@ -195,14 +205,14 @@ describe Rack::Handler::WEBrick do Net::HTTP.start(@host, @port){ |http| res = http.get("/chunked") res["Transfer-Encoding"].must_equal "chunked" - res["Content-Length"].must_equal nil + res["Content-Length"].must_be_nil res.body.must_equal "chunked" } end after do - @status_thread.join - @server.shutdown - @thread.join + @status_thread.join + @server.shutdown + @thread.join end end diff --git a/test/testrequest.rb b/test/testrequest.rb index cacd23d5..aabe7fa6 100644 --- a/test/testrequest.rb +++ b/test/testrequest.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'yaml' require 'net/http' require 'rack/lint' @@ -10,10 +12,10 @@ class TestRequest env["test.postdata"] = env["rack.input"].read minienv = env.dup # This may in the future want to replace with a dummy value instead. - minienv.delete_if { |k,v| NOSERIALIZE.any? { |c| v.kind_of?(c) } } + minienv.delete_if { |k, v| NOSERIALIZE.any? { |c| v.kind_of?(c) } } body = minienv.to_yaml size = body.bytesize - [status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]] + [status, { "Content-Type" => "text/yaml", "Content-Length" => size.to_s }, [body]] end module Helpers @@ -30,7 +32,7 @@ class TestRequest "#{ROOT}/bin/rackup" end - def GET(path, header={}) + def GET(path, header = {}) Net::HTTP.start(@host, @port) { |http| user = header.delete(:user) passwd = header.delete(:passwd) @@ -48,7 +50,7 @@ class TestRequest } end - def POST(path, formdata={}, header={}) + def POST(path, formdata = {}, header = {}) Net::HTTP.start(@host, @port) { |http| user = header.delete(:user) passwd = header.delete(:passwd) @@ -67,7 +69,7 @@ end class StreamingRequest def self.call(env) - [200, {"Content-Type" => "text/plain"}, new] + [200, { "Content-Type" => "text/plain" }, new] end def each diff --git a/test/unregistered_handler/rack/handler/unregistered.rb b/test/unregistered_handler/rack/handler/unregistered.rb index 3ca5a72c..e98468cc 100644 --- a/test/unregistered_handler/rack/handler/unregistered.rb +++ b/test/unregistered_handler/rack/handler/unregistered.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Handler # this class doesn't do anything, we're just seeing if we get it. diff --git a/test/unregistered_handler/rack/handler/unregistered_long_one.rb b/test/unregistered_handler/rack/handler/unregistered_long_one.rb index 2c2fae17..87c6c254 100644 --- a/test/unregistered_handler/rack/handler/unregistered_long_one.rb +++ b/test/unregistered_handler/rack/handler/unregistered_long_one.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rack module Handler # this class doesn't do anything, we're just seeing if we get it. |