diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..a89fa6d1737698609c76fc17741a70de68ec88e1 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,107 @@ +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 + - test-ruby-2.7 + +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-ruby-2.7: + docker: + - image: circleci/ruby:2.7 + # 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/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..7a7ad3a55d7fca35fce01e6b8280538f2e113710 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +RDOX +ChangeLog +*.gem +lighttpd.errors +*.rbc +stage +*.tar.gz +Gemfile.lock +.rbx +doc +/.bundle +/.yardoc diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000000000000000000000000000000000..22ed992086ac71f876125e8b1bc2a3c6f3b3ed26 --- /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/.yardopts b/.yardopts new file mode 100644 index 0000000000000000000000000000000000000000..f4d6aebae4cc1d7b85159bfcdf95421c287f55bc --- /dev/null +++ b/.yardopts @@ -0,0 +1,2 @@ +- +SPEC diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..4f0be3424ea7b43a7efd459771e0241eae154a84 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,77 @@ +## [2.1.4] - 2020-06-15 + +- [CVE-2020-8184] When parsing cookies, only decode the value + +## [2.1.3] - 2020-05-12 + +- [CVE-2020-8161] Use Dir.entries instead of Dir[glob] to prevent user-specified glob metacharacters +- +## [2.1.2] - 2020-01-27 + +- Fix multipart parser for some files to prevent denial of service ([@aiomaster](https://github.com/aiomaster)) +- Fix `Rack::Builder#use` with keyword arguments ([@kamipo](https://github.com/kamipo)) +- Skip deflating in Rack::Deflater if Content-Length is 0 ([@jeremyevans](https://github.com/jeremyevans)) +- Remove `SessionHash#transform_keys`, no longer needed ([@pavel](https://github.com/pavel)) +- Add to_hash to wrap Hash and Session classes ([@oleh-demyanyuk](https://github.com/oleh-demyanyuk)) +- Handle case where session id key is requested but missing ([@jeremyevans](https://github.com/jeremyevans)) + +## [2.1.1] - 2020-01-12 + +- Remove `Rack::Chunked` from `Rack::Server` default middleware. ([#1475](https://github.com/rack/rack/pull/1475), [@ioquatix](https://github.com/ioquatix)) + +## [2.1.0] - 2020-01-10 + +### Added + +- Add support for `SameSite=None` cookie value. ([@hennikul](https://github.com/hennikul)) +- Add trailer headers. ([@eileencodes](https://github.com/eileencodes)) +- Add MIME Types for video streaming. ([@styd](https://github.com/styd)) +- Add MIME Type for WASM. ([@buildrtech](https://github.com/buildrtech)) +- Add `Early Hints(103)` to status codes. ([@egtra](https://github.com/egtra)) +- Add `Too Early(425)` to status codes. ([@y-yagi]((https://github.com/y-yagi))) +- Add `Bandwidth Limit Exceeded(509)` to status codes. ([@CJKinni](https://github.com/CJKinni)) +- Add method for custom `ip_filter`. ([@svcastaneda](https://github.com/svcastaneda)) +- Add boot-time profiling capabilities to `rackup`. ([@tenderlove](https://github.com/tenderlove)) +- Add multi mapping support for `X-Accel-Mappings` header. ([@yoshuki](https://github.com/yoshuki)) +- Add `sync: false` option to `Rack::Deflater`. (Eric Wong) +- Add `Builder#freeze_app` to freeze application and all middleware instances. ([@jeremyevans](https://github.com/jeremyevans)) +- Add API to extract cookies from `Rack::MockResponse`. ([@petercline](https://github.com/petercline)) + +### Changed + +- Don't propagate nil values from middleware. ([@ioquatix](https://github.com/ioquatix)) +- Lazily initialize the response body and only buffer it if required. ([@ioquatix](https://github.com/ioquatix)) +- Fix deflater zlib buffer errors on empty body part. ([@felixbuenemann](https://github.com/felixbuenemann)) +- Set `X-Accel-Redirect` to percent-encoded path. ([@diskkid](https://github.com/diskkid)) +- Remove unnecessary buffer growing when parsing multipart. ([@tainoe](https://github.com/tainoe)) +- Expand the root path in `Rack::Static` upon initialization. ([@rosenfeld](https://github.com/rosenfeld)) +- Make `ShowExceptions` work with binary data. ([@axyjo](https://github.com/axyjo)) +- Use buffer string when parsing multipart requests. ([@janko-m](https://github.com/janko-m)) +- Support optional UTF-8 Byte Order Mark (BOM) in config.ru. ([@mikegee](https://github.com/mikegee)) +- Handle `X-Forwarded-For` with optional port. ([@dpritchett](https://github.com/dpritchett)) +- Use `Time#httpdate` format for Expires, as proposed by RFC 7231. ([@nanaya](https://github.com/nanaya)) +- Make `Utils.status_code` raise an error when the status symbol is invalid instead of `500`. ([@adambutler](https://github.com/adambutler)) +- Rename `Request::SCHEME_WHITELIST` to `Request::ALLOWED_SCHEMES`. +- Make `Multipart::Parser.get_filename` accept files with `+` in their name. ([@lucaskanashiro](https://github.com/lucaskanashiro)) +- Add Falcon to the default handler fallbacks. ([@ioquatix](https://github.com/ioquatix)) +- Update codebase to avoid string mutations in preparation for `frozen_string_literals`. ([@pat](https://github.com/pat)) +- Change `MockRequest#env_for` to rely on the input optionally responding to `#size` instead of `#length`. ([@janko](https://github.com/janko)) +- Rename `Rack::File` -> `Rack::Files` and add deprecation notice. ([@postmodern](https://github.com/postmodern)). +- Prefer Base64 “strict encoding†for Base64 cookies. ([@ioquatix](https://github.com/ioquatix)) + +### Removed + +- Remove `to_ary` from Response ([@tenderlove](https://github.com/tenderlove)) +- Deprecate `Rack::Session::Memcache` in favor of `Rack::Session::Dalli` from dalli gem ([@fatkodima](https://github.com/fatkodima)) + +### Fixed + +- Eliminate warnings for Ruby 2.7. ([@osamtimizer](https://github.com/osamtimizer])) + +### Documentation + +- Update broken example in `Session::Abstract::ID` documentation. ([tonytonyjan](https://github.com/tonytonyjan)) +- Add Padrino to the list of frameworks implmenting Rack. ([@wikimatze](https://github.com/wikimatze)) +- Remove Mongrel from the suggested server options in the help output. ([@tricknotes](https://github.com/tricknotes)) +- Replace `HISTORY.md` and `NEWS.md` with `CHANGELOG.md`. ([@twitnithegirl](https://github.com/twitnithegirl)) +- CHANGELOG updates. ([@drenmi](https://github.com/Drenmi), [@p8](https://github.com/p8)) diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000000000000000000000000000000000000..62a3494e88155ae9d7f6e617e6d3bed148fa8683 --- /dev/null +++ b/Gemfile @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec + +# What we need to do here is just *exclude* JRuby, but bundler has no way to do +# this, because of some argument that I know I had with Yehuda and Carl years +# ago, but I've since forgotten. Anyway, we actually need it here, and it's not +# avaialable, so prepare yourself for a yak shave when this breaks. +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 'dalli' + gem 'thin', platforms: c_platforms +end + +group :doc do + gem 'rdoc' +end diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index 406d1758ca840c060476ad2c502e501ba72869ac..0000000000000000000000000000000000000000 --- a/HISTORY.md +++ /dev/null @@ -1,505 +0,0 @@ -Sun Dec 4 18:48:03 2015 Jeremy Daer <jeremydaer@gmail.com> - - * 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. - -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> - - * 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> - - * 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 - -Thu Aug 27 15:43:48 2015 Aaron Patterson <tenderlove@ruby-lang.org> - - * 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 - 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 - 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 - 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 - - 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 - - Request#trusted_proxy? now only matches whole stirngs - - Add JSON cookie coder, to be default in Rack 1.6+ due to security concerns - - URLMap host matching in environments that don't set the Host header fixed - - Fix a race condition that could result in overwritten pidfiles - - Various documentation additions - -### February 7th, 2013, Thirty fifth public release 1.4.5 - - 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 - - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie - -### January 28th, 2013: Thirty fourth public release 1.5.1 - - 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 - - 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 - - Rack::Auth::AbstractRequest#scheme now yields strings, not symbols - - Rack::Utils cookie functions now format expires in RFC 2822 format - - Rack::File now has a default mime type - - rackup -b 'run Rack::File.new(".")', option provides command line configs - - Rack::Deflater will no longer double encode bodies - - Rack::Mime#match? provides convenience for Accept header matching - - Rack::Utils#q_values provides splitting for Accept headers - - Rack::Utils#best_q_match provides a helper for Accept headers - - Rack::Handler.pick provides convenience for finding available servers - - Puma added to the list of default servers (preferred over Webrick) - - Various middleware now correctly close body when replacing it - - Rack::Request#params is no longer persistent with only GET params - - Rack::Request#update_param and #delete_param provide persistent operations - - Rack::Request#trusted_proxy? now returns true for local unix sockets - - Rack::Response no longer forces Content-Types - - Rack::Sendfile provides local mapping configuration options - - Rack::Utils#rfc2109 provides old netscape style time output - - 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 - - [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 - - Security: Prevent unbounded reads in large multipart boundaries - -### January 7th, 2013: Thirtieth public release 1.3.8 - - Security: Prevent unbounded reads in large multipart boundaries - -### January 6th, 2013: Twenty ninth public release 1.4.2 - - Add warnings when users do not provide a session secret - - Fix parsing performance for unquoted filenames - - Updated URI backports - - Fix URI backport version matching, and silence constant warnings - - Correct parameter parsing with empty values - - Correct rackup '-I' flag, to allow multiple uses - - Correct rackup pidfile handling - - Report rackup line numbers correctly - - Fix request loops caused by non-stale nonces with time limits - - Fix reloader on Windows - - Prevent infinite recursions from Response#to_ary - - Various middleware better conforms to the body close specification - - Updated language for the body close specification - - Additional notes regarding ECMA escape compatibility issues - - Fix the parsing of multiple ranges in range headers - - Prevent errors from empty parameter keys - - Added PATCH verb to Rack::Request - - Various documentation updates - - Fix session merge semantics (fixes rack-test) - - Rack::Static :index can now handle multiple directories - - All tests now utilize Rack::Lint (special thanks to Lars Gierth) - - Rack::File cache_control parameter is now deprecated, and removed by 1.5 - - Correct Rack::Directory script name escaping - - Rack::Static supports header rules for sophisticated configurations - - Multipart parsing now works without a Content-Length header - - New logos courtesy of Zachary Scott! - - 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 - - Add warnings when users do not provide a session secret - - Fix parsing performance for unquoted filenames - - Updated URI backports - - Fix URI backport version matching, and silence constant warnings - - Correct parameter parsing with empty values - - Correct rackup '-I' flag, to allow multiple uses - - Correct rackup pidfile handling - - Report rackup line numbers correctly - - Fix request loops caused by non-stale nonces with time limits - - Fix reloader on Windows - - Prevent infinite recursions from Response#to_ary - - Various middleware better conforms to the body close specification - - Updated language for the body close specification - - 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 - - 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 - - Add warnings when users do not provide a session secret - -### January 22nd, 2012: Twenty fifth public release 1.4.1 - - 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) - - Rack::File now returns 404 for illegal directory traversals - - Rack::File now returns 405 for illegal methods (non HEAD/GET) - - Rack::Cascade now catches 405 by default, as well as 404 - - Cookies missing '--' no longer cause an exception to be raised - - Various style changes and documentation spelling errors - - Rack::BodyProxy always ensures to execute its block - - Additional test coverage around cookies and secrets - - Rack::Session::Cookie can now be supplied either secret or old_secret - - Tests are no longer dependent on set order - - Rack::Static no longer defaults to serving index files - - Rack.release was fixed - -### December 28th, 2011: Twenty fourth public release 1.4.0 - - 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 - - Rack::ContentType will not set Content-Type for responses without a body - - Status code 205 does not send a response body - - Rack::Response::Helpers will not rely on instance variables - - Rack::Utils.build_query no longer outputs '=' for nil query values - - Various mime types added - - Rack::MockRequest now supports HEAD - - Rack::Directory now supports files that contain RFC3986 reserved chars - - Rack::File now only supports GET and HEAD requests - - Rack::Server#start now passes the block to Rack::Handler::<h>#run - - Rack::Static now supports an index option - - Added the Teapot status code - - rackup now defaults to Thin instead of Mongrel (if installed) - - 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. - - 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 - - Fix annoying warnings caused by the backport in 1.3.4 - -### October 1, 2011: Nineteenth public release 1.3.4 - - 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 - - Fix a bug with MRI regex engine to prevent XSS by malformed unicode - -### September 16, 2011: Seventeenth public release 1.3.3 - - 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 - - Fix for Rails and rack-test, Rack::Utils#escape calls to_s - -### July 13, 2011: Fifteenth public release 1.3.1 - - Fix 1.9.1 support - - Fix JRuby support - - Properly handle $KCODE in Rack::Utils.escape - - Make method_missing/respond_to behavior consistent for Rack::Lock, - Rack::Auth::Digest::Request and Rack::Multipart::UploadedFile - - Reenable passing rack.session to session middleware - - Rack::CommonLogger handles streaming responses correctly - - 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 - - Pulled in relevant bug fixes from 1.3 - - Fixed 1.8.6 support - -### May 22nd, 2011: Thirteenth public release 1.3.0 - - Various performance optimizations - - Various multipart fixes - - Various multipart refactors - - Infinite loop fix for multipart - - Test coverage for Rack::Server returns - - Allow files with '..', but not path components that are '..' - - rackup accepts handler-specific options on the command line - - Request#params no longer merges POST into GET (but returns the same) - - Use URI.encode_www_form_component instead. Use core methods for escaping. - - Allow multi-line comments in the config file - - Bug L#94 reported by Nikolai Lugovoi, query parameter unescaping. - - Rack::Response now deletes Content-Length when appropriate - - Rack::Deflater now supports streaming - - Improved Rack::Handler loading and searching - - Support for the PATCH verb - - env['rack.session.options'] now contains session options - - Cookies respect renew - - Session middleware uses SecureRandom.hex - -### March 13th, 2011: Twelfth public release 1.2.2/1.1.2. - - 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. - - 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. - - Removed Camping adapter: Camping 2.0 supports Rack as-is - - Removed parsing of quoted values - - Add Request.trace? and Request.options? - - Add mime-type for .webm and .htc - - Fix HTTP_X_FORWARDED_FOR - - Various multipart fixes - - Switch test suite to bacon - -### January 3rd, 2010: Ninth public release 1.1.0. - - Moved Auth::OpenID to rack-contrib. - - SPEC change that relaxes Lint slightly to allow subclasses of the - required types - - SPEC change to document rack.input binary mode in greator detail - - SPEC define optional rack.logger specification - - File servers support X-Cascade header - - Imported Config middleware - - Imported ETag middleware - - Imported Runtime middleware - - Imported Sendfile middleware - - New Logger and NullLogger middlewares - - Added mime type for .ogv and .manifest. - - Don't squeeze PATH_INFO slashes - - Use Content-Type to determine POST params parsing - - Update Rack::Utils::HTTP_STATUS_CODES hash - - Add status code lookup utility - - Response should call #to_i on the status - - Add Request#user_agent - - Request#host knows about forwared host - - Return an empty string for Request#host if HTTP_HOST and - SERVER_NAME are both missing - - Allow MockRequest to accept hash params - - Optimizations to HeaderHash - - Refactored rackup into Rack::Server - - Added Utils.build_nested_query to complement Utils.parse_nested_query - - Added Utils::Multipart.build_multipart to complement - Utils::Multipart.parse_multipart - - Extracted set and delete cookie helpers into Utils so they can be - used outside Response - - Extract parse_query and parse_multipart in Request so subclasses - can change their behavior - - 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. - - Bump remainder of rack.versions. - - Support the pure Ruby FCGI implementation. - - Fix for form names containing "=": split first then unescape components - - Fixes the handling of the filename parameter with semicolons in names. - - Add anchor to nested params parsing regexp to prevent stack overflows - - Use more compatible gzip write api instead of "<<". - - Make sure that Reloader doesn't break when executed via ruby -e - - Make sure WEBrick respects the :Host option - - Many Ruby 1.9 fixes. - -### April 25th, 2009: Seventh public release 1.0.0. - - 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 - encoding is used. - - SPEC change: rack.input must be rewindable and support reading into - a buffer, wrap with Rack::RewindableInput if it isn't. - - SPEC change: rack.session is now specified. - - SPEC change: Bodies can now additionally respond to #to_path with - a filename to be served. - - NOTE: String bodies break in 1.9, use an Array consisting of a - single String instead. - - New middleware Rack::Lock. - - New middleware Rack::ContentType. - - Rack::Reloader has been rewritten. - - Major update to Rack::Auth::OpenID. - - Support for nested parameter parsing in Rack::Response. - - Support for redirects in Rack::Response. - - HttpOnly cookie support in Rack::Response. - - The Rakefile has been rewritten. - - Many bugfixes and small improvements. - -### January 9th, 2009: Sixth public release 0.9.1. - - Fix directory traversal exploits in Rack::File and Rack::Directory. - -### January 6th, 2009: Fifth public release 0.9. - - Rack is now managed by the Rack Core Team. - - Rack::Lint is stricter and follows the HTTP RFCs more closely. - - Added ConditionalGet middleware. - - Added ContentLength middleware. - - Added Deflater middleware. - - Added Head middleware. - - Added MethodOverride middleware. - - Rack::Mime now provides popular MIME-types and their extension. - - Mongrel Header now streams. - - Added Thin handler. - - Official support for swiftiplied Mongrel. - - Secure cookies. - - Made HeaderHash case-preserving. - - Many bugfixes and small improvements. - -### August 21st, 2008: Fourth public release 0.4. - - New middleware, Rack::Deflater, by Christoffer Sawicki. - - OpenID authentication now needs ruby-openid 2. - - New Memcache sessions, by blink. - - Explicit EventedMongrel handler, by Joshua Peek <josh@joshpeek.com> - - Rack::Reloader is not loaded in rackup development mode. - - rackup can daemonize with -D. - - Many bugfixes, especially for pool sessions, URLMap, thread safety - and tempfile handling. - - Improved tests. - - Rack moved to Git. - -### February 26th, 2008: Third public release 0.3. - - LiteSpeed handler, by Adrian Madrid. - - SCGI handler, by Jeremy Evans. - - Pool sessions, by blink. - - OpenID authentication, by blink. - - :Port and :File options for opening FastCGI sockets, by blink. - - Last-Modified HTTP header for Rack::File, by blink. - - Rack::Builder#use now accepts blocks, by Corey Jewett. - (See example/protectedlobster.ru) - - 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. - - HTTP Basic authentication. - - Cookie Sessions. - - Static file handler. - - Improved Rack::Request. - - Improved Rack::Response. - - Added Rack::ShowStatus, for better default error messages. - - Bug fixes in the Camping adapter. - - Removed Rails adapter, was too alpha. - -### March 3rd, 2007: First public release 0.1. - -/* vim: set filetype=changelog */ diff --git a/COPYING b/MIT-LICENSE similarity index 89% rename from COPYING rename to MIT-LICENSE index 1f5c7013370574c7f023c1697ee2644cbf93a423..703d118f9a293d561fd731038ffd994777c19ed2 100644 --- a/COPYING +++ b/MIT-LICENSE @@ -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. diff --git a/README.rdoc b/README.rdoc index a6100e95a3dbec0eaf6feab6922d3b1a3a461b0d..5d4ad7c4e394bc1c236014c7807d7c68553353ce 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,99 +1,103 @@ -= 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/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: -* WEBrick +The included *handlers* connect all kinds of web servers to \Rack: + +* WEBrick[https://github.com/ruby/webrick] * FCGI * CGI * SCGI -* LiteSpeed -* Thin - -These web servers include Rack handlers in their distributions: -* 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 +* LiteSpeed[https://www.litespeedtech.com/] +* Thin[https://rubygems.org/gems/thin] + +These web servers include \Rack handlers in their distributions: + +* Agoo[https://github.com/ohler55/agoo] +* Falcon[https://github.com/socketry/falcon] +* {NGINX Unit}[https://unit.nginx.org/] +* {Phusion Passenger}[https://www.phusionpassenger.com/] (which is mod_rack for Apache and for nginx) +* Puma[https://puma.io/] +* Unicorn[https://bogomips.org/unicorn/] +* uWSGI[https://uwsgi-docs.readthedocs.io/en/latest/] + +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: -* Camping -* Coset -* Espresso -* Halcyon -* Mack -* Maveric -* Merb +These frameworks include \Rack adapters in their distributions: + +* Camping[http://www.ruby-camping.com/] +* Coset[http://leahneukirchen.org/repos/coset/] +* Hanami[https://hanamirb.org/] +* Padrino[http://padrinorb.com/] * Racktools::SimpleApplication -* Ramaze -* Ruby on Rails -* Rum -* Sinatra -* Sin -* Vintage -* Waves -* Wee +* Ramaze[http://ramaze.net/] +* Roda[https://github.com/jeremyevans/roda] +* {Ruby on Rails}[https://rubyonrails.org/] +* Rum[https://github.com/leahneukirchen/rum] +* Sinatra[http://sinatrarb.com/] +* Utopia[https://github.com/socketry/utopia] +* WABuR[https://github.com/ohler55/wabur] * ... 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 presenting them in a nice and helpful way with clickable backtrace. -* Rack::File, for serving static files. +* Rack::Files, for serving static files. * ...many others! All these components use the same interface, which is described in -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 +121,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 +137,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. @@ -146,37 +145,15 @@ installation and bacon. To run the test suite completely, you need: * fcgi - * memcache-client + * dalli * thin -The full set of tests test FCGI access with lighttpd (on port -9203) so you will need lighttpd installed as well as the FCGI -libraries and the fcgi gem: - -Download and install lighttpd: - - http://www.lighttpd.net/download - -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 - ./configure --prefix=/usr/local - make - sudo make install - cd .. - -Installing the Ruby fcgi gem: - - gem install fcgi - -Furthermore, to test Memcache sessions, you need memcached (will be -run on port 11211) and memcache-client installed. +To test Memcache sessions, you need memcached (will be +run on port 11211) and dalli 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 +175,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 +204,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 -* Leah 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 +215,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,34 +247,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. +== Links -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. +\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> -== Links +== License -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/group/rack-devel> -Rack's Rubyforge project:: <http://rubyforge.org/projects/rack> +\Rack is released under the {MIT License}[https://opensource.org/licenses/MIT]. diff --git a/Rakefile b/Rakefile index c112f1da8566685dcc68d5eec831ea931d9a2711..a365ff5420657f585f5ca8792f7fd0dc9f450a1f 100644 --- a/Rakefile +++ b/Rakefile @@ -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 new file mode 100644 index 0000000000000000000000000000000000000000..04fdd48839864f39c4735074875af33ff69524ea --- /dev/null +++ b/SECURITY_POLICY.md @@ -0,0 +1,66 @@ +# Rack maintenance + +## Supported versions + +### New features + +New features will only be added to the master branch and will not be made available in point releases. + +### Bug fixes + +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: 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: 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: 2.0.x +* Next most recent release series: 1.6.x +* Last most recent release series: 1.5.x + +### Unsupported Release Series + +When a release series is no longer supported, it’s your own responsibility to deal with bugs and security issues. We may provide back-ports of the fixes and publish them to git, however there will be no new versions released. If you are not comfortable maintaining your own versions, you should upgrade to a supported version. + +## 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/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 + +## Disclosure Policy + +Rack has a 5 step disclosure policy. + +1. Security report received and is assigned a primary handler. This person will coordinate the fix and release process. +2. Problem is confirmed and, a list of all affected versions is determined. Code is audited to find any potential similar problems. +3. Fixes are prepared for all releases which are still supported. These fixes are not committed to the public repository but rather held locally pending the announcement. +4. A suggested embargo date for this vulnerability is chosen and distros@openwall is notified. This notification will include patches for all versions still under support and a contact address for packagers who need advice back-porting patches to older versions. +5. On the embargo date, the [ruby security announcement mailing list](mailto:ruby-security-ann@googlegroups.com) is sent a copy of the announcement. The changes are pushed to the public repository and new gems released to rubygems. + +Typically the embargo date will be set 72 hours from the time vendor-sec is first notified, however this may vary depending on the severity of the bug or difficulty in applying a fix. + +This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it’s important that we follow the release process above to ensure that the disclosure is handled in a consistent manner. + +## Receiving Security Updates + +The best way to receive all the security announcements is to subscribe to the [ruby security announcement mailing list](mailto:ruby-security-ann@googlegroups.com). The mailing list is very low traffic, and it receives the public notifications the moment the embargo is lifted. If you produce packages of Rack and require prior notification of vulnerabilities, you should be subscribed to vendor-sec. + +No one outside the core team, the initial reporter or vendor-sec will be notified prior to the lifting of the embargo. We regret that we cannot make exceptions to this policy for high traffic or important sites, as any disclosure beyond the minimum required to coordinate a fix could cause an early leak of the vulnerability. + +## 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/forum/#!forum/rack-core). diff --git a/SPEC b/SPEC index d3a7ca8111d54dc13b1e6eab4bcfb917b81c2f62..59ec9d7202608bec9fe9a7e136c44aaf2a151253 100644 --- a/SPEC +++ b/SPEC @@ -60,9 +60,8 @@ below. 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: <tt>rack.version</tt>:: The Array representing this version of Rack @@ -226,9 +225,9 @@ This is an HTTP status. When parsed as integer (+to_i+), it must be greater than or equal to 100. === The Headers The header must respond to +each+, and yield values of key and value. +The header keys must be Strings. Special headers starting "rack." are for communicating with the server, and must not be sent back to the client. -The header keys must be Strings. The header must not contain a +Status+ key. The header must conform to RFC7230 token specification, i.e. cannot contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}". diff --git a/bin/rackup b/bin/rackup index ad94af4be3b4c41a0bdb7b356f4c96fdfd24fb2a..58988a0b32d16a93e818e02533fd5745b943dc09 100755 --- a/bin/rackup +++ b/bin/rackup @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "rack" Rack::Server.start diff --git a/debian/changelog b/debian/changelog index 2aa82b862938cb48dfcdebbfb42ff3c710c35d89..9989349ebdcf8b0a082c1ddd6b6931110c1d6a69 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,114 @@ +ruby-rack (2.1.4-2) unstable; urgency=medium + + * Revert "Drop all patches" + - Rack::Builder::parse_file#test_0006_strips + leading unicode byte order mark when present still + fails in i386 and stuff. Meh, I'll take a look later. + + -- Utkarsh Gupta <utkarsh@debian.org> Sun, 03 Jan 2021 17:49:29 +0530 + +ruby-rack (2.1.4-1) unstable; urgency=medium + + [ Pirate Praveen ] + * New upstream version 2.1.4 + * Bump Standards-Version to 4.5.1 (no changes needed) + * Drop patches applied upstream + + [ Utkarsh Gupta ] + * Drop all patches + + -- Utkarsh Gupta <utkarsh@debian.org> Sun, 03 Jan 2021 17:25:43 +0530 + +ruby-rack (2.1.1-6) unstable; urgency=medium + + [ Cédric Boutillier ] + * [ci skip] Update team name + * [ci skip] Add .gitattributes to keep unwanted files out + of the source package + + [ Debian Janitor ] + * Apply multi-arch hints. + ruby-rack: Add :all qualifier + for ruby dependency. + + [ Utkarsh Gupta ] + * When parsing cookies, only decode the values. + Patch utils to fix cookie parsing. (Fixes: CVE-2020-8184) + (Closes: #963477) + + -- Utkarsh Gupta <utkarsh@debian.org> Sat, 02 Jan 2021 17:42:02 +0530 + +ruby-rack (2.1.1-5) unstable; urgency=medium + + * Add patch to use Dir.entries instead of Dir[glob] to prevent + user-specified glob metacharacters (Fixes: CVE-2020-8161) + + -- Utkarsh Gupta <utkarsh@debian.org> Thu, 21 May 2020 17:06:27 +0530 + +ruby-rack (2.1.1-4) unstable; urgency=medium + + * Remove ruby-minitest-global-expectations from Depends + * Add ruby-minitest-global-expectations for tests + + -- Utkarsh Gupta <utkarsh@debian.org> Fri, 10 Apr 2020 18:37:00 +0530 + +ruby-rack (2.1.1-3) unstable; urgency=medium + + * Add patch to skip random failure + (probably fixed in later upstream version) + + -- Utkarsh Gupta <utkarsh@debian.org> Fri, 10 Apr 2020 04:21:09 +0530 + +ruby-rack (2.1.1-2) unstable; urgency=medium + + [ Debian Janitor ] + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + [ Utkarsh Gupta ] + * Shoot to unstable + * Enable tests :D + * Add BD on ruby-minitest-global-expectations + * Add runtime dependency on ruby-minitest-global-expectations + * Fix package wrt cme + * Use AUTOPKGTEST_TMP in tests as ADTTMP is deprecated + * Add myself as an uploader + * Add Rules-Requires-Root: no + * Add Breaks for ruby-rack-oauth2 + + -- Utkarsh Gupta <utkarsh@debian.org> Fri, 10 Apr 2020 03:43:38 +0530 + +ruby-rack (2.1.1-1) experimental; urgency=medium + + * Team upload + * New upstream version 2.1.1 + * Bump Standards-Version to 4.4.1 (no changes needed) + * Switch test to minitest (but disable tests because build deps not packaged) + * Switch to github tarballs for tests + * Upload to experimental because autopkgtest for berkshelf-api coquelicot + nanoc rails redmine ruby-acts-as-api ruby-faye ruby-grape ruby-moneta + ruby-omniauth ruby-rack-attack ruby-rack-oauth2 ruby-rack-openid + ruby-voight-kampff failed and rebuilds of berkshelf-api coquelicot nanoc + redmine ruby-grape ruby-omniauth ruby-rack-oauth2 ruby-warden failed + + -- Pirate Praveen <praveen@debian.org> Sun, 12 Jan 2020 20:00:24 +0530 + +ruby-rack (2.0.7-2) unstable; urgency=medium + + * Team upload + * Re-upload to unstable + * Add salsa-ci.yml + * Bump Standards-Version to 4.4.0 + * Bump debhelper-compat to 12 + + -- Utkarsh Gupta <guptautkarsh2102@gmail.com> Tue, 03 Sep 2019 00:22:18 +0530 + +ruby-rack (2.0.7-1) experimental; urgency=medium + + * Team upload + * New upstream version 2.0.7 + + -- Utkarsh Gupta <guptautkarsh2102@gmail.com> Wed, 15 May 2019 21:13:44 +0530 + ruby-rack (2.0.6-3) unstable; urgency=medium * Team upload. @@ -9,7 +120,7 @@ ruby-rack (2.0.6-3) unstable; urgency=medium ruby-rack (2.0.6-2) unstable; urgency=medium * Team upload - * Re-upload to unstable + * Re-upload to unstable -- Sruthi Chandran <srud@disroot.org> Thu, 03 Jan 2019 21:42:53 +0530 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b4de3947675361a7770d29b8982c407b0ec6b2a0..0000000000000000000000000000000000000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 diff --git a/debian/control b/debian/control index 6793b96eb41566e7833a9a37840be2bcd4040954..2c295eb26a7851e57cb50fd4d50340651c895c0e 100644 --- a/debian/control +++ b/debian/control @@ -1,34 +1,35 @@ Source: ruby-rack +Maintainer: Debian Ruby Team <pkg-ruby-extras-maintainers@lists.alioth.debian.org> +Uploaders: Chris Lamb <lamby@debian.org>, + Lucas Nussbaum <lucas@debian.org>, + Youhei SASAKI <uwabami@gfd-dennou.org>, + Paul van Tilburg <paulvt@debian.org>, + Utkarsh Gupta <utkarsh@debian.org> Section: ruby Priority: optional -Maintainer: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers@lists.alioth.debian.org> -Uploaders: - Chris Lamb <lamby@debian.org>, - Lucas Nussbaum <lucas@debian.org>, - Youhei SASAKI <uwabami@gfd-dennou.org>, - Paul van Tilburg <paulvt@debian.org>, -Build-Depends: - debhelper (>= 11~), - gem2deb, - rake, - ruby-bacon, - ruby-concurrent (>= 1.0.3~), - ruby-dalli, - thin, -Standards-Version: 4.3.0 -Vcs-Git: https://salsa.debian.org/ruby-team/ruby-rack.git +Build-Depends: debhelper-compat (= 12), + gem2deb, + rake, + ruby-bacon, + ruby-concurrent (>= 1.0.3~), + ruby-dalli, + ruby-minitest-global-expectations, + thin +Standards-Version: 4.5.1 Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-rack +Vcs-Git: https://salsa.debian.org/ruby-team/ruby-rack.git Homepage: https://rack.github.io/ XS-Ruby-Versions: all +Rules-Requires-Root: no Package: ruby-rack Architecture: all XB-Ruby-Versions: ${ruby:Versions} -Depends: - ruby | ruby-interpreter, - ${misc:Depends}, - ${shlibs:Depends}, -Breaks: ruby-sinatra (<< 2) +Depends: ruby:any | ruby-interpreter, + ${misc:Depends}, + ${shlibs:Depends} +Breaks: ruby-sinatra (<< 2), + ruby-rack-oauth2 (<< 1.11) Description: 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 diff --git a/debian/patches/series b/debian/patches/series index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0144d346f188e4d26bc242fb2f2cd2abb56d37cf 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -0,0 +1 @@ +skip-random-failure.patch diff --git a/debian/patches/skip-random-failure.patch b/debian/patches/skip-random-failure.patch new file mode 100644 index 0000000000000000000000000000000000000000..4abb52fa409646ac30fb18f63590c6690737da08 --- /dev/null +++ b/debian/patches/skip-random-failure.patch @@ -0,0 +1,15 @@ +Description: Skip random failure. +Author: Utkarsh Gupta <utkarsh@debian.org> +Forwarded: not-needed +Last-Update: 2020-04-09 + +--- a/test/spec_builder.rb ++++ b/test/spec_builder.rb +@@ -253,6 +253,7 @@ + end + + it "strips leading unicode byte order mark when present" do ++ skip + app, _ = Rack::Builder.parse_file config_file('bom.ru') + Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK' + end diff --git a/debian/ruby-tests.rake b/debian/ruby-tests.rake index 350d87c73cfbeac8c9763a5f9102a9c0dc4eb273..c747f15246ad18ad36b4f33f5a57539fcd6f45cc 100644 --- a/debian/ruby-tests.rake +++ b/debian/ruby-tests.rake @@ -1,7 +1,13 @@ # -*- mode: ruby; coding: utf-8 -*- -require 'rbconfig' unless defined? RbConfig -ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) -task :default do - sh "#{ruby} /usr/bin/bacon -I./test -w -a" -end +require "rake/testtask" + +desc "Run all the tests" +task default: :test + +Rake::TestTask.new("test") 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 \ No newline at end of file diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..33c3a640d2a84306b6a8b5640692ac3481739e65 --- /dev/null +++ b/debian/salsa-ci.yml @@ -0,0 +1,4 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml diff --git a/debian/tests/control b/debian/tests/control index 09f541ea6ef01fe005531c639b1fb06f3c62af7b..3276f56d2d7a6ec77f0151a581ac651cfd7d5f0b 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -2,5 +2,5 @@ Tests: smoke-test Depends: @, curl Test-Command: gem2deb-test-runner -Depends: @, gem2deb-test-runner, rake, ruby-bacon, thin +Depends: @, gem2deb-test-runner, rake, ruby-bacon, thin, ruby-minitest-global-expectations Restrictions: allow-stderr diff --git a/debian/tests/smoke-test b/debian/tests/smoke-test index 8b5af5a33e6e3ba10c003329c5b9487d92c192bd..ad25a547d950fbcfa45b2fe143d4112498f23a0b 100755 --- a/debian/tests/smoke-test +++ b/debian/tests/smoke-test @@ -4,7 +4,7 @@ exec 2>&1 set -ex -appdir=${ADTTMP:-/tmp}/app +appdir=${AUTOPKGTEST_TMP:-/tmp}/app mkdir $appdir cd $appdir diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 0000000000000000000000000000000000000000..62aed4764da483bd81b7a2110ca287a97599d2d4 --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,4 @@ +Bug-Database: https://github.com/rack/rack/issues +Bug-Submit: https://github.com/rack/rack/issues/new +Repository: https://github.com/rack/rack.git +Repository-Browse: https://github.com/rack/rack diff --git a/debian/watch b/debian/watch index 18653c12621a73155633354d746a06e558f188ad..e5592986da8bb2cf79affaebdbe68bed9e58e3a7 100644 --- a/debian/watch +++ b/debian/watch @@ -1,2 +1,2 @@ version=3 -https://gemwatch.debian.net/rack .*/rack-(.*).tar.gz + https://github.com/rack/rack/tags .*/v?(\d\S*)\.tar\.gz \ No newline at end of file diff --git a/example/lobster.ru b/example/lobster.ru index cc7ffcae88901574b81c932af1622ef1f79b255c..901e18a53098a1e84785bd57b8e629187703b555 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 26b23661f8fc72282826978632c53006f87b25e4..fe4f0b0948756c7ced3774be889fe1942d49ed68 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 1ba48702dc2fe982fce0749bf765b9425e22274e..0eb243cc6d0a8ea4f8e890a45a7d79eb580b9229 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 edf80dd9fe1e1fc141f91dac334f4122d7672e42..634235ae318eecd1ba05f03e29b1cdb7378c3df4 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.6" + RELEASE = "2.1.4" # 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" @@ -97,6 +99,7 @@ module Rack autoload :ContentType, "rack/content_type" autoload :ETag, "rack/etag" autoload :File, "rack/file" + autoload :Files, "rack/files" autoload :Deflater, "rack/deflater" autoload :Directory, "rack/directory" autoload :ForwardRequest, "rack/recursive" diff --git a/lib/rack/auth/abstract/handler.rb b/lib/rack/auth/abstract/handler.rb index c657691e1c14654da711cc038b33a55e11f2e3cf..3ed87091c7e7a33f8bcaa76e523c254a66e4f9fe 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 b738cc98a07a66ab37f78abd5886ed577daccda5..23da4bf2717c0056fba03b10a9b7496f2678d507 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 diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb index 9c5892141cd48d0298cad561fbd12d2540335911..d334939ca2b8a75fe438590a31286bcf730e8065 100644 --- a/lib/rack/auth/basic.rb +++ b/lib/rack/auth/basic.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + require 'rack/auth/abstract/handler' require 'rack/auth/abstract/request' +require 'base64' module Rack module Auth @@ -45,7 +48,7 @@ module Rack end def credentials - @credentials ||= params.unpack("m*").first.split(/:/, 2) + @credentials ||= Base64.decode64(params).split(':', 2) end def username diff --git a/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb index ddee35def328f25f9fc2b1a3e0b7cda3a4420a68..62bff9846ae6babbbb34a40c215f2459fafe21f1 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 57089cb30466713806244e093e3cc5ed5ee3fd9a..3216d973e0daddfe0493588fed27d35c4f46a665 100644 --- a/lib/rack/auth/digest/nonce.rb +++ b/lib/rack/auth/digest/nonce.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + require 'digest/md5' +require 'base64' module Rack module Auth @@ -18,7 +21,7 @@ module Rack end def self.parse(string) - new(*string.unpack("m*").first.split(' ', 2)) + new(*Base64.decode64(string).split(' ', 2)) end def initialize(timestamp = Time.now, given_digest = nil) @@ -26,11 +29,11 @@ module Rack end def to_s - [([ @timestamp, digest ] * ' ')].pack("m*").strip + Base64.encode64("#{@timestamp} #{digest}").strip end def digest - ::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':') + ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}") end def valid? diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb index 2b226e62832129296bcb30a4640270c2bb47daa4..f611b3c35eb4dee2a05a1cfae8c7ceb66e5feb0d 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 105c76747f92f4d333ee46ec58a929014c2060ad..a3ab47439b74d8ac0ad1ed9fcbf94e1783b897a1 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 7fcfe31671a2fafc76f0d096f2a14a4eb936702d..cb161980ed9420a90fdade7359f64224d8a5fa73 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,11 +8,7 @@ module Rack @closed = false end - def respond_to?(method_name, include_all=false) - case method_name - when :to_ary, 'to_ary' - return false - end + def respond_to?(method_name, include_all = false) super or @body.respond_to?(method_name, include_all) end @@ -37,7 +35,6 @@ module Rack end def method_missing(method_name, *args, &block) - super if :to_ary == method_name @body.__send__(method_name, *args, &block) end end diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index 975cf1e19ce821e5dd1fd1ae5b9246881fbccdf8..ebfa1f1184d3afe924248065f94d0d0b21d9738e 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,10 +97,11 @@ 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 + ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true) # Takes an argument that is an object that responds to #call and returns a Rack response. # The simplest form of this is a lambda object: @@ -113,7 +130,7 @@ module Rack # # use SomeMiddleware # run MyApp - def warmup(prc=nil, &block) + def warmup(prc = nil, &block) @warmup = prc || block end @@ -141,10 +158,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 +180,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 6b8f415aebf4b097c0f9e09a7b6c9ee9f7d3c864..1ed7ffa980330382a0c98e36464c845391d56169 100644 --- a/lib/rack/cascade.rb +++ b/lib/rack/cascade.rb @@ -1,15 +1,17 @@ +# 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]) - @apps = []; @has_app = {} + def initialize(apps, catch = [404, 405]) + @apps = [] apps.each { |app| add app } @catch = {} @@ -39,12 +41,11 @@ module Rack end def add(app) - @has_app[app] = true @apps << app end def include?(app) - @has_app.include? app + @apps.include?(app) end alias_method :<<, :add diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb index 3076931c4028a6cf3c1965ad4fff26253f7f72d2..e7e7d8d1db1d2f0ca717b685c199532c4d2a5712 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,7 +20,7 @@ module Rack @body = body end - def each + def each(&block) term = TERM @body.each do |chunk| size = chunk.bytesize @@ -28,11 +30,28 @@ module Rack 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) @@ -43,7 +62,7 @@ module Rack # a version (nor response headers) def chunkable_version?(ver) case ver - when "HTTP/1.0", nil, "HTTP/0.9" + when 'HTTP/1.0', nil, 'HTTP/0.9' false else true @@ -54,15 +73,19 @@ module Rack status, headers, body = @app.call(env) headers = HeaderHash.new(headers) - if ! chunkable_version?(env[HTTP_VERSION]) || - STATUS_WITH_NO_ENTITY_BODY.include?(status) || + if ! chunkable_version?(env[SERVER_PROTOCOL]) || + 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 ae410430e8b24b0f45174bfb349fecf4660af8ca..a513ff6ea5eebf9e8504380d552a9ec8722aacb5 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], + env[SERVER_PROTOCOL], 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 441dd38238355d7a37ca2004c468193456924308..bda8daf6dcf0910d0070dc78d958d366e202e307 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 dc255d27e6467acb31a8c5586a352511878506c4..41f6f7dd57314bea3092932a04c104570cded304 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 2df7dfc8125657f3d30ce70d581bcc5f1e5e6614..e37fc3058fef23fa1bfe58a378b27f91017b9f87 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 78ba43b7155e10565dfde453c893efc7477c6de6..010cc37b706c5bf4d2a8455be2c846f443317bd1 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 0000000000000000000000000000000000000000..a32fcdf629a3fff24c5d680ed26ae23ac2ae29d2 --- /dev/null +++ b/lib/rack/core_ext/regexp.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Regexp has `match?` since Ruby 2.4 +# so to support Ruby < 2.4 we need to define this method + +module Rack + module RegexpExtensions + refine Regexp do + def match?(string, pos = 0) + !!match(string, pos) + end + end unless //.respond_to?(:match?) + end +end diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb index 46d5b20af45e22e82bdb825995bcfc6a78bd8df7..9a30c017542f0442651f20eb2268d0d989ca72d6 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -1,7 +1,11 @@ +# 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. # @@ -15,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.map(&:bytesize).reduce(0, :+) > 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 flushed 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) @@ -52,33 +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)] + 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 @@ -90,9 +102,8 @@ module Rack end def close - return if @closed - @closed = true @body.close if @body.respond_to?(:close) + @body = nil end end @@ -101,18 +112,22 @@ 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) + # No point in compressing empty body, also handles usage with + # Rack::Sendfile. + return false if headers[CONTENT_LENGTH] == '0' + true end end diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index 89cfe807a202578f929c5d5fbc0a55611fbb3bc7..4c1f4dd61fa2903a53da305b76dc48938a179a82 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + require 'time' require 'rack/utils' require 'rack/mime' +require 'rack/files' module Rack # Rack::Directory serves entries below the +root+ given, according to the @@ -8,7 +11,7 @@ module Rack # will be presented in an html based index. If a file is found, the env will # be passed to the specified +app+. # - # If +app+ is not specified, a Rack::File of the same +root+ will be used. + # If +app+ is not specified, a Rack::Files of the same +root+ will be used. class Directory DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>" @@ -41,9 +44,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,9 +59,9 @@ 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) + @app = app || Rack::Files.new(@root) @head = Rack::Head.new(lambda { |env| get env }) end @@ -86,9 +89,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,20 +99,20 @@ 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','','','']] - glob = ::File.join(path, '*') + files = [['../', 'Parent Directory', '', '', '']] url_head = (script_name.split('/') + path_info.split('/')).map do |part| Rack::Utils.escape_path part end - Dir[glob].sort.each do |node| + Dir.entries(path).reject { |e| e.start_with?('.') }.sort.each do |node| + node = ::File.join path, node stat = stat(node) next unless stat basename = ::File.basename(node) @@ -126,7 +129,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 +157,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 5a8c6452a7dbc0858651abe9ddb7b4b97dbd8e3a..fd3de554a3ad35e7d865a7d8f2fdcaeb7f4922ec 100644 --- a/lib/rack/etag.rb +++ b/lib/rack/etag.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack' require 'digest/sha2' @@ -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 3782a22ebb6b21658e75d3f56c0a67f8388f2bd1..77b716754f227c7676271ff3a003235c64cdbe8a 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 09eb0afb84ad48b7d6018771f5c1dacbbca1d953..52b48e8bac52c8fe786029a5a45c397d1534ad84 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -1,176 +1,7 @@ -require 'time' -require 'rack/utils' -require 'rack/mime' -require 'rack/request' -require 'rack/head' +# frozen_string_literal: true -module Rack - # Rack::File serves files below the +root+ directory given, according to the - # path info of the Rack request. - # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file - # as http://localhost:9292/passwd - # - # Handlers can detect if bodies are a Rack::File, and use mechanisms - # like sendfile on the +path+. - - class File - ALLOWED_VERBS = %w[GET HEAD OPTIONS] - ALLOW_HEADER = ALLOWED_VERBS.join(', ') - - attr_reader :root - - def initialize(root, headers={}, default_mime = 'text/plain') - @root = root - @headers = headers - @default_mime = default_mime - @head = Rack::Head.new(lambda { |env| get env }) - end - - def call(env) - # HEAD requests drop the response body, including 4xx error messages. - @head.call env - end - - def get(env) - request = Rack::Request.new env - unless ALLOWED_VERBS.include? request.request_method - return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER}) - end - - path_info = Utils.unescape_path request.path_info - return fail(400, "Bad Request") unless Utils.valid_path?(path_info) - - clean_path_info = Utils.clean_path_info(path_info) - path = ::File.join(@root, clean_path_info) - - available = begin - ::File.file?(path) && ::File.readable?(path) - rescue SystemCallError - false - end - - if available - serving(request, path) - else - fail(404, "File not found: #{path_info}") - end - end - - def serving(request, path) - if request.options? - return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []] - end - last_modified = ::File.mtime(path).httpdate - return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified - - headers = { "Last-Modified" => last_modified } - mime_type = mime_type path, @default_mime - headers[CONTENT_TYPE] = mime_type if mime_type - - # Set custom headers - @headers.each { |field, content| headers[field] = content } if @headers - - response = [ 200, headers ] - - size = filesize path - - range = nil - ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size) - if ranges.nil? || ranges.length > 1 - # No ranges, or multiple ranges (which we don't support): - # TODO: Support multiple byte-ranges - response[0] = 200 - range = 0..size-1 - elsif ranges.empty? - # Unsatisfiable. Return error, and file size: - response = fail(416, "Byte range unsatisfiable") - response[1]["Content-Range"] = "bytes */#{size}" - return response - else - # Partial content: - range = ranges[0] - response[0] = 206 - response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" - size = range.end - range.begin + 1 - end +require 'rack/files' - response[2] = [response_body] unless response_body.nil? - - response[1][CONTENT_LENGTH] = size.to_s - response[2] = make_body request, path, range - response - end - - class Iterator - attr_reader :path, :range - alias :to_path :path - - def initialize path, range - @path = path - @range = range - end - - def each - ::File.open(path, "rb") do |file| - file.seek(range.begin) - remaining_len = range.end-range.begin+1 - while remaining_len > 0 - part = file.read([8192, remaining_len].min) - break unless part - remaining_len -= part.length - - yield part - end - end - end - - def close; end - end - - private - - def make_body request, path, range - if request.head? - [] - else - Iterator.new path, range - end - end - - def fail(status, body, headers = {}) - body += "\n" - - [ - status, - { - CONTENT_TYPE => "text/plain", - CONTENT_LENGTH => body.size.to_s, - "X-Cascade" => "pass" - }.merge!(headers), - [body] - ] - end - - # The MIME type for the contents of the file located at @path - def mime_type path, default_mime - Mime.mime_type(::File.extname(path), default_mime) - end - - def filesize path - # If response_body is present, use its size. - return response_body.bytesize if response_body - - # We check via File::size? whether this file provides size info - # via stat (e.g. /proc files often don't), otherwise we have to - # figure it out by reading the whole file into memory. - ::File.size?(path) || ::File.read(path).bytesize - end - - # By default, the response body for file requests is nil. - # In this case, the response body will be generated later - # from the file at @path - def response_body - nil - end - end +module Rack + File = Files end diff --git a/lib/rack/files.rb b/lib/rack/files.rb new file mode 100644 index 0000000000000000000000000000000000000000..f1a91c8bcef23a1538b3d030cb38291a5d55206f --- /dev/null +++ b/lib/rack/files.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +require 'time' +require 'rack/utils' +require 'rack/mime' +require 'rack/request' +require 'rack/head' + +module Rack + # Rack::Files serves files below the +root+ directory given, according to the + # path info of the Rack request. + # e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file + # as http://localhost:9292/passwd + # + # Handlers can detect if bodies are a Rack::Files, and use mechanisms + # like sendfile on the +path+. + + class Files + ALLOWED_VERBS = %w[GET HEAD OPTIONS] + ALLOW_HEADER = ALLOWED_VERBS.join(', ') + + attr_reader :root + + def initialize(root, headers = {}, default_mime = 'text/plain') + @root = (::File.expand_path(root) if root) + @headers = headers + @default_mime = default_mime + @head = Rack::Head.new(lambda { |env| get env }) + end + + def call(env) + # HEAD requests drop the response body, including 4xx error messages. + @head.call env + end + + def get(env) + request = Rack::Request.new env + unless ALLOWED_VERBS.include? request.request_method + return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER }) + end + + path_info = Utils.unescape_path request.path_info + return fail(400, "Bad Request") unless Utils.valid_path?(path_info) + + clean_path_info = Utils.clean_path_info(path_info) + path = ::File.join(@root, clean_path_info) + + available = begin + ::File.file?(path) && ::File.readable?(path) + rescue SystemCallError + false + end + + if available + serving(request, path) + else + fail(404, "File not found: #{path_info}") + end + end + + def serving(request, path) + if request.options? + return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []] + end + last_modified = ::File.mtime(path).httpdate + return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified + + headers = { "Last-Modified" => last_modified } + mime_type = mime_type path, @default_mime + headers[CONTENT_TYPE] = mime_type if mime_type + + # Set custom headers + @headers.each { |field, content| headers[field] = content } if @headers + + response = [ 200, headers ] + + size = filesize path + + range = nil + ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size) + if ranges.nil? || ranges.length > 1 + # No ranges, or multiple ranges (which we don't support): + # TODO: Support multiple byte-ranges + response[0] = 200 + range = 0..size - 1 + elsif ranges.empty? + # Unsatisfiable. Return error, and file size: + response = fail(416, "Byte range unsatisfiable") + response[1]["Content-Range"] = "bytes */#{size}" + return response + else + # Partial content: + range = ranges[0] + response[0] = 206 + response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" + size = range.end - range.begin + 1 + end + + response[2] = [response_body] unless response_body.nil? + + response[1][CONTENT_LENGTH] = size.to_s + response[2] = make_body request, path, range + response + end + + class Iterator + attr_reader :path, :range + alias :to_path :path + + def initialize path, range + @path = path + @range = range + end + + def each + ::File.open(path, "rb") do |file| + file.seek(range.begin) + remaining_len = range.end - range.begin + 1 + while remaining_len > 0 + part = file.read([8192, remaining_len].min) + break unless part + remaining_len -= part.length + + yield part + end + end + end + + def close; end + end + + private + + def make_body request, path, range + if request.head? + [] + else + Iterator.new path, range + end + end + + def fail(status, body, headers = {}) + body += "\n" + + [ + status, + { + CONTENT_TYPE => "text/plain", + CONTENT_LENGTH => body.size.to_s, + "X-Cascade" => "pass" + }.merge!(headers), + [body] + ] + end + + # The MIME type for the contents of the file located at @path + def mime_type path, default_mime + Mime.mime_type(::File.extname(path), default_mime) + end + + def filesize path + # If response_body is present, use its size. + return response_body.bytesize if response_body + + # We check via File::size? whether this file provides size info + # via stat (e.g. /proc files often don't), otherwise we have to + # figure it out by reading the whole file into memory. + ::File.size?(path) || ::File.read(path).bytesize + end + + # By default, the response body for file requests is nil. + # In this case, the response body will be generated later + # from the file at @path + def response_body + nil + end + end +end diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb index 70a77fa97fec0504731d1951d18e36a5fa6b139d..df17b238ddb73d599fce01a934149b539e4e695f 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. # @@ -17,7 +19,7 @@ module Rack end if klass = @handlers[server] - klass.split("::").inject(Object) { |o, x| o.const_get(x) } + const_get(klass) else const_get(server, false) end @@ -43,6 +45,9 @@ module Rack raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}." end + SERVER_NAMES = %w(puma thin falcon webrick).freeze + private_constant :SERVER_NAMES + def self.default # Guess. if ENV.include?("PHP_FCGI_CHILDREN") @@ -52,7 +57,7 @@ module Rack elsif ENV.include?("RACK_HANDLER") self.get(ENV["RACK_HANDLER"]) else - pick ['puma', 'thin', 'webrick'] + pick SERVER_NAMES end end diff --git a/lib/rack/handler/cgi.rb b/lib/rack/handler/cgi.rb index 528076946daaf89fd5dabc58a7f99a06a4c3c875..a223c5453e98138d0e9d5084cc647d49bca14889 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 e918dc94bc0c78ff11d9b892e1f60ed8486c5232..b3f825dac990b1e2a9dbf0a14547cfc0cb19514b 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 d2cfd793521a73bca3c400d3adc1d8bae6b262b0..803182a2dd310359bdce654e997ce79435234a78 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 e056a01d8bcbd7d416cbec18581f13ad5b8b3c92..c8e916061bbdac7a3a5b082757470b5e2306baf6 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 ca8806463ee0cdfd7a996a5693c72ccdd0c632ef..100dfd11947bb326035ff6073dedb8c38f06df4b 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 d0fcd21362176f932a8dacdf0d9652dcc9a0053a..4affdbde66c6021a5b9acfc13d4990bfeea5a86e 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,7 +81,7 @@ 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 diff --git a/lib/rack/head.rb b/lib/rack/head.rb index 6f1d7472817f7c3cd6cdd0bb73cd5277a8a67ed3..c257ae4d5d72120f088295275c0df37aee93dab4 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 683ba68411db0c332173455046fda0d9f91fe083..98ba9b44111939d1160bc40bd36c4e72c30510b0 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 } } @@ -626,15 +627,17 @@ module Rack assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") { header.respond_to? :each } - header.each { |key, value| - ## Special headers starting "rack." are for communicating with the - ## server, and must not be sent back to the client. - next if key =~ /^rack\..+$/ + header.each { |key, value| ## The header keys must be Strings. assert("header key must be a string, was #{key.class}") { key.kind_of? String } + + ## Special headers starting "rack." are for communicating with the + ## server, and must not be sent back to the client. + next if key =~ /^rack\..+$/ + ## The header must not contain a +Status+ key. assert("header must not contain Status") { key.downcase != "status" } ## The header must conform to RFC7230 token specification, i.e. cannot @@ -662,7 +665,7 @@ module Rack ## 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 @@ -676,7 +679,7 @@ module Rack ## There must not be a <tt>Content-Length</tt> header when the ## +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 4d6e39f2bb567744a66de946892336c69b7682c9..77b607c3180d3cbde0c007e1d08fdf2198b10971 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 b5a41e8e1ad0b8c3ce4e854f94504dd4e45e58b5..96366cd306f7857cd8b1df0f81879638353ad763 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' diff --git a/lib/rack/logger.rb b/lib/rack/logger.rb index 01fc321c7212ffab8c454d0eb1105a1c457a069f..6c4bede0cf37eedf036dfd8290f0df43c2b10c13 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 7e6cd3a85ae9bc7c8ba360f5d42babf21f6770dc..41937c9947e63aee85a2e4d8b48860bcce617ee0 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,15 +25,18 @@ module Rack # { 'charset' => 'utf-8' } def params(content_type) return {} if content_type.nil? - Hash[*content_type.split(SPLIT_PATTERN)[1..-1]. - collect { |s| s.split('=', 2) }. - map { |k,v| [k.downcase, strip_doublequotes(v)] }.flatten] + + content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh| + k, v = s.split('=', 2) + + hsh[k.tap(&:downcase!)] = strip_doublequotes(v) + end end private def strip_doublequotes(str) - (str[0] == ?" && str[-1] == ?") ? str[1..-2] : str + (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str end end end diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb index 14debe92b06fb544ac19b2f7e4ff99e91cfb3a7a..453901fc60692f43ac042e1f50e411d77b9df60d 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) diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb index d82dc1319c937817ef72c33f738a4c3a31d13a6f..f6c02c1fd6ad871eb09e4bf3783a37c8dc6b91d0 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 10070f11380c09ab08ce34b07e676f1d248f2db4..3feaedd91882e8828d41061914f59b7a453519c7 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,7 +88,7 @@ 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] == ?/ @@ -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,16 +158,19 @@ 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) + + buffered_body! end def =~(other) @@ -186,11 +192,64 @@ module Rack # ... # res.body.should == "foo!" # end - super.join + buffer = String.new + + super.each do |chunk| + buffer << chunk + end + + return buffer end def empty? [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 db59ee59ad7b8a27810e810674d9dfa09da880b5..bd91f43f4fcaeacf8c6383c376f4421e278dbdfb 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 @@ -14,10 +16,10 @@ module Rack TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/ - BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i - BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i + BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i + BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni - MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name=(#{VALUE})/ni + MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni # Updated definitions from RFC 2231 ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]} diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb index f0b70a8d63107ae345a2adbc4eef9de57d28c50c..9ed2bb07cd4f35946d31937ada9ec67f93bf434c 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 a19d6ea32f38d29ea3e7fa50978afd96a7390542..3e30a6b4ac250ab59d13c73c4584720d2096911b 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.gsub("\0".freeze, '%00'.freeze))]) + 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 @@ -140,6 +147,7 @@ module Rack 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 = String.new - @query_parser = query_parser @params = query_parser.make_params @boundary = "--#{boundary}" @bufsize = bufsize - @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n - @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max @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,16 +268,19 @@ module Rack end def handle_mime_body - if i = @buf.index(rx) - # Save the rest. - @collector.on_mime_body @mime_index, @buf.slice!(0, i) - @buf.slice!(0, 2) # Remove \r\n after the content + if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet + body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string + @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 the read body part. - if @rx_max_size < @buf.size - @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size) + # 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 @@ -280,16 +288,13 @@ module Rack 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 @@ -310,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! @@ -327,7 +332,7 @@ module Rack filename end - CHARSET = "charset" + CHARSET = "charset" def tag_multipart_encoding(filename, content_type, name, body) name = name.to_s @@ -344,10 +349,10 @@ 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[0] == '"' && v[-1] == '"' + v = v[1..-2] if v.start_with?('"') && v.end_with?('"') encoding = Encoding.find v if k == CHARSET end end @@ -357,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 924b1f089b26a6019348b0c095cf5e500f318293..d01f2d6f32fc414ac6af533f30723d8a0634bb3d 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 abc61206298e3fc34cf2cd33e1440b761ed52fe6..3eff73d683fd336dca8f436b9f3cb7a4a6eee5cd 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 be74bc069cca3b36ea235d2d4cb73b9c27ceed50..2a4eb24496d71c5b0e29c3614158826e1368c3df 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 @@ -49,7 +55,7 @@ module Rack end end - return params.to_params_hash + return params.to_h end # parse_nested_query expands a query string into structural types. Supported @@ -61,13 +67,13 @@ 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 - return params.to_params_hash + return params.to_h rescue ArgumentError => e raise InvalidParameterError, e.message 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 @@ -135,7 +141,7 @@ module Rack end def params_hash_has_key?(hash, key) - return false if key =~ /\[\]/ + return false if /\[\]/.match?(key) key.split(/[\[\]]+/).inject(hash) do |h, part| next h if part == '' @@ -171,22 +177,42 @@ module Rack @params.key?(key) end - def to_params_hash - hash = @params - hash.keys.each do |key| - value = hash[key] - if value.kind_of?(self.class) - if value.object_id == self.object_id - hash[key] = hash - else - hash[key] = value.to_params_hash - end - elsif value.kind_of?(Array) - value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x} + # Recursively unwraps nested `Params` objects and constructs an object + # of the same shape, but using the objects' internal representations + # (Ruby hashes) in place of the objects. The result is a hash consisting + # purely of Ruby primitives. + # + # Mutation warning! + # + # 1. This method mutates the internal representation of the `Params` + # objects in order to save object allocations. + # + # 2. The value you get back is a reference to the internal hash + # representation, not a copy. + # + # 3. Because the `Params` object's internal representation is mutable + # through the `#[]=` method, it is not thread safe. The result of + # getting the hash representation while another thread is adding a + # key to it is non-deterministic. + # + def to_h + @params.each do |key, value| + case value + when self + # Handle circular references gracefully. + @params[key] = @params + when Params + @params[key] = value.to_h + when Array + value.map! { |v| v.kind_of?(Params) ? v.to_h : v } + else + # Ignore anything that is not a `Params` object or + # a collection that can contain one. end end - hash + @params end + alias_method :to_params_hash, :to_h end end end diff --git a/lib/rack/recursive.rb b/lib/rack/recursive.rb index 7645d284a8a64a4ae23e2c1923d3992c7c92ac51..6a94ca83d39bcfaf1bcdb39cdf0d4431a76ff763 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 296dd6a1fb351dda4371ce672262876846c0f257..e23ed1fbea7a31f1b28a2ede83fc5d9ea20942d8 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 6307b6142cf800a0f8e5e9cabe51ed8236cb4c91..54ea86c4f6878d051ce56a6bfdc809cbfd2c80ae 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,7 +15,18 @@ module Rack # req.params["data"] class Request - SCHEME_WHITELIST = %w(https http).freeze + 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 @@ -100,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', @@ -108,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', @@ -119,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 @@ -159,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 @@ -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 @@ -337,7 +358,7 @@ module Rack # Fix for Safari Ajax postings that always append \0 # form_vars.sub!(/\0\z/, '') # performance replacement: - form_vars.slice!(-1) if form_vars[-1] == ?\0 + form_vars.slice!(-1) if form_vars.end_with?("\0") set_header RACK_REQUEST_FORM_VARS, form_vars set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&') @@ -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,7 +439,7 @@ 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> @@ -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,21 +498,52 @@ 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 - scheme_headers = [ - get_header(HTTP_X_FORWARDED_SCHEME), - get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0] - ] + allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) || + allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))) + end - scheme_headers.each do |header| - return header if SCHEME_WHITELIST.include?(header) + 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 - nil + def extract_port(uri) + if (colon_index = uri.index(':')) + uri[colon_index + 1, uri.length] + end end end diff --git a/lib/rack/response.rb b/lib/rack/response.rb index a9f0c2a3617902fbe63c29e5400c852d0679b830..5c8dbab96f45de80e215afee5ef55d1bfb399e5f 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack/request' require 'rack/utils' require 'rack/body_proxy' @@ -23,32 +25,34 @@ module Rack attr_reader :header alias headers header - CHUNKED = 'chunked'.freeze + CHUNKED = 'chunked' + STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY - def initialize(body=[], status=200, header={}) + def initialize(body = nil, 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 - @length = 0 + @writer = self.method(:append) - @body = [] + @block = nil + @length = 0 - if body.respond_to? :to_str - write body.to_str - elsif body.respond_to?(:each) - body.each { |part| - write part.to_s - } + # Keep track of whether we have expanded the user supplied body. + if body.nil? + @body = [] + @buffered = true + elsif body.respond_to?(:to_str) + @body = [body] + @buffered = true else - raise TypeError, "stringable or iterable required" + @body = body + @buffered = false end - yield self if block_given? + yield self if block_given? end - def redirect(target, status=302) + def redirect(target, status = 302) self.status = status self.location = target end @@ -58,41 +62,45 @@ module Rack end def finish(&block) - @block = block - - if [204, 304].include?(status.to_i) + if STATUS_WITH_NO_ENTITY_BODY[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_given? + @block = block + [status.to_i, header, self] + else + [status.to_i, header, @body] + end end end + alias to_a finish # For *response - alias to_ary finish # For implicit-splat on Ruby 1.9.2 def each(&callback) @body.each(&callback) - @writer = callback - @block.call(self) if @block + @buffered = true + + if @block + @writer = callback + @block.call(self) + end end # Append to body and update Content-Length. # # NOTE: Do not mix #write and direct #body access! # - def write(str) - s = str.to_s - @length += s.bytesize unless chunked? - @writer.call s + def write(chunk) + buffered_body! - set_header(CONTENT_LENGTH, @length.to_s) unless chunked? - str + @writer.call(chunk.to_s) end def close - body.close if body.respond_to?(:close) + @body.close if @body.respond_to?(:close) end def empty? @@ -184,7 +192,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 @@ -211,6 +219,38 @@ module Rack def etag= v set_header ETAG, v end + + protected + + def buffered_body! + return if @buffered + + if @body.is_a?(Array) + # The user supplied body was an array: + @body = @body.compact + else + # Turn the user supplied body into a buffered array: + body = @body + @body = Array.new + + body.each do |part| + @writer.call(part.to_s) + end + end + + @buffered = true + end + + def append(chunk) + @body << chunk + + unless chunked? + @length += chunk.bytesize + set_header(CONTENT_LENGTH, @length.to_s) + end + + return chunk + end end include Helpers diff --git a/lib/rack/rewindable_input.rb b/lib/rack/rewindable_input.rb index dd6b78439d58e01d03a54ab51b329d2da9287808..352bbeaa30f429bc200c68106aa6d09a5c04b358 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' @@ -40,7 +42,7 @@ module Rack end # Closes this RewindableInput object without closing the originally - # wrapped IO oject. Cleans up any temporary resources that this RewindableInput + # wrapped IO object. Cleans up any temporary resources that this RewindableInput # has created. # # This method may be called multiple times. It does nothing on subsequent calls. @@ -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 bb15bdb1bc5f6c066c10aff41cd84795186fbd4b..d2bca9e5e4992eb0088769c685d9a12a5f63f4ac 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 bdb7cf2fb7960b769d3920a41f0b2cc0ea947e63..3774b26067a420cab846a35fe3bab18320a9f3db 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -1,4 +1,6 @@ -require 'rack/file' +# frozen_string_literal: true + +require 'rack/files' require 'rack/body_proxy' module Rack @@ -14,7 +16,7 @@ module Rack # # In order to take advantage of this middleware, the response body must # respond to +to_path+ and the request must include an X-Sendfile-Type - # header. Rack::File and other components implement +to_path+ so there's + # header. Rack::Files and other components implement +to_path+ so there's # rarely anything you need to do in your application. The X-Sendfile-Type # header is typically set in your web servers configuration. The following # sections attempt to document @@ -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 1f37aacb5eb65005f3b2b7a9faf36fdce7409d9a..6137f043b3c312f99d2e62e48ff33928e4190c18 100644 --- a/lib/rack/server.rb +++ b/lib/rack/server.rb @@ -1,10 +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) @@ -21,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 } @@ -47,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 +81,24 @@ module Rack options[:pid] = ::File.expand_path(f) } + 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:" @@ -114,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 @@ -152,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 @@ -182,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 = [] @@ -206,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 @@ -222,21 +254,19 @@ 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], logging_middleware, [Rack::TempfileReaper] ] m["development"] = [ [Rack::ContentLength], - [Rack::Chunked], logging_middleware, [Rack::ShowExceptions], [Rack::Lint], @@ -280,7 +310,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] @@ -321,6 +353,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 diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb index 1bb8d5d060a8bce6a33218f71d97de23b9b9984f..20ef8b833e93d40f94dfffc6f00e1a9cb8c9aeb1 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 @@ -6,15 +8,54 @@ require 'time' require 'rack/request' require 'rack/response' require 'securerandom' +require 'digest/sha2' module Rack module Session + class SessionId + ID_VERSION = 2 + + attr_reader :public_id + + def initialize(public_id) + @public_id = public_id + end + + def private_id + "#{ID_VERSION}::#{hash_sid(public_id)}" + end + + alias :cookie_value :public_id + alias :to_s :public_id + + def empty?; false; end + def inspect; public_id.inspect; end + + private + + def hash_sid(sid) + Digest::SHA256.hexdigest(sid) + end + end + module Abstract # 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 @@ -57,7 +98,7 @@ module Rack @data[key.to_s] end - def fetch(key, default=Unspecified, &block) + def fetch(key, default = Unspecified, &block) load_for_read! if default == Unspecified @data.fetch(key.to_s, &block) @@ -160,11 +201,7 @@ module Rack end def stringify_keys(other) - hash = {} - other.each do |key, value| - hash[key.to_s] = value - end - hash + other.to_hash.transform_keys(&:to_s) end end @@ -199,22 +236,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) @@ -226,7 +263,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) @@ -349,7 +386,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.") @@ -357,7 +394,7 @@ module Rack req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE else cookie = Hash.new - cookie[:value] = data + cookie[:value] = cookie_value(data) cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after] cookie[:expires] = Time.now + options[:max_age] if options[:max_age] set_cookie(req, res, cookie.merge!(options)) @@ -365,6 +402,10 @@ module Rack end public :commit_session + def cookie_value(data) + data + end + # Sets the cookie back to the client with session id. We skip the cookie # setting if the value didn't change (sid is the same) or expires was given. @@ -406,6 +447,40 @@ module Rack end end + class PersistedSecure < Persisted + class SecureSessionHash < SessionHash + def [](key) + if key == "session_id" + load_for_read! + id.public_id if id + else + super + end + end + end + + def generate_sid(*) + public_id = super + + SessionId.new(public_id) + end + + def extract_session_id(*) + public_id = super + public_id && SessionId.new(public_id) + end + + private + + def session_class + SecureSessionHash + end + + def cookie_value(data) + data.cookie_value + end + end + class ID < Persisted def self.inherited(klass) k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID } diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb index 71bb96f4f1150c046595db1105cfc141d65c098f..d110aee249ecec9d40e862e7a7e65185c7cd8899 100644 --- a/lib/rack/session/cookie.rb +++ b/lib/rack/session/cookie.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'openssl' require 'zlib' require 'rack/request' require 'rack/response' require 'rack/session/abstract/id' require 'json' +require 'base64' module Rack @@ -45,15 +48,15 @@ module Rack # }) # - class Cookie < Abstract::Persisted + class Cookie < Abstract::PersistedSecure # Encode session cookies as Base64 class Base64 def encode(str) - [str].pack('m') + ::Base64.strict_encode64(str) end def decode(str) - str.unpack('m').first + ::Base64.decode64(str) end # Encode session cookies as Marshaled Base64 data @@ -103,7 +106,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 +119,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 +140,7 @@ module Rack session_data = request.cookies[@key] if @secrets.size > 0 && session_data - digest, session_data = session_data.reverse.split("--", 2) - digest.reverse! if digest - session_data.reverse! if session_data + session_data, _, digest = session_data.rpartition('--') session_data = nil unless digest_match?(session_data, digest) end @@ -147,12 +148,21 @@ 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 end + class SessionId < DelegateClass(Session::SessionId) + attr_reader :cookie_value + + def initialize(session_id, cookie_value) + super(session_id) + @cookie_value = cookie_value + end + end + def write_session(req, session_id, session, options) session = session.merge("session_id" => session_id) session_data = coder.encode(session) @@ -165,7 +175,7 @@ module Rack req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.") nil else - session_data + SessionId.new(session_id, session_data) end end diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb index 4cf5ea09e35c4e9035d00dd1879f610657c9bc54..6a601174075b257a061b754b4b12748efbda32d5 100644 --- a/lib/rack/session/memcache.rb +++ b/lib/rack/session/memcache.rb @@ -1,93 +1,10 @@ -# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net +# frozen_string_literal: true -require 'rack/session/abstract/id' -require 'memcache' +require 'rack/session/dalli' module Rack module Session - # Rack::Session::Memcache provides simple cookie based session management. - # Session data is stored in memcached. The corresponding session key is - # maintained in the cookie. - # You may treat Session::Memcache as you would Session::Pool with the - # following caveats. - # - # * Setting :expire_after to 0 would note to the Memcache server to hang - # onto the session data until it would drop it according to it's own - # specifications. However, the cookie sent to the client would expire - # immediately. - # - # Note that memcache does drop data before it may be listed to expire. For - # a full description of behaviour, please see memcache's documentation. - - class Memcache < Abstract::ID - attr_reader :mutex, :pool - - DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \ - :namespace => 'rack:session', - :memcache_server => 'localhost:11211' - - def initialize(app, options={}) - super - - @mutex = Mutex.new - mserv = @default_options[:memcache_server] - 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?) - raise 'No memcache servers' - end - end - - def generate_sid - loop do - sid = super - break sid unless @pool.get(sid, true) - end - end - - def get_session(env, sid) - with_lock(env) do - unless sid and session = @pool.get(sid) - sid, session = generate_sid, {} - unless /^STORED/ =~ @pool.add(sid, session) - raise "Session collision on '#{sid.inspect}'" - end - end - [sid, session] - end - end - - def set_session(env, session_id, new_session, options) - expiry = options[:expire_after] - expiry = expiry.nil? ? 0 : expiry + 1 - - with_lock(env) do - @pool.set session_id, new_session, expiry - session_id - end - end - - def destroy_session(env, session_id, options) - with_lock(env) do - @pool.delete(session_id) - generate_sid unless options[:drop] - end - end - - def with_lock(env) - @mutex.lock if env[RACK_MULTITHREAD] - yield - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - if $VERBOSE - warn "#{self} is unable to find memcached server." - warn $!.inspect - end - raise - ensure - @mutex.unlock if @mutex.locked? - end - - end + warn "Rack::Session::Memcache is deprecated, please use Rack::Session::Dalli from 'dalli' gem instead." + Memcache = Dalli end end diff --git a/lib/rack/session/pool.rb b/lib/rack/session/pool.rb index 4c9c25c7a0e248218ac6e2305a1d8f4b6497c304..f5b6265046f92d3a84c29a6b63e01643c89121ac 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 @@ -24,11 +26,11 @@ module Rack # ) # Rack::Handler::WEBrick.run sessioned - class Pool < Abstract::Persisted + class Pool < Abstract::PersistedSecure 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 @@ -37,15 +39,15 @@ module Rack def generate_sid loop do sid = super - break sid unless @pool.key? sid + break sid unless @pool.key? sid.private_id end end def find_session(req, sid) with_lock(req) do - unless sid and session = @pool[sid] + unless sid and session = get_session_with_fallback(sid) sid, session = generate_sid, {} - @pool.store sid, session + @pool.store sid.private_id, session end [sid, session] end @@ -53,14 +55,15 @@ module Rack def write_session(req, session_id, new_session, options) with_lock(req) do - @pool.store session_id, new_session + @pool.store session_id.private_id, new_session session_id end end def delete_session(req, session_id, options) with_lock(req) do - @pool.delete(session_id) + @pool.delete(session_id.public_id) + @pool.delete(session_id.private_id) generate_sid unless options[:drop] end end @@ -71,6 +74,12 @@ module Rack ensure @mutex.unlock if @mutex.locked? end + + private + + def get_session_with_fallback(sid) + @pool[sid.private_id] || @pool[sid.public_id] + end end end end diff --git a/lib/rack/show_exceptions.rb b/lib/rack/show_exceptions.rb index ef30fce4325fb2fb521d554fda35de8ac692a3a3..843af607afb717eab3445f3740d7b65909a4ef2b 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' @@ -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 54db8f4715599ed10668fcf0ed82a82807f212cd..3fdfca5e6f3b2109387b68131eb865f00ab80e54 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/static.rb b/lib/rack/static.rb index 17f47649422c91cacead9fea634f2d8abad3cb29..9a0017db942d03ce1ebbc2fb7eb6b33c555e5b34 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -1,11 +1,15 @@ -require "rack/file" +# frozen_string_literal: true + +require "rack/files" require "rack/utils" +require_relative 'core_ext/regexp' + module Rack # The Rack::Static middleware intercepts requests for static files # (javascript files, images, stylesheets, etc) based on the url prefixes or - # route mappings passed in the options, and serves them using a Rack::File + # route mappings passed in the options, and serves them using a Rack::Files # object. This allows a Rack stack to serve both static and dynamic content. # # Examples: @@ -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) + @file_server = Rack::Files.new(root) end def add_index_root?(path) - @index && path =~ /\/$/ + @index && route_file(path) && path.end_with?('/') 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 && env['HTTP_ACCEPT_ENCODING'] && /\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 d829980619b2258a47b84589cd6bc5df012718da..73b6c1c8df2d2b76793b6dd24f0c13fdea6d9e36 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 510b4b5003e1d7758a624f499a99171b5c1ebbd0..c5d9c44f5392d7620e5166080d6c71dfb1b89892 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 c253f3cf24e4c94400114de897f4b337a62fc902..492f9bcfe245b52a25383bedd38dd351faec4a89 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 @@ -132,7 +138,7 @@ module Rack q_value_header.to_s.split(/\s*,\s*/).map do |part| value, parameters = part.split(/\s*;\s*/, 2) quality = 1.0 - if md = /\Aq=([\d.]+)/.match(parameters) + if parameters && (md = /\Aq=([\d.]+)/.match(parameters)) quality = md[1].to_f end [value, quality] @@ -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 @@ -210,8 +215,12 @@ module Rack # the Cookie header such that those with more specific Path attributes # 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 } + return {} unless header + header.split(/[;,] */n).each_with_object({}) do |cookie, cookies| + next if cookie.empty? + key, value = cookie.split('=', 2) + cookies[key] = (unescape(value) rescue value) unless cookies.key?(key) + end end module_function :parse_cookies_header @@ -221,41 +230,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 +282,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 +308,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 +351,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 +365,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 +386,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 +412,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 +442,7 @@ module Rack def to_hash hash = {} - each { |k,v| hash[k] = v } + each { |k, v| hash[k] = v } hash end @@ -510,13 +495,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', @@ -533,6 +519,7 @@ module Rack 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', + 306 => '(Unused)', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', @@ -557,6 +544,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 +559,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 << 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 +573,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 +594,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 d7f01d188f72bb780c6fc6dae05db955dbe7d5ee..f7b13b17074078a4b14c6f3aef7f316283866e97 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" @@ -15,20 +17,28 @@ middleware) into a single method call. 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 = 'Leah Neukirchen' s.email = 'leah@vuxu.org' s.homepage = 'https://rack.github.io/' s.required_ruby_version = '>= 2.2.2' + s.metadata = { + "bug_tracker_uri" => "https://github.com/rack/rack/issues", + "changelog_uri" => "https://github.com/rack/rack/blob/master/CHANGELOG.md", + "documentation_uri" => "https://rubydoc.info/github/rack/rack", + "homepage_uri" => "https://rack.github.io", + "mailing_list_uri" => "https://groups.google.com/forum/#!forum/rack-devel", + "source_code_uri" => "https://github.com/rack/rack" + } s.add_development_dependency 'minitest', "~> 5.0" s.add_development_dependency 'minitest-sprint' + s.add_development_dependency 'minitest-global_expectations' s.add_development_dependency 'rake' end diff --git a/test/.bacon b/test/.bacon new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/builder/an_underscore_app.rb b/test/builder/an_underscore_app.rb index 7ce1a0cc9a01a483155f992497bf13ed12121965..f58a2be509f0e422202dfd35fcbb0e120a730aa1 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 c07f82cda5307912156061a3209c1cc32f0efba8..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..5740f9a13826a4865da00ec40dcefd548d617caf --- /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 0722f0a0e958a50a7033fa43f9ff55a23980de93..894ba5d017927d96c19dcbeb82aa70711e82cce0 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 7f36d8cbbe18c1df1ef8bb29b707c2954843315b..dd8d45a9255e0c59bcdb45065cec4e2a6ceb282c 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 f4c84aded503f5271fa61004a96dc0513a4149a7..9ad88986087ce02ab6776dc9dc49d9e5d5323bbe 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 4af324404677d7d69eed5438603f205bfe165751..dca48fd9190c5dd55d895322d72858f6771446e7 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/lighttpd.conf b/test/cgi/lighttpd.conf deleted file mode 100755 index c195f78cdc15cdbcefd2bf744af429b08b5ba44a..0000000000000000000000000000000000000000 --- a/test/cgi/lighttpd.conf +++ /dev/null @@ -1,26 +0,0 @@ -server.modules = ("mod_fastcgi", "mod_cgi") -server.document-root = "." -server.errorlog = var.CWD + "/lighttpd.errors" -server.port = 9203 -server.bind = "127.0.0.1" - -server.event-handler = "select" - -cgi.assign = ("/test" => "", -# ".ru" => "" - ) - -fastcgi.server = ( - "test.fcgi" => ("localhost" => - ("min-procs" => 1, - "socket" => "/tmp/rack-test-fcgi", - "bin-path" => "test.fcgi")), - "test.ru" => ("localhost" => - ("min-procs" => 1, - "socket" => "/tmp/rack-test-ru-fcgi", - "bin-path" => CWD + "/rackup_stub.rb test.ru")), - "sample_rackup.ru" => ("localhost" => - ("min-procs" => 1, - "socket" => "/tmp/rack-test-rackup-fcgi", - "bin-path" => CWD + "/rackup_stub.rb sample_rackup.ru")), - ) diff --git a/test/cgi/rackup_stub.rb b/test/cgi/rackup_stub.rb index a216cdc393773c70055e2d0961fafd84e277864d..5f0e4365e6d74b4a3bb712f5541669920cefb618 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 a73df81c134bc51882925ad04d0e1b1b86d3486d..c8e94c9f1527d859870b095ec22ab7974d3fe42c 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 e4837a4ebbf8cd2b58b47b44c440f9dbed7b7f75..a1de2fbe39a84cb7800adc392ffcfbacbe2d0658 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 deleted file mode 100755 index 31f433996dbc70cc7cbcfaa8916a3682aef20488..0000000000000000000000000000000000000000 --- a/test/cgi/test.fcgi +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env ruby -# -*- ruby -*- - -require 'uri' -$:.unshift '../../lib' -require 'rack' -require '../testrequest' - -Rack::Handler::FastCGI.run(Rack::Lint.new(TestRequest.new)) diff --git a/test/cgi/test.gz b/test/cgi/test.gz old mode 100755 new mode 100644 index 312c60e2ab5a4b6dddb30f2adf6783d7a199eb89..a23c856c8e9f2b68ace458f61f8343e6c7f0d131 Binary files a/test/cgi/test.gz and b/test/cgi/test.gz differ diff --git a/test/cgi/test.ru b/test/cgi/test.ru index 7913ef781d068572a2a9b10a767464bb6fe35ddd..1263778df2d458483d78060efe49e4f9fe151bd3 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 22be6975829ecaf163fd1dbd3a56a853fe68dff4..f38c803607cb95a8eb599a6de01c775ba17d5433 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 aa9c0e0af0727fde531852b659646ed01d75f70c..38f7df405b8bb84d68be22dfa16696568e908382 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,34 +1,8 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' module Rack class TestCase < Minitest::Test - # Check for Lighttpd and launch it for tests if available. - `which lighttpd` - - if $?.success? - begin - # Keep this first. - LIGHTTPD_PID = fork { - ENV['RACK_ENV'] = 'deployment' - ENV['RUBYLIB'] = [ - ::File.expand_path('../../lib', __FILE__), - ENV['RUBYLIB'], - ].compact.join(':') - - Dir.chdir(::File.expand_path("../cgi", __FILE__)) do - exec "lighttpd -D -f lighttpd.conf" - end - } - rescue NotImplementedError - warn "Your Ruby doesn't support Kernel#fork. Skipping Rack::Handler::CGI and ::FastCGI tests." - else - Minitest.after_run do - Process.kill 15, LIGHTTPD_PID - Process.wait LIGHTTPD_PID - end - end - else - warn "Lighttpd isn't installed. Skipping Rack::Handler::CGI and FastCGI tests. Install lighttpd to run them." - end end end diff --git a/test/multipart/filename_with_plus b/test/multipart/filename_with_plus new file mode 100644 index 0000000000000000000000000000000000000000..aa75022b937827fd16a0c77bc1e230e2b436b8e2 --- /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/multipart/robust_field_separation b/test/multipart/robust_field_separation new file mode 100644 index 0000000000000000000000000000000000000000..34956b150c3263f6329bd620255375c77e23d895 --- /dev/null +++ b/test/multipart/robust_field_separation @@ -0,0 +1,6 @@ +--AaB03x +Content-Disposition: form-data;name="text" +Content-Type: text/plain + +contents +--AaB03x-- diff --git a/test/rackup/.gitignore b/test/rackup/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..2f9d279a62f672883b107397c0ce0a5d19fc234a --- /dev/null +++ b/test/rackup/.gitignore @@ -0,0 +1 @@ +log_output diff --git a/test/rackup/config.ru b/test/rackup/config.ru index f1e2e1f3000a5cf9c21d27a29184d19b52296d32..fa9b6ecab50cbd3b7a1eabafa520d07710823738 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 4964953b8d8d6e3f84d5e4b5c0fd004912b6eb58..21b6051676cb8ffe2cf2a3bb1e226accb30ae634 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 45d28576f5a9dddabf6f589b28b6099580fd56fc..3e479ace9b8ae13fec45aef8383d0841fd2e19a2 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 diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb index 7230bb684743c110d6356cf30c0da725f3a8b975..cc205aa9f0dc8c867ca636ea0907bcb305af903a 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 4db447a0acab273d8102bf6eba8775f3a3e79d84..d3853e1e9f269c5a5473fbf1eae594124e2398c5 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 @@ -61,8 +63,8 @@ describe Rack::BodyProxy do body.respond_to?(:to_ary).must_equal true proxy = Rack::BodyProxy.new(body) { } - proxy.respond_to?(:to_ary).must_equal false - proxy.respond_to?("to_ary").must_equal false + x = [proxy] + assert_equal x, x.flatten end it 'not close more than one time' do diff --git a/test/spec_builder.rb b/test/spec_builder.rb index ae1c40065ce51d1363a7ee0620db773ba40f6e95..06918616dfdb701832668bd8422dface154b07fa 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' @@ -6,7 +8,7 @@ require 'rack/show_exceptions' require 'rack/urlmap' class NothingMiddleware - def initialize(app) + def initialize(app, **) @app = app end def call(env) @@ -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' @@ -43,11 +45,11 @@ describe Rack::Builder do it "doesn't dupe env even when mapping" do app = builder_to_app do - use NothingMiddleware + use NothingMiddleware, noop: :noop 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 180ce46eb05444cdfd80a9afd50b8be7ae21972f..b372a56d2f46c345a63903f0a18ccbaf4be4f3b7 100644 --- a/test/spec_cascade.rb +++ b/test/spec_cascade.rb @@ -1,7 +1,9 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/cascade' -require 'rack/file' +require 'rack/files' require 'rack/lint' require 'rack/urlmap' require 'rack/mock' @@ -12,12 +14,12 @@ describe Rack::Cascade do end docroot = File.expand_path(File.dirname(__FILE__)) - app1 = Rack::File.new(docroot) + app1 = Rack::Files.new(docroot) app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" }) 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]) @@ -26,7 +28,7 @@ describe Rack::Cascade do Rack::MockRequest.new(cascade).get("/toobad").must_be :not_found? Rack::MockRequest.new(cascade).get("/cgi/../..").must_be :client_error? - # Put is not allowed by Rack::File so it'll 405. + # Put is not allowed by Rack::Files so it'll 405. Rack::MockRequest.new(cascade).put("/foo").must_be :ok? end diff --git a/test/spec_cgi.rb b/test/spec_cgi.rb deleted file mode 100644 index 77020c2f6f26b10bad89ed8ba00f9ca1d26168e9..0000000000000000000000000000000000000000 --- a/test/spec_cgi.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'helper' - -if defined? LIGHTTPD_PID - -require File.expand_path('../testrequest', __FILE__) -require 'rack/handler/cgi' - -describe Rack::Handler::CGI do - include TestRequest::Helpers - - before do - @host = '127.0.0.1' - @port = 9203 - end - - if `which lighttpd` && !$?.success? - raise "lighttpd not found" - end - - it "respond" do - sleep 1 - GET("/test") - response.wont_be :nil? - end - - it "be a lighttpd" do - GET("/test") - status.must_equal 200 - response["SERVER_SOFTWARE"].must_match(/lighttpd/) - response["HTTP_VERSION"].must_equal "HTTP/1.1" - response["SERVER_PROTOCOL"].must_equal "HTTP/1.1" - response["SERVER_PORT"].must_equal @port.to_s - response["SERVER_NAME"].must_equal @host - end - - it "have rack headers" do - GET("/test") - 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"] - end - - it "have CGI headers on GET" do - GET("/test") - response["REQUEST_METHOD"].must_equal "GET" - response["SCRIPT_NAME"].must_equal "/test" - response["REQUEST_PATH"].must_equal "/" - response["PATH_INFO"].must_be_nil - response["QUERY_STRING"].must_equal "" - response["test.postdata"].must_equal "" - - GET("/test/foo?quux=1") - response["REQUEST_METHOD"].must_equal "GET" - response["SCRIPT_NAME"].must_equal "/test" - response["REQUEST_PATH"].must_equal "/" - response["PATH_INFO"].must_equal "/foo" - response["QUERY_STRING"].must_equal "quux=1" - end - - it "have CGI headers on POST" do - 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" - response["REQUEST_PATH"].must_equal "/" - response["QUERY_STRING"].must_equal "" - response["HTTP_X_TEST_HEADER"].must_equal "42" - response["test.postdata"].must_equal "rack-form-data=23" - end - - it "support HTTP auth" do - GET("/test", {:user => "ruth", :passwd => "secret"}) - response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" - end - - it "set status" do - GET("/test?secret") - status.must_equal 403 - response["rack.url_scheme"].must_equal "http" - end -end - -end # if defined? LIGHTTPD_PID diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb index dc6e8c9d2abc6bed1ac58806c7ba6955c74245cc..23f640a5bf1915c570d75dc380e7a99916c3b45b 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' @@ -16,11 +18,31 @@ describe Rack::Chunked do before do @env = Rack::MockRequest. - env_for('/', 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => 'GET') + env_for('/', 'SERVER_PROTOCOL' => 'HTTP/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,8 +80,8 @@ 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!']] } - @env['HTTP_VERSION'] = 'HTTP/1.0' + app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] } + @env['SERVER_PROTOCOL'] = 'HTTP/1.0' status, headers, body = chunked(app).call(@env) status.must_equal 200 headers.wont_include 'Transfer-Encoding' @@ -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 @@ -75,16 +97,16 @@ describe Rack::Chunked do body.join.must_equal 'Hello World!' end - @env.delete('HTTP_VERSION') # unicorn will do this on pre-HTTP/1.0 requests + @env.delete('SERVER_PROTOCOL') # unicorn will do this on pre-HTTP/1.0 requests check.call - @env['HTTP_VERSION'] = 'HTTP/0.9' # not sure if this happens in practice + @env['SERVER_PROTOCOL'] = 'HTTP/0.9' # not sure if this happens in practice check.call end 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 diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb index 3589576b879a011d78d28c057f3d653acd85b2b8..330a6480b8ebf8b8ab0937d14998e6ff2a7f5c55 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 58f37ad5ec98614095fd0d365cf1f63453b29442..8402f04e86654e204510e312b2f6c2d2b1956538 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 16f0a6649e0f0c646f16ff6d89a5a907a23389cd..d97107b68c5c95ce78f5e18c32bdfc0716819e72 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 89752bbec6a4895d08fed0e9851758f4c8a3718d..2e7a858155fe6121bad6dea6dbd1ec0a9f18ead6 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 @@ -40,7 +42,7 @@ describe Rack::ContentLength do 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_be_nil end @@ -62,7 +64,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) body.closed.must_be_nil response[2].close @@ -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 daf75355dd473d47d0822a81b61dfa6bcd1abafd..53f1d1728c3a6a2f2a3b0b2fe3452662e0f9dc05 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_be_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 0f27c859fb69a88fb018da78189fad7cd5462277..378e2cf30a3d8ebcc792f454fb0e6e6c2d8dec9a 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 @@ -87,7 +98,7 @@ describe Rack::Deflater do end def deflate_or_gzip - {'deflate, gzip' => 'gzip'} + { 'deflate, gzip' => 'gzip' } end it 'be able to deflate bodies that respond to each' do @@ -103,6 +114,19 @@ describe Rack::Deflater do end end + it 'be able to deflate bodies that respond to each and contain empty chunks' do + app_body = Object.new + class << app_body; def each; yield('foo'); yield(''); yield('bar'); end; end + + verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body| + headers.must_equal({ + 'Content-Encoding' => 'gzip', + 'Vary' => 'Accept-Encoding', + 'Content-Type' => 'text/plain' + }) + end + end + it 'flush deflated chunks to the client as they become ready' do app_body = Object.new class << app_body; def each; yield('foo'); yield('bar'); end; end @@ -299,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) @@ -311,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) @@ -320,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) @@ -332,16 +356,25 @@ 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) end + it "not deflate if content-length is 0" do + options = { + 'response_headers' => { + 'Content-Length' => '0' + }, + } + verify(200, '', { 'gzip' => nil }, options) + end + 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_or_gzip, options) @@ -350,7 +383,7 @@ describe Rack::Deflater do 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) @@ -364,7 +397,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 } } @@ -372,4 +405,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 42bdea9f6cae697bed000f907b5f553897aa1589..6d84a2bffa6e0f2fb0f5892bde89e9b69a985e07 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 @@ -82,6 +84,18 @@ describe Rack::Directory do res.must_be :forbidden? end + it "not allow dir globs" do + Dir.mktmpdir do |dir| + weirds = "uploads/.?/.?" + full_dir = File.join(dir, weirds) + FileUtils.mkdir_p full_dir + FileUtils.touch File.join(dir, "secret.txt") + app = Rack::Directory.new(File.join(dir, "uploads")) + res = Rack::MockRequest.new(app).get("/.%3F") + refute_match "secret.txt", res.body + end + end + it "404 if it can't find the file" do res = Rack::MockRequest.new(Rack::Lint.new(app)). get("/cgi/blubb") @@ -109,11 +123,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 74795759b4a664cd9f441ba3f4e2e6539ee8a4cd..750ceaac846ee99505c9a739bde41d2552860431 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_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 7fc7b055cded6e59a8dc9a88b99c7518cae1109a..8c079361cff197058f821a85556f7de23a041639 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 deleted file mode 100644 index 5a48327b15bdcefa1d2223b070a020c3521ef665..0000000000000000000000000000000000000000 --- a/test/spec_fastcgi.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'helper' - -if defined? LIGHTTPD_PID - -require File.expand_path('../testrequest', __FILE__) -require 'rack/handler/fastcgi' - -describe Rack::Handler::FastCGI do - include TestRequest::Helpers - - before do - @host = '127.0.0.1' - @port = 9203 - end - - it "respond" do - sleep 1 - GET("/test") - response.wont_be :nil? - end - - it "respond via rackup server" do - GET("/sample_rackup.ru") - status.must_equal 200 - end - - it "be a lighttpd" do - GET("/test.fcgi") - status.must_equal 200 - response["SERVER_SOFTWARE"].must_match(/lighttpd/) - response["HTTP_VERSION"].must_equal "HTTP/1.1" - response["SERVER_PROTOCOL"].must_equal "HTTP/1.1" - response["SERVER_PORT"].must_equal @port.to_s - response["SERVER_NAME"].must_equal @host - end - - it "have rack headers" do - GET("/test.fcgi") - 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"] - end - - it "have CGI headers on GET" do - GET("/test.fcgi") - response["REQUEST_METHOD"].must_equal "GET" - response["SCRIPT_NAME"].must_equal "/test.fcgi" - response["REQUEST_PATH"].must_equal "/" - response["PATH_INFO"].must_equal "" - response["QUERY_STRING"].must_equal "" - response["test.postdata"].must_equal "" - - GET("/test.fcgi/foo?quux=1") - response["REQUEST_METHOD"].must_equal "GET" - response["SCRIPT_NAME"].must_equal "/test.fcgi" - response["REQUEST_PATH"].must_equal "/" - response["PATH_INFO"].must_equal "/foo" - response["QUERY_STRING"].must_equal "quux=1" - end - - it "have CGI headers on POST" do - 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" - response["REQUEST_PATH"].must_equal "/" - response["QUERY_STRING"].must_equal "" - response["HTTP_X_TEST_HEADER"].must_equal "42" - response["test.postdata"].must_equal "rack-form-data=23" - end - - it "support HTTP auth" do - GET("/test.fcgi", {:user => "ruth", :passwd => "secret"}) - response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" - end - - it "set status" do - GET("/test.fcgi?secret") - status.must_equal 403 - response["rack.url_scheme"].must_equal "http" - end -end - -end # if defined? LIGHTTPD_PID diff --git a/test/spec_file.rb b/test/spec_files.rb similarity index 69% rename from test/spec_file.rb rename to test/spec_files.rb index 48c0ab909abbcb0ca7c2a2ebbd2882c6bfdfb55f..6e75c073a00f509a571d890b0e9d60d885599c89 100644 --- a/test/spec_file.rb +++ b/test/spec_files.rb @@ -1,39 +1,55 @@ -require 'minitest/autorun' -require 'rack/file' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' +require 'rack/files' require 'rack/lint' require 'rack/mock' -describe Rack::File do +describe Rack::Files do DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT - def file(*args) - Rack::Lint.new Rack::File.new(*args) + def files(*args) + Rack::Lint.new Rack::Files.new(*args) + end + + it "can be used without root" do + # https://github.com/rack/rack/issues/1464 + + app = Rack::Files.new(nil) + + request = Rack::Request.new( + Rack::MockRequest.env_for("/cgi/test") + ) + + file_path = File.expand_path("cgi/test", __dir__) + status, headers, body = app.serving(request, file_path) + assert_equal 200, status end it 'serves files with + in the file name' do Dir.mktmpdir do |dir| File.write File.join(dir, "you+me.txt"), "hello world" - app = file(dir) + app = files(dir) env = Rack::MockRequest.env_for("/you+me.txt") - status,_,body = app.call env + status, _, body = app.call env assert_equal 200, status - str = '' + str = ''.dup body.each { |x| str << x } assert_match "hello world", str end end it "serve files" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") res.must_be :ok? assert_match(res, /ruby/) end it "set Last-Modified header" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") path = File.join(DOCROOT, "/cgi/test") @@ -43,7 +59,7 @@ describe Rack::File do it "return 304 if file isn't modified since last serve" do path = File.join(DOCROOT, "/cgi/test") - res = Rack::MockRequest.new(file(DOCROOT)). + res = Rack::MockRequest.new(files(DOCROOT)). get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate) res.status.must_equal 304 @@ -52,14 +68,14 @@ describe Rack::File do it "return the file if it's modified since last serve" do path = File.join(DOCROOT, "/cgi/test") - res = Rack::MockRequest.new(file(DOCROOT)). + res = Rack::MockRequest.new(files(DOCROOT)). get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => (File.mtime(path) - 100).httpdate) res.must_be :ok? end it "serve files with URL encoded filenames" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test" + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test" res.must_be :ok? # res.must_match(/ruby/) # nope @@ -69,12 +85,12 @@ describe Rack::File do end it "serve uri with URL encoded null byte (%00) in filenames" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test%00") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test%00") res.must_be :bad_request? end it "allow safe directory traversal" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get('/cgi/../cgi/test') res.must_be :successful? @@ -87,7 +103,7 @@ describe Rack::File do end it "not allow unsafe directory traversal" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get("/../README.rdoc") res.must_be :client_error? @@ -102,7 +118,7 @@ describe Rack::File do end it "allow files with .. in their name" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.get("/cgi/..test") res.must_be :not_found? @@ -114,33 +130,33 @@ describe Rack::File do end it "not allow unsafe directory traversal with encoded periods" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/%2E%2E/README") + res = Rack::MockRequest.new(files(DOCROOT)).get("/%2E%2E/README") res.must_be :client_error? res.must_be :not_found? end it "allow safe directory traversal with encoded periods" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%2E%2E/cgi/test") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%2E%2E/cgi/test") res.must_be :successful? end it "404 if it can't find the file" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/blubb") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/blubb") res.must_be :not_found? end it "detect SystemCallErrors" do - res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi") + res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi") res.must_be :not_found? end it "return bodies that respond to #to_path" do env = Rack::MockRequest.env_for("/cgi/test") - status, _, body = Rack::File.new(DOCROOT).call(env) + status, _, body = Rack::Files.new(DOCROOT).call(env) path = File.join(DOCROOT, "/cgi/test") @@ -152,26 +168,26 @@ describe Rack::File do it "return correct byte range in body" do env = Rack::MockRequest.env_for("/cgi/test") env["HTTP_RANGE"] = "bytes=22-33" - res = Rack::MockResponse.new(*file(DOCROOT).call(env)) + res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 206 res["Content-Length"].must_equal "12" - 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 env = Rack::MockRequest.env_for("/cgi/test") env["HTTP_RANGE"] = "bytes=1234-5678" - res = Rack::MockResponse.new(*file(DOCROOT).call(env)) + res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 416 - res["Content-Range"].must_equal "bytes */193" + res["Content-Range"].must_equal "bytes */208" end it "support custom http headers" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38', + status, heads, _ = files(DOCROOT, 'Cache-Control' => 'public, max-age=38', 'Access-Control-Allow-Origin' => '*').call(env) status.must_equal 200 @@ -181,7 +197,7 @@ describe Rack::File do it "support not add custom http headers if none are supplied" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = file(DOCROOT).call(env) + status, heads, _ = files(DOCROOT).call(env) status.must_equal 200 heads['Cache-Control'].must_be_nil @@ -189,7 +205,7 @@ describe Rack::File do end it "only support GET, HEAD, and OPTIONS requests" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) forbidden = %w[post put patch delete] forbidden.each do |method| @@ -207,7 +223,7 @@ describe Rack::File do end it "set Allow correctly for OPTIONS requests" do - req = Rack::MockRequest.new(file(DOCROOT)) + req = Rack::MockRequest.new(files(DOCROOT)) res = req.options('/cgi/test') res.must_be :successful? res.headers['Allow'].wont_equal nil @@ -215,35 +231,35 @@ describe Rack::File do end it "set Content-Length correctly for HEAD requests" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.head "/cgi/test" res.must_be :successful? - res['Content-Length'].must_equal "193" + res['Content-Length'].must_equal "208" end it "default to a mime type of text/plain" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_equal "text/plain" end it "allow the default mime type to be set" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, 'application/octet-stream'))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, 'application/octet-stream'))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_equal "application/octet-stream" end it "not set Content-Type if the mime type is not set" do - req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil))) + req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, nil))) res = req.get "/cgi/test" res.must_be :successful? res['Content-Type'].must_be_nil end it "return error when file not found for head request" do - res = Rack::MockRequest.new(file(DOCROOT)).head("/cgi/missing") + res = Rack::MockRequest.new(files(DOCROOT)).head("/cgi/missing") res.must_be :not_found? res.body.must_be :empty? end diff --git a/test/spec_handler.rb b/test/spec_handler.rb index dff474c989fb3ca86272d61c53543c51cff70b77..5746dc2251a7bf6e0513442d47d6098ae293c5b9 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 17b4a3497cc0a4f808db2d77584a8960c22a944d..f6f41a5d9d506e247db1abeda25ee1568d71dd37 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 d99c1aa3150c4e6040d175e39d5ef9d58558dbc0..192f260f09ff8a922f820dc58a82cd4725fb3ebb 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? @@ -272,7 +274,7 @@ describe Rack::Lint do [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/) @@ -283,7 +285,7 @@ describe Rack::Lint do [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 fd4e70826f1ff4bd37936d91dbad3697ddd803f8..9f3b9a897d512b07fab2cd268790eaf76c75e6fd 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 c6f7c05ec70a8568964b90fc676c631510280162..cd9e1230d2d8117cad30b49a9757a3b1385339df 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,7 +145,7 @@ 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 @@ -158,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 @@ -170,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 @@ -180,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 @@ -195,7 +197,7 @@ describe Rack::Lock do 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]] }) + app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, [inner_env.object_id.to_s]] }) _, _, body = app.call(env) diff --git a/test/spec_logger.rb b/test/spec_logger.rb index ea503e1d066bf33bcc98ee2e943f02e1a4c4273c..f453b14df369f1ab82cd771af5a581df08b86d4f 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 1d9f0fc36326ef680595d5d301aa6277d1111690..7d52b4d48e6a1acfbcf3eda942ae3670be2b82cf 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 @@ -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 1371ca9d465aaf64cd411d47c785ade0a05a839d..6b01f7c94600d4b858ecac5bed64bf7ab8e57bb0 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,12 +8,12 @@ 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" @@ -32,7 +34,7 @@ describe Rack::MethodOverride do 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" @@ -49,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" @@ -64,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" @@ -95,14 +97,14 @@ EOF "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 + 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 569233b491237589a77e6ce6e77d4c47e26769a2..8d1ca2566d120896138cde6c8275fc05657c20e4 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 diff --git a/test/spec_mock.rb b/test/spec_mock.rb index e383f203fdd37b75682e7fa1c2262c204bdcae99..d7246d3f930bf02b3bb8dee78bb0424201e63ed7 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,16 +216,16 @@ 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 @@ -216,13 +233,13 @@ describe Rack::MockRequest 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 + 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 @@ -248,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 @@ -263,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/) @@ -270,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" @@ -286,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 2f957d92ab510a0b87ffdce7d38c2d62d1c63bc9..dbf6fba4d2a29fdcee9d263ede719b2be2e60f58 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -1,11 +1,12 @@ -# coding: utf-8 +# frozen_string_literal: true -require 'minitest/autorun' +require 'minitest/global_expectations/autorun' require 'rack' require 'rack/multipart' require 'rack/multipart/parser' require 'rack/utils' require 'rack/mock' +require 'timeout' describe Rack::Multipart do def multipart_fixture(name, boundary = "AaB03x") @@ -107,11 +108,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 +132,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, } @@ -151,6 +147,31 @@ describe Rack::Multipart do wr.close end + # see https://github.com/rack/rack/pull/1309 + it "parse strange multipart pdf" do + boundary = '---------------------------932620571087722842402766118' + + data = StringIO.new + data.write("--#{boundary}") + data.write("\r\n") + data.write('Content-Disposition: form-data; name="a"; filename="a.pdf"') + data.write("\r\n") + data.write("Content-Type:application/pdf\r\n") + data.write("\r\n") + data.write("-" * (1024 * 1024)) + data.write("\r\n") + data.write("--#{boundary}--\r\n") + + fixture = { + "CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}", + "CONTENT_LENGTH" => data.length.to_s, + :input => data, + } + + env = Rack::MockRequest.env_for '/', fixture + Timeout::timeout(10) { Rack::Multipart.parse_multipart(env) } + end + it 'raises an EOF error on content-length mistmatch' do env = Rack::MockRequest.env_for("/", multipart_fixture(:empty)) env['rack.input'] = StringIO.new @@ -177,7 +198,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 +227,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 @@ -311,6 +332,12 @@ describe Rack::Multipart do params["files"][:filename].must_equal "flowers.exe\u0000.jpg" end + it "is robust separating Content-Disposition fields" do + env = Rack::MockRequest.env_for("/", multipart_fixture(:robust_field_separation)) + params = Rack::Multipart.parse_multipart(env) + params["text"].must_equal "contents" + 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) @@ -368,6 +395,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) @@ -482,7 +522,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", @@ -562,7 +602,7 @@ 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 = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => "contents" }]) data.must_be_nil end @@ -589,7 +629,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 @@ -688,7 +728,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 @@ -707,7 +747,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 3002d97fc7f84cec6e80aefc2e7e364ebaa529a8..1037c9fa369b716d951f32581d34182e8504fc2e 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 4a60e6004b1a2371911965b67a06c82dea8bc02a..e77d966d5d611c192af0c35e6164964ae536d4de 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 cfaedbcfe6cdc9aff9377dca2248c62fc826b2ae..583a367e35fa2013b3b56e7e2401bbbec574e48c 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) @@ -727,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" @@ -958,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 @@ -1077,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 @@ -1097,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) @@ -1108,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 @@ -1221,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' @@ -1286,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_be_nil - req.trusted_proxy?("example.org\n127.0.0.1").must_be_nil - req.trusted_proxy?("127.0.0.1\nexample.org").must_be_nil - req.trusted_proxy?("11.0.0.1").must_be_nil - req.trusted_proxy?("172.15.0.1").must_be_nil - req.trusted_proxy?("172.32.0.1").must_be_nil - req.trusted_proxy?("2001:470:1f0b:18f8::1").must_be_nil + req.trusted_proxy?('127.0.0.1').must_equal true + req.trusted_proxy?('10.0.0.1').must_equal true + req.trusted_proxy?('172.16.0.1').must_equal true + req.trusted_proxy?('172.20.0.1').must_equal true + req.trusted_proxy?('172.30.0.1').must_equal true + req.trusted_proxy?('172.31.0.1').must_equal true + req.trusted_proxy?('192.168.0.1').must_equal true + req.trusted_proxy?('::1').must_equal true + req.trusted_proxy?('fd00::').must_equal true + req.trusted_proxy?('localhost').must_equal true + req.trusted_proxy?('unix').must_equal true + req.trusted_proxy?('unix:/tmp/sock').must_equal true + + req.trusted_proxy?("unix.example.org").must_equal false + req.trusted_proxy?("example.org\n127.0.0.1").must_equal false + req.trusted_proxy?("127.0.0.1\nexample.org").must_equal false + req.trusted_proxy?("11.0.0.1").must_equal false + req.trusted_proxy?("172.15.0.1").must_equal false + req.trusted_proxy?("172.32.0.1").must_equal false + req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal false end it "sets the default session to an empty hash" do @@ -1317,7 +1350,7 @@ EOF class MyRequest < Rack::Request def params - {:foo => "bar"} + { foo: "bar" } end end @@ -1330,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 @@ -1338,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 987199de50526a555904f53638f98a2b3038564a..c0736c73d7240a094cb1ce40e9e5b993d735d441 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 @@ -60,6 +62,16 @@ describe Rack::Response do response["Content-Type"].must_equal "text/plain" end + it "doesn't mutate given headers" do + [{}, Rack::Utils::HeaderHash.new].each do |header| + response = Rack::Response.new([], 200, header) + response.header["Content-Type"] = "text/plain" + response.header["Content-Type"].must_equal "text/plain" + + header.wont_include("Content-Type") + end + end + it "can override the initial Content-Type with a different case" do response = Rack::Response.new("", 200, "content-type" => "text/plain") response["Content-Type"].must_equal "text/plain" @@ -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,15 +301,15 @@ 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_be_nil header['Content-Length'].must_be_nil lambda { - Rack::Response.new(Object.new) - }.must_raise(TypeError). - message.must_match(/stringable or iterable required/) + Rack::Response.new(Object.new).each{} + }.must_raise(NoMethodError). + message.must_match(/undefined method .each. for/) end it "knows if it's empty" do @@ -403,6 +433,28 @@ describe Rack::Response do res.headers["Content-Length"].must_equal "8" end + it "does not wrap body" do + body = Object.new + res = Rack::Response.new(body) + + # It was passed through unchanged: + res.finish.last.must_equal body + end + + it "does wraps body when using #write" do + body = ["Foo"] + res = Rack::Response.new(body) + + # Write something using the response object: + res.write("Bar") + + # The original body was not modified: + body.must_equal ["Foo"] + + # But a new buffered body was created: + res.finish.last.must_equal ["Foo", "Bar"] + end + it "calls close on #body" do res = Rack::Response.new res.body = StringIO.new @@ -421,12 +473,6 @@ describe Rack::Response do res.body.must_be :closed? b.wont_equal res.body - res.body = StringIO.new - res.status = 205 - _, _, b = res.finish - res.body.wont_be :closed? - b.wont_equal res.body - res.body = StringIO.new res.status = 304 _, _, b = res.finish @@ -434,10 +480,20 @@ describe Rack::Response do b.wont_equal res.body end - it "wraps the body from #to_ary to prevent infinite loops" do + it "doesn't call close on #body when 205" do res = Rack::Response.new - res.finish.last.wont_respond_to(:to_ary) - lambda { res.finish.last.to_ary }.must_raise NoMethodError + + res.body = StringIO.new + res.status = 205 + _, _, b = res.finish + res.body.wont_be :closed? + end + + it "flatten doesn't cause infinite loop" do + # https://github.com/rack/rack/issues/419 + res = Rack::Response.new("Hello World") + + res.finish.flatten.must_be_kind_of(Array) end end diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb index 5adfba1de652d4e720bcdbc634479de1f6de8379..6bb5f5cf884600795efc945c551f65b6ae2a6616 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 f7f52ad9afd85b81a5ca477bb36260507d9bba60..10e561dec1ef6c049bc72c574eb39f2f67b2c457 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 1868985750e36dd07f2ef31069884774dd0306af..cbed8db3c1c812c8fcfcc4edff1009dd705b93a7 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 4864a87a4c6de2ddc02d249a3376d76d9238651a..8aecc554cea8cd1652844ee259799c05142e1860 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,29 +55,29 @@ 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_be_nil ensure @@ -84,7 +86,7 @@ describe Rack::Server do end it "be quiet if said so" do - server = Rack::Server.new(:app => "FOO", :quiet => true) + server = Rack::Server.new(app: "FOO", quiet: true) Rack::Server.logging_middleware.call(server).must_be_nil end @@ -118,19 +120,23 @@ 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 - body = open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read } + body = if URI.respond_to?(:open) + URI.open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read } + else + open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read } + end body.must_equal 'success' Process.kill(:INT, $$) @@ -140,34 +146,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 +188,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 a6568f19842b87f61e87ea857db7671f5b2e71aa..3591a3dea145f09e15465f149130040f51b99c71 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 76b34a01ec99786e4609b137559f083889b65044..5d0d10ce4f23ef8a9ec087788ab140ad6a514f96 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 @@ -42,4 +44,10 @@ describe Rack::Session::Abstract::SessionHash do lambda { hash.fetch(:unknown) }.must_raise KeyError end end + + describe "#stringify_keys" do + it "returns hash or session hash with keys stringified" do + assert_equal({ "foo" => :bar, "baz" => :qux }, hash.send(:stringify_keys, hash).to_h) + end + end end diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb index 9201a72954500b4cbf151cb846cc5a838ba1074b..57850239e55696c754d6087c3bdef668e1017b88 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"] @@ -74,26 +76,32 @@ describe Rack::Session::Cookie do it 'uses base64 to encode' do coder = Rack::Session::Cookie::Base64.new str = 'fuuuuu' - coder.encode(str).must_equal [str].pack('m') + coder.encode(str).must_equal [str].pack('m0') end it 'uses base64 to decode' do coder = Rack::Session::Cookie::Base64.new - str = ['fuuuuu'].pack('m') - coder.decode(str).must_equal str.unpack('m').first + str = ['fuuuuu'].pack('m0') + coder.decode(str).must_equal str.unpack('m0').first + end + + it 'handles non-strict base64 encoding' do + coder = Rack::Session::Cookie::Base64.new + str = ['A' * 256].pack('m') + coder.decode(str).must_equal 'A' * 256 end describe 'Marshal' do it 'marshals and base64 encodes' do coder = Rack::Session::Cookie::Base64::Marshal.new str = 'fuuuuu' - coder.encode(str).must_equal [::Marshal.dump(str)].pack('m') + coder.encode(str).must_equal [::Marshal.dump(str)].pack('m0') end it 'marshals and base64 decodes' do coder = Rack::Session::Cookie::Base64::Marshal.new - str = [::Marshal.dump('fuuuuu')].pack('m') - coder.decode(str).must_equal ::Marshal.load(str.unpack('m').first) + str = [::Marshal.dump('fuuuuu')].pack('m0') + coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first) end it 'rescues failures on decode' do @@ -106,13 +114,13 @@ describe Rack::Session::Cookie 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') + coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m0') end 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) + str = [::JSON.dump(%w[fuuuuu])].pack('m0') + coder.decode(str).must_equal ::JSON.parse(str.unpack('m0').first) end it 'rescues failures on decode' do @@ -126,14 +134,14 @@ describe Rack::Session::Cookie do coder = Rack::Session::Cookie::Base64::ZipJSON.new obj = %w[fuuuuu] json = JSON.dump(obj) - coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m') + coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m0') end it 'base64 decodes, inflates, and decodes json' do coder = Rack::Session::Cookie::Base64::ZipJSON.new obj = %w[fuuuuu] json = JSON.dump(obj) - b64 = [Zlib::Deflate.deflate(json)].pack('m') + b64 = [Zlib::Deflate.deflate(json)].pack('m0') coder.decode(b64).must_equal obj end @@ -148,22 +156,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 +186,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 +194,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 +242,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 +354,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 +431,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 +441,44 @@ 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 + + it 'allows for non-strict encoded cookie' do + long_session_app = lambda do |env| + env['rack.session']['value'] = 'A' * 256 + env['rack.session']['counter'] = 1 + hash = env["rack.session"].dup + hash.delete("session_id") + Rack::Response.new(hash.inspect).to_a + end + + non_strict_coder = Class.new { + def encode(str) + [Marshal.dump(str)].pack('m') + end + + def decode(str) + return unless str + + Marshal.load(str.unpack('m').first) + end + }.new + + non_strict_response = response_for(app: [ + long_session_app, { coder: non_strict_coder } + ]) + + response = response_for(app: [ + incrementor + ], cookie: non_strict_response) + + response.body.must_match %Q["value"=>"#{'A' * 256}"] + response.body.must_match '"counter"=>2' + response.body.must_match(/\A{[^}]+}\z/) + end end diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb deleted file mode 100644 index 93a03d120569e2d7987368ca4d99b493316af178..0000000000000000000000000000000000000000 --- a/test/spec_session_memcache.rb +++ /dev/null @@ -1,320 +0,0 @@ -require 'minitest/autorun' -begin - require 'rack/session/memcache' - require 'rack/lint' - require 'rack/mock' - require 'thread' - - describe Rack::Session::Memcache do - session_key = Rack::Session::Memcache::DEFAULT_OPTIONS[:key] - session_match = /#{session_key}=([0-9a-fA-F]+);/ - incrementor = lambda do |env| - env["rack.session"]["counter"] ||= 0 - env["rack.session"]["counter"] += 1 - Rack::Response.new(env["rack.session"].inspect).to_a - end - drop_session = Rack::Lint.new(proc do |env| - env['rack.session.options'][:drop] = true - incrementor.call(env) - end) - renew_session = Rack::Lint.new(proc do |env| - env['rack.session.options'][:renew] = true - incrementor.call(env) - end) - defer_session = Rack::Lint.new(proc do |env| - env['rack.session.options'][:defer] = true - incrementor.call(env) - end) - skip_session = Rack::Lint.new(proc do |env| - env['rack.session.options'][:skip] = true - incrementor.call(env) - end) - incrementor = Rack::Lint.new(incrementor) - - # test memcache connection - Rack::Session::Memcache.new(incrementor) - - it "faults on no connection" do - lambda { - 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.namespace.must_equal 'test:rack:session' - end - - it "passes options to MemCache" do - pool = Rack::Session::Memcache.new(incrementor, :namespace => 'test:rack:session') - pool.pool.namespace.must_equal 'test:rack:session' - end - - it "creates a new cookie" do - pool = Rack::Session::Memcache.new(incrementor) - res = Rack::MockRequest.new(pool).get("/") - res["Set-Cookie"].must_include "#{session_key}=" - res.body.must_equal '{"counter"=>1}' - end - - it "determines session from a cookie" do - pool = Rack::Session::Memcache.new(incrementor) - req = Rack::MockRequest.new(pool) - res = req.get("/") - cookie = res["Set-Cookie"] - req.get("/", "HTTP_COOKIE" => cookie). - body.must_equal '{"counter"=>2}' - req.get("/", "HTTP_COOKIE" => cookie). - body.must_equal '{"counter"=>3}' - end - - it "determines session only from a cookie by default" do - pool = Rack::Session::Memcache.new(incrementor) - req = Rack::MockRequest.new(pool) - res = req.get("/") - sid = res["Set-Cookie"][session_match, 1] - req.get("/?rack.session=#{sid}"). - body.must_equal '{"counter"=>1}' - req.get("/?rack.session=#{sid}"). - body.must_equal '{"counter"=>1}' - end - - it "determines session from params" do - pool = Rack::Session::Memcache.new(incrementor, :cookie_only => false) - req = Rack::MockRequest.new(pool) - res = req.get("/") - sid = res["Set-Cookie"][session_match, 1] - req.get("/?rack.session=#{sid}"). - body.must_equal '{"counter"=>2}' - req.get("/?rack.session=#{sid}"). - body.must_equal '{"counter"=>3}' - end - - it "survives nonexistant cookies" do - bad_cookie = "rack.session=blarghfasel" - pool = Rack::Session::Memcache.new(incrementor) - res = Rack::MockRequest.new(pool). - get("/", "HTTP_COOKIE" => bad_cookie) - res.body.must_equal '{"counter"=>1}' - cookie = res["Set-Cookie"][session_match] - cookie.wont_match(/#{bad_cookie}/) - end - - it "maintains freshness" do - 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"] - res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie) - res["Set-Cookie"].must_equal cookie - res.body.must_include '"counter"=>2' - puts 'Sleeping to expire session' if $DEBUG - sleep 4 - res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie) - res["Set-Cookie"].wont_equal cookie - res.body.must_include '"counter"=>1' - end - - it "does not send the same session id if it did not change" do - pool = Rack::Session::Memcache.new(incrementor) - req = Rack::MockRequest.new(pool) - - res0 = req.get("/") - cookie = res0["Set-Cookie"][session_match] - res0.body.must_equal '{"counter"=>1}' - - res1 = req.get("/", "HTTP_COOKIE" => cookie) - res1["Set-Cookie"].must_be_nil - res1.body.must_equal '{"counter"=>2}' - - res2 = req.get("/", "HTTP_COOKIE" => cookie) - res2["Set-Cookie"].must_be_nil - res2.body.must_equal '{"counter"=>3}' - end - - it "deletes cookies with :drop option" do - pool = Rack::Session::Memcache.new(incrementor) - req = Rack::MockRequest.new(pool) - drop = Rack::Utils::Context.new(pool, drop_session) - dreq = Rack::MockRequest.new(drop) - - res1 = req.get("/") - session = (cookie = res1["Set-Cookie"])[session_match] - res1.body.must_equal '{"counter"=>1}' - - res2 = dreq.get("/", "HTTP_COOKIE" => cookie) - res2["Set-Cookie"].must_be_nil - res2.body.must_equal '{"counter"=>2}' - - res3 = req.get("/", "HTTP_COOKIE" => cookie) - res3["Set-Cookie"][session_match].wont_equal session - res3.body.must_equal '{"counter"=>1}' - end - - it "provides new session id with :renew option" do - pool = Rack::Session::Memcache.new(incrementor) - req = Rack::MockRequest.new(pool) - renew = Rack::Utils::Context.new(pool, renew_session) - rreq = Rack::MockRequest.new(renew) - - res1 = req.get("/") - session = (cookie = res1["Set-Cookie"])[session_match] - res1.body.must_equal '{"counter"=>1}' - - res2 = rreq.get("/", "HTTP_COOKIE" => cookie) - new_cookie = res2["Set-Cookie"] - new_session = new_cookie[session_match] - new_session.wont_equal session - res2.body.must_equal '{"counter"=>2}' - - res3 = req.get("/", "HTTP_COOKIE" => new_cookie) - res3.body.must_equal '{"counter"=>3}' - - # Old cookie was deleted - res4 = req.get("/", "HTTP_COOKIE" => cookie) - res4.body.must_equal '{"counter"=>1}' - end - - it "omits cookie with :defer option but still updates the state" do - pool = Rack::Session::Memcache.new(incrementor) - count = Rack::Utils::Context.new(pool, incrementor) - defer = Rack::Utils::Context.new(pool, defer_session) - dreq = Rack::MockRequest.new(defer) - creq = Rack::MockRequest.new(count) - - res0 = dreq.get("/") - res0["Set-Cookie"].must_be_nil - res0.body.must_equal '{"counter"=>1}' - - res0 = creq.get("/") - res1 = dreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) - res1.body.must_equal '{"counter"=>2}' - res2 = dreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) - res2.body.must_equal '{"counter"=>3}' - end - - it "omits cookie and state update with :skip option" do - pool = Rack::Session::Memcache.new(incrementor) - count = Rack::Utils::Context.new(pool, incrementor) - skip = Rack::Utils::Context.new(pool, skip_session) - sreq = Rack::MockRequest.new(skip) - creq = Rack::MockRequest.new(count) - - res0 = sreq.get("/") - res0["Set-Cookie"].must_be_nil - res0.body.must_equal '{"counter"=>1}' - - res0 = creq.get("/") - res1 = sreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) - res1.body.must_equal '{"counter"=>2}' - res2 = sreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"]) - res2.body.must_equal '{"counter"=>2}' - end - - it "updates deep hashes correctly" do - 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 - else - session[:f][:g][:h] = :j - end - [200, {}, [session.inspect]] - end - pool = Rack::Session::Memcache.new(hash_check) - req = Rack::MockRequest.new(pool) - - res0 = req.get("/") - session_id = (cookie = res0["Set-Cookie"])[session_match, 1] - ses0 = pool.pool.get(session_id, true) - - req.get("/", "HTTP_COOKIE" => cookie) - ses1 = pool.pool.get(session_id, true) - - ses1.wont_equal ses0 - end - - # anyone know how to do this better? - it "cleanly merges sessions when multithreaded" do - skip unless $DEBUG - - warn 'Running multithread test for Session::Memcache' - pool = Rack::Session::Memcache.new(incrementor) - req = Rack::MockRequest.new(pool) - - res = req.get('/') - res.body.must_equal '{"counter"=>1}' - cookie = res["Set-Cookie"] - session_id = cookie[session_match, 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 - incrementor.call(env) - end - tses = Rack::Utils::Context.new pool, delta_incrementor - treq = Rack::MockRequest.new(tses) - tnum = rand(7).to_i+5 - r = Array.new(tnum) do - Thread.new(treq) do |run| - run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) - end - end.reverse.map{|t| t.run.join.value } - r.each do |request| - request['Set-Cookie'].must_equal cookie - request.body.must_include '"counter"=>2' - end - - session = pool.pool.get(session_id) - session.size.must_equal tnum+1 # counter - session['counter'].must_equal 2 # meeeh - - tnum = rand(7).to_i+5 - r = Array.new(tnum) do - app = Rack::Utils::Context.new pool, time_delta - req = Rack::MockRequest.new app - Thread.new(req) do |run| - run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) - end - end.reverse.map{|t| t.run.join.value } - r.each do |request| - request['Set-Cookie'].must_equal cookie - request.body.must_include '"counter"=>3' - end - - session = pool.pool.get(session_id) - 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] - end - tses = Rack::Utils::Context.new pool, drop_counter - treq = Rack::MockRequest.new(tses) - tnum = rand(7).to_i+5 - r = Array.new(tnum) do - Thread.new(treq) do |run| - run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true) - end - end.reverse.map{|t| t.run.join.value } - r.each do |request| - request['Set-Cookie'].must_equal cookie - request.body.must_include '"foo"=>"bar"' - end - - session = pool.pool.get(session_id) - session.size.must_equal r.size+1 - session['counter'].must_be_nil? - session['foo'].must_equal 'bar' - end - end -rescue RuntimeError - $stderr.puts "Skipping Rack::Session::Memcache tests. Start memcached and try again." -rescue LoadError - $stderr.puts "Skipping Rack::Session::Memcache tests (Memcache is required). `gem install memcache-client` and try again." -end diff --git a/test/spec_session_persisted_secure_secure_session_hash.rb b/test/spec_session_persisted_secure_secure_session_hash.rb new file mode 100644 index 0000000000000000000000000000000000000000..21cbf8e6e5f544482b11d2a494545446a9050c31 --- /dev/null +++ b/test/spec_session_persisted_secure_secure_session_hash.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' +require 'rack/session/abstract/id' + +describe Rack::Session::Abstract::PersistedSecure::SecureSessionHash do + attr_reader :hash + + def setup + super + @store = Class.new do + def load_session(req) + [Rack::Session::SessionId.new("id"), { foo: :bar, baz: :qux }] + end + def session_exists?(req) + true + end + end + @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(@store.new, nil) + end + + it "returns keys" do + assert_equal ["foo", "baz"], hash.keys + end + + it "returns values" do + assert_equal [:bar, :qux], hash.values + end + + describe "#[]" do + it "returns value for a matching key" do + assert_equal :bar, hash[:foo] + end + + it "returns value for a 'session_id' key" do + assert_equal "id", hash['session_id'] + end + + it "returns nil value for missing 'session_id' key" do + store = @store.new + def store.load_session(req) + [nil, {}] + end + @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(store, nil) + assert_nil hash['session_id'] + end + 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 + + describe "#stringify_keys" do + it "returns hash or session hash with keys stringified" do + assert_equal({ "foo" => :bar, "baz" => :qux }, hash.send(:stringify_keys, hash).to_h) + end + end +end + diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb index 2d061691548636d151ce5ba72e8f87529f8f0565..dd1b6573f74e1553312b5dcfe51cf4af8985cd43 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' @@ -6,7 +8,7 @@ require 'rack/session/pool' describe Rack::Session::Pool do session_key = Rack::Session::Pool::DEFAULT_OPTIONS[:key] - session_match = /#{session_key}=[0-9a-fA-F]+;/ + session_match = /#{session_key}=([0-9a-fA-F]+);/ incrementor = lambda do |env| env["rack.session"]["counter"] ||= 0 @@ -14,7 +16,7 @@ describe Rack::Session::Pool do Rack::Response.new(env["rack.session"].inspect).to_a end - session_id = Rack::Lint.new(lambda do |env| + get_session_id = Rack::Lint.new(lambda do |env| Rack::Response.new(env["rack.session"].inspect).to_a end) @@ -143,6 +145,43 @@ describe Rack::Session::Pool do pool.pool.size.must_equal 1 end + it "can read the session with the legacy id" do + pool = Rack::Session::Pool.new(incrementor) + req = Rack::MockRequest.new(pool) + + res0 = req.get("/") + cookie = res0["Set-Cookie"] + session_id = Rack::Session::SessionId.new cookie[session_match, 1] + ses0 = pool.pool[session_id.private_id] + pool.pool[session_id.public_id] = ses0 + pool.pool.delete(session_id.private_id) + + res1 = req.get("/", "HTTP_COOKIE" => cookie) + res1["Set-Cookie"].must_be_nil + res1.body.must_equal '{"counter"=>2}' + pool.pool[session_id.private_id].wont_be_nil + end + + it "drops the session in the legacy id as well" do + pool = Rack::Session::Pool.new(incrementor) + req = Rack::MockRequest.new(pool) + drop = Rack::Utils::Context.new(pool, drop_session) + dreq = Rack::MockRequest.new(drop) + + res0 = req.get("/") + cookie = res0["Set-Cookie"] + session_id = Rack::Session::SessionId.new cookie[session_match, 1] + ses0 = pool.pool[session_id.private_id] + pool.pool[session_id.public_id] = ses0 + pool.pool.delete(session_id.private_id) + + res2 = dreq.get("/", "HTTP_COOKIE" => cookie) + res2["Set-Cookie"].must_be_nil + res2.body.must_equal '{"counter"=>2}' + pool.pool[session_id.private_id].must_be_nil + pool.pool[session_id.public_id].must_be_nil + end + # anyone know how to do this better? it "should merge sessions when multithreaded" do unless $DEBUG @@ -157,18 +196,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 +219,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 @@ -191,19 +230,19 @@ describe Rack::Session::Pool do end it "does not return a cookie if cookie was not written (only read)" do - app = Rack::Session::Pool.new(session_id) + app = Rack::Session::Pool.new(get_session_id) res = Rack::MockRequest.new(app).get("/") res["Set-Cookie"].must_be_nil 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 61e5d92d692175a7d45a2d4c8d57fb1469932d56..a4ade121d5c844c0ada17534361b9caeefaf6c51 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) @@ -78,6 +98,28 @@ describe Rack::ShowExceptions do 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) diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb index d32dc7cb9594884fdd59f225a35d5b4546463a71..ca23134e0598ed424b5d68e8a3fa808755342b57 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 634f8acf763e8e9e026f8079193311d91b25a401..d33e8edcbf143ec3a8e4316683f756e63d935579 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,7 +109,7 @@ 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_be_nil res.headers['Content-Type'].must_equal 'text/x-script.ruby' @@ -111,21 +125,21 @@ describe Rack::Static do 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 b7c6256390127e998a3080426e6c09cd255682c9..0e7de841503714ee113e0b0f6f496cfbcb7fa433 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 85b225ed922e83142cc27d7e6d67d248e21a65a9..0729c3f3c39f7d29975efff20fabd984f69241ac 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 9d655c22040221fb22f588d77149bb1f8854b2c2..9ce382987f56266bb28e2a85f924d37c6a04d5ee 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 143ad30a68eaa62a7c2349e727149da6a927ac15..5fd9224113847a1b4b784cb7d6ab84c5bb36eb75 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). @@ -233,13 +234,13 @@ describe Rack::Utils do 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"}]}] + 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"}}, + { "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 @@ -249,7 +250,7 @@ describe Rack::Utils do 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) @@ -320,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") }. @@ -329,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 @@ -461,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 @@ -492,19 +499,23 @@ 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" }) + + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "%66oo=baz;foo=bar") + cookies = Rack::Utils.parse_cookies(env) + cookies.must_equal({ "%66oo" => "baz", "foo" => "bar" }) end it "adds new cookies to nil header" do @@ -538,48 +549,48 @@ end describe Rack::Utils, "byte_range" do it "ignore missing or syntactically invalid byte ranges" do - 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 + 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_be_nil - Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_be_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 @@ -612,7 +623,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 @@ -635,7 +646,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 @@ -664,16 +675,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 @@ -690,14 +701,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 6ab0a74ca2db406c12a44cb40fe2f026e827dc46..d4191aa41724bd0e5fae767718a0855b4818bd25 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 55899f84bb8cfe1498dbe71f12f1f8ce3c8e85fd..0d0aa8f7fabe81b4b6ecaa66875bdeba59ff29d9 100644 --- a/test/spec_webrick.rb +++ b/test/spec_webrick.rb @@ -1,4 +1,6 @@ -require 'minitest/autorun' +# frozen_string_literal: true + +require 'minitest/global_expectations/autorun' require 'rack/mock' require 'thread' require File.expand_path('../testrequest', __FILE__) @@ -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 @@ -124,17 +126,27 @@ describe Rack::Handler::WEBrick do 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 queue.push(server) } end 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 @@ -185,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"] ] }) @@ -199,8 +211,8 @@ describe Rack::Handler::WEBrick do 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 cacd23d5079b09cb6b8ebae8450ee05df451af8f..aabe7fa6b342ad068fdec7b7cec94271df1ebe00 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 3ca5a72c5d17df3bd2780f7a615dd886a0026d95..e98468cc6669fa05d1106ec6d5e5b09229e3ea74 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 2c2fae1709f2da529bd600aacd7263c70c13d3af..87c6c25431f90fa5a5128c14fcf178682be1509f 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.