diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..ee510310dfdab43538ef3d38d1257198f13274b3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..5ace4600a1f26e6892982f3e2f069ebfab108d87 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a25de591ba3c64f2a64205d89265dc3bb0e0244f --- /dev/null +++ b/.github/workflows/depsreview.yaml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v2 diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml deleted file mode 100644 index 0757016be374eed62379ca5819970b0ff73ecc98..0000000000000000000000000000000000000000 --- a/.github/workflows/development.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Development - -on: [push, pull_request] - -jobs: - test: - strategy: - fail-fast: false - matrix: - os: [ubuntu-20.04] - ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0', 3.1, 3.2] - runs-on: ${{matrix.os}} - steps: - - uses: actions/checkout@v2 - - - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{matrix.ruby}} - - - uses: actions/cache@v1 - with: - path: vendor/bundle - key: bundle-use-ruby-${{matrix.os}}-${{matrix.ruby}}-${{hashFiles('**/Gemfile')}} - restore-keys: | - bundle-use-ruby-${{matrix.os}}-${{matrix.ruby}}- - - - name: Installing packages - run: sudo apt-get install libfcgi-dev libmemcached-dev - - - name: Bundle install... - run: | - bundle config path vendor/bundle - bundle install - - - run: bundle exec rake diff --git a/.github/workflows/test-external.yaml b/.github/workflows/test-external.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d55882cea2e1078ac1e2283dde8db72d3c1691f1 --- /dev/null +++ b/.github/workflows/test-external.yaml @@ -0,0 +1,28 @@ +name: Test External + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + ruby: ['2.7', '3.0', '3.1'] + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v3 + + - uses: ruby/setup-ruby-pkgs@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + apt-get: _update_ libfcgi-dev libmemcached-dev + brew: fcgi libmemcached + + - run: bundle exec bake test:external diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..697b3cd275a2fe618a5ad6d7b73b2afef724ab65 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,38 @@ +name: Test + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + ruby: + - '2.4' + - '2.5' + - '2.6' + - '2.7' + - '3.0' + - '3.1' + - '3.2' + - jruby + - truffleruby-head + include: + - os: macos-latest + ruby: '3.1' + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v3 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.ruby}} + bundler-cache: true + + - run: bundle exec rake diff --git a/.rubocop.yml b/.rubocop.yml index ca9867670d647b8b73c5cb9ab1c7c86941948ab1..7188939dc382c008007b7fe1966f8f575c10a0bd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,8 @@ +require: + - rubocop-packaging + AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.4 DisabledByDefault: true Exclude: - '**/vendor/**/*' @@ -50,8 +53,11 @@ Layout/SpaceBeforeFirstArg: Layout/SpaceInsideHashLiteralBraces: Enabled: true -Layout/Tab: +Layout/IndentationStyle: Enabled: true Layout/TrailingWhitespace: Enabled: true + +Lint/DeprecatedOpenSSLConstant: + Enabled: true diff --git a/.yardopts b/.yardopts index f4d6aebae4cc1d7b85159bfcdf95421c287f55bc..06b7cada7a8d7b665714d1c3544eff601b351b10 100644 --- a/.yardopts +++ b/.yardopts @@ -1,2 +1,2 @@ - -SPEC +SPEC.rdoc diff --git a/CHANGELOG.md b/CHANGELOG.md index 85cb1fc2a65504b9ccaab168e05d3939182e0c46..09a7aa440a608bd99f3c425e59d0e9a4a7c6c8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,33 +2,148 @@ All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/). -## [2.2.6.4] - 2023-03-13 +## [3.0.7] - 2023-03-16 + +- Make query parameters without `=` have `nil` values. ([#2059](https://github.com/rack/rack/pull/2059), [@jeremyevans]) + +## [3.0.6.1] - 2023-03-13 - [CVE-2023-27539] Avoid ReDoS in header parsing -## [2.2.6.3] - 2023-03-02 +## [3.0.6] - 2023-03-13 -- [CVE-2023-27530] Introduce multipart_total_part_limit to limit total parts +- Add `QueryParser#missing_value` for handling missing values + tests. ([#2052](https://github.com/rack/rack/pull/2052), [@ioquatix]) -## [2.2.6.2] - 2022-01-17 +## [3.0.5] - 2023-03-13 -- [CVE-2022-44570] Fix ReDoS in Rack::Utils.get_byte_ranges +- Split form/query parsing into two steps. ([#2038](https://github.com/rack/rack/pull/2038), [@matthewd](https://github.com/matthewd)) + +## [3.0.4.1] - 2023-03-02 + +- [CVE-2023-27530] Introduce multipart_total_part_limit to limit total parts -## [2.2.6.1] - 2022-01-17 +## [3.0.4.1] - 2023-01-17 - [CVE-2022-44571] Fix ReDoS vulnerability in multipart parser +- [CVE-2022-44570] Fix ReDoS in Rack::Utils.get_byte_ranges - [CVE-2022-44572] Forbid control characters in attributes (also ReDoS) -## [2.2.6] - 2022-01-17 +## [3.0.4] - 2023-01-17 -- Extend `Rack::MethodOverride` to handle `QueryParser::ParamsTooDeepError` error. ([#2011](https://github.com/rack/rack/pull/2011), [@byroot](https://github.com/byroot)) +- `Rack::Request#POST` should consistently raise errors. Cache errors that occur when invoking `Rack::Request#POST` so they can be raised again later. ([#2010](https://github.com/rack/rack/pull/2010), [@ioquatix]) +- Fix `Rack::Lint` error message for `HTTP_CONTENT_TYPE` and `HTTP_CONTENT_LENGTH`. ([#2007](https://github.com/rack/rack/pull/2007), [@byroot](https://github.com/byroot)) +- Extend `Rack::MethodOverride` to handle `QueryParser::ParamsTooDeepError` error. ([#2006](https://github.com/rack/rack/pull/2006), [@byroot](https://github.com/byroot)) -## [2.2.5] - 2022-12-27 +## [3.0.3] - 2022-12-27 ### Fixed - `Rack::URLMap` uses non-deprecated form of `Regexp.new`. ([#1998](https://github.com/rack/rack/pull/1998), [@weizheheng](https://github.com/weizheheng)) +## [3.0.2] - 2022-12-05 + +### Fixed + +- `Utils.build_nested_query` URL-encodes nested field names including the square brackets. +- Allow `Rack::Response` to pass through streaming bodies. ([#1993](https://github.com/rack/rack/pull/1993), [@ioquatix]) + +## [3.0.1] - 2022-11-18 + +### Fixed + +- `MethodOverride` does not look for an override if a request does not include form/parseable data. +- `Rack::Lint::Wrapper` correctly handles `respond_to?` with `to_ary`, `each`, `call` and `to_path`, forwarding to the body. ([#1981](https://github.com/rack/rack/pull/1981), [@ioquatix]) + +## [3.0.0] - 2022-09-06 + +- No changes + +## [3.0.0.rc1] - 2022-09-04 + +### SPEC Changes + +- Stream argument must implement `<<` https://github.com/rack/rack/pull/1959 +- `close` may be called on `rack.input` https://github.com/rack/rack/pull/1956 +- `rack.response_finished` may be used for executing code after the response has been finished https://github.com/rack/rack/pull/1952 + +## [3.0.0.beta1] - 2022-08-08 + +### Security + +- Do not use semicolon as GET parameter separator. ([#1733](https://github.com/rack/rack/pull/1733), [@jeremyevans]) + +### SPEC Changes + +- Response array must now be non-frozen. +- Response `status` must now be an integer greater than or equal to 100. +- Response `headers` must now be an unfrozen hash. +- Response header keys can no longer include uppercase characters. +- Response header values can be an `Array` to handle multiple values (and no longer supports `\n` encoded headers). +- Response body can now respond to `#call` (streaming body) instead of `#each` (enumerable body), for the equivalent of response hijacking in previous versions. +- Middleware must no longer call `#each` on the body, but they can call `#to_ary` on the body if it responds to `#to_ary`. +- `rack.input` is no longer required to be rewindable. +- `rack.multithread`/`rack.multiprocess`/`rack.run_once`/`rack.version` are no longer required environment keys. +- `SERVER_PROTOCOL` is now a required environment key, matching the HTTP protocol used in the request. +- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional. +- `rack.hijack_io` has been removed completely. +- `rack.response_finished` is an optional environment key which contains an array of callable objects that must accept `#call(env, status, headers, error)` and are invoked after the response is finished (either successfully or unsuccessfully). +- It is okay to call `#close` on `rack.input` to indicate that you no longer need or care about the input. +- The stream argument supplied to the streaming body and hijack must support `#<<` for writing output. + +### Removed + +- Remove `rack.multithread`/`rack.multiprocess`/`rack.run_once`. These variables generally come too late to be useful. ([#1720](https://github.com/rack/rack/pull/1720), [@ioquatix], [@jeremyevans])) +- Remove deprecated Rack::Request::SCHEME_WHITELIST. ([@jeremyevans]) +- Remove internal cookie deletion using pattern matching, there are very few practical cases where it would be useful and browsers handle it correctly without us doing anything special. ([#1844](https://github.com/rack/rack/pull/1844), [@ioquatix]) +- Remove `rack.version` as it comes too late to be useful. ([#1938](https://github.com/rack/rack/pull/1938), [@ioquatix]) +- Extract `rackup` command, `Rack::Server`, `Rack::Handler` and related code into a separate gem. ([#1937](https://github.com/rack/rack/pull/1937), [@ioquatix]) + +### Added + +- `Rack::Headers` added to support lower-case header keys. ([@jeremyevans]) +- `Rack::Utils#set_cookie_header` now supports `escape_key: false` to avoid key escaping. ([@jeremyevans]) +- `Rack::RewindableInput` supports size. ([@ahorek](https://github.com/ahorek)) +- `Rack::RewindableInput::Middleware` added for making `rack.input` rewindable. ([@jeremyevans]) +- The RFC 7239 Forwarded header is now supported and considered by default when looking for information on forwarding, falling back to the X-Forwarded-* headers. `Rack::Request.forwarded_priority` accessor has been added for configuring the priority of which header to check. ([#1423](https://github.com/rack/rack/issues/1423), [@jeremyevans]) +- Allow response headers to contain array of values. ([#1598](https://github.com/rack/rack/issues/1598), [@ioquatix]) +- Support callable body for explicit streaming support and clarify streaming response body behaviour. ([#1745](https://github.com/rack/rack/pull/1745), [@ioquatix], [#1748](https://github.com/rack/rack/pull/1748), [@wjordan]) +- Allow `Rack::Builder#run` to take a block instead of an argument. ([#1942](https://github.com/rack/rack/pull/1942), [@ioquatix]) +- Add `rack.response_finished` to `Rack::Lint`. ([#1802](https://github.com/rack/rack/pull/1802), [@BlakeWilliams], [#1952](https://github.com/rack/rack/pull/1952), [@ioquatix]) +- The stream argument must implement `#<<`. ([#1959](https://github.com/rack/rack/pull/1959), [@ioquatix]) + +### Changed + +- BREAKING CHANGE: Require `status` to be an Integer. ([#1662](https://github.com/rack/rack/pull/1662), [@olleolleolle](https://github.com/olleolleolle)) +- BREAKING CHANGE: Query parsing now treats parameters without `=` as having the empty string value instead of nil value, to conform to the URL spec. ([#1696](https://github.com/rack/rack/issues/1696), [@jeremyevans]) +- Relax validations around `Rack::Request#host` and `Rack::Request#hostname`. ([#1606](https://github.com/rack/rack/issues/1606), [@pvande](https://github.com/pvande)) +- Removed antiquated handlers: FCGI, LSWS, SCGI, Thin. ([#1658](https://github.com/rack/rack/pull/1658), [@ioquatix]) +- Removed options from `Rack::Builder.parse_file` and `Rack::Builder.load_file`. ([#1663](https://github.com/rack/rack/pull/1663), [@ioquatix]) +- `Rack::HTTP_VERSION` has been removed and the `HTTP_VERSION` env setting is no longer set in the CGI and Webrick handlers. ([#970](https://github.com/rack/rack/issues/970), [@jeremyevans]) +- `Rack::Request#[]` and `#[]=` now warn even in non-verbose mode. ([#1277](https://github.com/rack/rack/issues/1277), [@jeremyevans]) +- Decrease default allowed parameter recursion level from 100 to 32. ([#1640](https://github.com/rack/rack/issues/1640), [@jeremyevans]) +- Attempting to parse a multipart response with an empty body now raises Rack::Multipart::EmptyContentError. ([#1603](https://github.com/rack/rack/issues/1603), [@jeremyevans]) +- `Rack::Utils.secure_compare` uses OpenSSL's faster implementation if available. ([#1711](https://github.com/rack/rack/pull/1711), [@bdewater](https://github.com/bdewater)) +- `Rack::Request#POST` now caches an empty hash if input content type is not parseable. ([#749](https://github.com/rack/rack/pull/749), [@jeremyevans]) +- BREAKING CHANGE: Updated `trusted_proxy?` to match full 127.0.0.0/8 network. ([#1781](https://github.com/rack/rack/pull/1781), [@snbloch](https://github.com/snbloch)) +- Explicitly deprecate `Rack::File` which was an alias for `Rack::Files`. ([#1811](https://github.com/rack/rack/pull/1720), [@ioquatix]). +- Moved `Rack::Session` into [separate gem](https://github.com/rack/rack-session). ([#1805](https://github.com/rack/rack/pull/1805), [@ioquatix]) +- `rackup -D` option to daemonizes no longer changes the working directory to the root. ([#1813](https://github.com/rack/rack/pull/1813), [@jeremyevans]) +- The `x-forwarded-proto` header is now considered before the `x-forwarded-scheme` header for determining the forwarded protocol. `Rack::Request.x_forwarded_proto_priority` accessor has been added for configuring the priority of which header to check. ([#1809](https://github.com/rack/rack/issues/1809), [@jeremyevans]) +- `Rack::Request.forwarded_authority` (and methods that call it, such as `host`) now returns the last authority in the forwarded header, instead of the first, as earlier forwarded authorities can be forged by clients. This restores the Rack 2.1 behavior. ([#1829](https://github.com/rack/rack/issues/1809), [@jeremyevans]) +- Use lower case cookie attributes when creating cookies, and fold cookie attributes to lower case when reading cookies (specifically impacting `secure` and `httponly` attributes). ([#1849](https://github.com/rack/rack/pull/1849), [@ioquatix]) +- The response array must now be mutable (non-frozen) so middleware can modify it without allocating a new Array,therefore reducing object allocations. ([#1887](https://github.com/rack/rack/pull/1887), [#1927](https://github.com/rack/rack/pull/1927), [@amatsuda], [@ioquatix]) +- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional. `rack.hijack_io` is no longer required/specified. ([#1939](https://github.com/rack/rack/pull/1939), [@ioquatix]) +- Allow calling close on `rack.input`. ([#1956](https://github.com/rack/rack/pull/1956), [@ioquatix]) + +### Fixed + +- Make Rack::MockResponse handle non-hash headers. ([#1629](https://github.com/rack/rack/issues/1629), [@jeremyevans]) +- TempfileReaper now deletes temp files if application raises an exception. ([#1679](https://github.com/rack/rack/issues/1679), [@jeremyevans]) +- Handle cookies with values that end in '=' ([#1645](https://github.com/rack/rack/pull/1645), [@lukaso](https://github.com/lukaso)) +- Make `Rack::NullLogger` respond to `#fatal!` [@jeremyevans]) +- Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm)) +- `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst)) + ## [2.2.4] - 2022-06-30 - Better support for lower case headers in `Rack::ETag` middleware. ([#1919](https://github.com/rack/rack/pull/1919), [@ioquatix](https://github.com/ioquatix)) @@ -36,23 +151,21 @@ All notable changes to this project will be documented in this file. For info on ## [2.2.3.1] - 2022-05-27 -### Security - - [CVE-2022-30123] Fix shell escaping issue in Common Logger - [CVE-2022-30122] Restrict parsing of broken MIME attachments -## [2.2.3] - 2020-02-11 +## [2.2.3] - 2020-06-15 ### Security -- [CVE-2020-8184] Only decode cookie values +- [[CVE-2020-8184](https://nvd.nist.gov/vuln/detail/CVE-2020-8184)] Do not allow percent-encoded cookie name to override existing cookie names. BREAKING CHANGE: Accessing cookie names that require URL encoding with decoded name no longer works. ([@fletchto99](https://github.com/fletchto99)) ## [2.2.2] - 2020-02-11 ### Fixed -- Fix incorrect `Rack::Request#host` value. ([#1591](https://github.com/rack/rack/pull/1591), [@ioquatix](https://github.com/ioquatix)) -- Revert `Rack::Handler::Thin` implementation. ([#1583](https://github.com/rack/rack/pull/1583), [@jeremyevans](https://github.com/jeremyevans)) +- Fix incorrect `Rack::Request#host` value. ([#1591](https://github.com/rack/rack/pull/1591), [@ioquatix]) +- Revert `Rack::Handler::Thin` implementation. ([#1583](https://github.com/rack/rack/pull/1583), [@jeremyevans]) - Double assignment is still needed to prevent an "unused variable" warning. ([#1589](https://github.com/rack/rack/pull/1589), [@kamipo](https://github.com/kamipo)) - Fix to handle same_site option for session pool. ([#1587](https://github.com/rack/rack/pull/1587), [@kamipo](https://github.com/kamipo)) @@ -60,105 +173,111 @@ All notable changes to this project will be documented in this file. For info on ### Fixed -- Rework `Rack::Request#ip` to handle empty `forwarded_for`. ([#1577](https://github.com/rack/rack/pull/1577), [@ioquatix](https://github.com/ioquatix)) +- Rework `Rack::Request#ip` to handle empty `forwarded_for`. ([#1577](https://github.com/rack/rack/pull/1577), [@ioquatix]) ## [2.2.0] - 2020-02-08 ### SPEC Changes -- `rack.session` request environment entry must respond to `to_hash` and return unfrozen Hash. ([@jeremyevans](https://github.com/jeremyevans)) -- Request environment cannot be frozen. ([@jeremyevans](https://github.com/jeremyevans)) -- CGI values in the request environment with non-ASCII characters must use ASCII-8BIT encoding. ([@jeremyevans](https://github.com/jeremyevans)) -- Improve SPEC/lint relating to SERVER_NAME, SERVER_PORT and HTTP_HOST. ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix](https://github.com/ioquatix)) +- `rack.session` request environment entry must respond to `to_hash` and return unfrozen Hash. ([@jeremyevans]) +- Request environment cannot be frozen. ([@jeremyevans]) +- CGI values in the request environment with non-ASCII characters must use ASCII-8BIT encoding. ([@jeremyevans]) +- Improve SPEC/lint relating to SERVER_NAME, SERVER_PORT and HTTP_HOST. ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix]) ### Added -- `rackup` supports multiple `-r` options and will require all arguments. ([@jeremyevans](https://github.com/jeremyevans)) +- `rackup` supports multiple `-r` options and will require all arguments. ([@jeremyevans]) - `Server` supports an array of paths to require for the `:require` option. ([@khotta](https://github.com/khotta)) - `Files` supports multipart range requests. ([@fatkodima](https://github.com/fatkodima)) -- `Multipart::UploadedFile` supports an IO-like object instead of using the filesystem, using `:filename` and `:io` options. ([@jeremyevans](https://github.com/jeremyevans)) -- `Multipart::UploadedFile` supports keyword arguments `:path`, `:content_type`, and `:binary` in addition to positional arguments. ([@jeremyevans](https://github.com/jeremyevans)) -- `Static` supports a `:cascade` option for calling the app if there is no matching file. ([@jeremyevans](https://github.com/jeremyevans)) -- `Session::Abstract::SessionHash#dig`. ([@jeremyevans](https://github.com/jeremyevans)) -- `Response.[]` and `MockResponse.[]` for creating instances using status, headers, and body. ([@ioquatix](https://github.com/ioquatix)) -- Convenient cache and content type methods for `Rack::Response`. ([#1555](https://github.com/rack/rack/pull/1555), [@ioquatix](https://github.com/ioquatix)) +- `Multipart::UploadedFile` supports an IO-like object instead of using the filesystem, using `:filename` and `:io` options. ([@jeremyevans]) +- `Multipart::UploadedFile` supports keyword arguments `:path`, `:content_type`, and `:binary` in addition to positional arguments. ([@jeremyevans]) +- `Static` supports a `:cascade` option for calling the app if there is no matching file. ([@jeremyevans]) +- `Session::Abstract::SessionHash#dig`. ([@jeremyevans]) +- `Response.[]` and `MockResponse.[]` for creating instances using status, headers, and body. ([@ioquatix]) +- Convenient cache and content type methods for `Rack::Response`. ([#1555](https://github.com/rack/rack/pull/1555), [@ioquatix]) ### Changed -- `Request#params` no longer rescues EOFError. ([@jeremyevans](https://github.com/jeremyevans)) -- `Directory` uses a streaming approach, significantly improving time to first byte for large directories. ([@jeremyevans](https://github.com/jeremyevans)) -- `Directory` no longer includes a Parent directory link in the root directory index. ([@jeremyevans](https://github.com/jeremyevans)) -- `QueryParser#parse_nested_query` uses original backtrace when reraising exception with new class. ([@jeremyevans](https://github.com/jeremyevans)) -- `ConditionalGet` follows RFC 7232 precedence if both If-None-Match and If-Modified-Since headers are provided. ([@jeremyevans](https://github.com/jeremyevans)) +- `Request#params` no longer rescues EOFError. ([@jeremyevans]) +- `Directory` uses a streaming approach, significantly improving time to first byte for large directories. ([@jeremyevans]) +- `Directory` no longer includes a Parent directory link in the root directory index. ([@jeremyevans]) +- `QueryParser#parse_nested_query` uses original backtrace when reraising exception with new class. ([@jeremyevans]) +- `ConditionalGet` follows RFC 7232 precedence if both If-None-Match and If-Modified-Since headers are provided. ([@jeremyevans]) - `.ru` files supports the `frozen-string-literal` magic comment. ([@eregon](https://github.com/eregon)) -- Rely on autoload to load constants instead of requiring internal files, make sure to require 'rack' and not just 'rack/...'. ([@jeremyevans](https://github.com/jeremyevans)) -- `Etag` will continue sending ETag even if the response should not be cached. ([@henm](https://github.com/henm)) +- Rely on autoload to load constants instead of requiring internal files, make sure to require 'rack' and not just 'rack/...'. ([@jeremyevans]) +- BREAKING CHANGE: `Etag` will continue sending ETag even if the response should not be cached. Streaming no longer works without a workaround, see [#1619](https://github.com/rack/rack/issues/1619#issuecomment-848460528). ([@henm](https://github.com/henm)) - `Request#host_with_port` no longer includes a colon for a missing or empty port. ([@AlexWayfer](https://github.com/AlexWayfer)) -- All handlers uses keywords arguments instead of an options hash argument. ([@ioquatix](https://github.com/ioquatix)) -- `Files` handling of range requests no longer return a body that supports `to_path`, to ensure range requests are handled correctly. ([@jeremyevans](https://github.com/jeremyevans)) -- `Multipart::Generator` only includes `Content-Length` for files with paths, and `Content-Disposition` `filename` if the `UploadedFile` instance has one. ([@jeremyevans](https://github.com/jeremyevans)) -- `Request#ssl?` is true for the `wss` scheme (secure websockets). ([@jeremyevans](https://github.com/jeremyevans)) -- `Rack::HeaderHash` is memoized by default. ([#1549](https://github.com/rack/rack/pull/1549), [@ioquatix](https://github.com/ioquatix)) +- All handlers uses keywords arguments instead of an options hash argument. ([@ioquatix]) +- `Files` handling of range requests no longer return a body that supports `to_path`, to ensure range requests are handled correctly. ([@jeremyevans]) +- `Multipart::Generator` only includes `Content-Length` for files with paths, and `Content-Disposition` `filename` if the `UploadedFile` instance has one. ([@jeremyevans]) +- `Request#ssl?` is true for the `wss` scheme (secure websockets). ([@jeremyevans]) +- `Rack::HeaderHash` is memoized by default. ([#1549](https://github.com/rack/rack/pull/1549), [@ioquatix]) - `Rack::Directory` allow directory traversal inside root directory. ([#1417](https://github.com/rack/rack/pull/1417), [@ThomasSevestre](https://github.com/ThomasSevestre)) -- Sort encodings by server preference. ([#1184](https://github.com/rack/rack/pull/1184), [@ioquatix](https://github.com/ioquatix), [@wjordan](https://github.com/wjordan)) -- Rework host/hostname/authority implementation in `Rack::Request`. `#host` and `#host_with_port` have been changed to correctly return IPv6 addresses formatted with square brackets, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix](https://github.com/ioquatix)) -- `Rack::Builder` parsing options on first `#\` line is deprecated. ([#1574](https://github.com/rack/rack/pull/1574), [@ioquatix](https://github.com/ioquatix)) +- Sort encodings by server preference. ([#1184](https://github.com/rack/rack/pull/1184), [@ioquatix], [@wjordan](https://github.com/wjordan)) +- Rework host/hostname/authority implementation in `Rack::Request`. `#host` and `#host_with_port` have been changed to correctly return IPv6 addresses formatted with square brackets, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix]) +- `Rack::Builder` parsing options on first `#\` line is deprecated. ([#1574](https://github.com/rack/rack/pull/1574), [@ioquatix]) ### Removed -- `Directory#path` as it was not used and always returned nil. ([@jeremyevans](https://github.com/jeremyevans)) -- `BodyProxy#each` as it was only needed to work around a bug in Ruby <1.9.3. ([@jeremyevans](https://github.com/jeremyevans)) +- `Directory#path` as it was not used and always returned nil. ([@jeremyevans]) +- `BodyProxy#each` as it was only needed to work around a bug in Ruby <1.9.3. ([@jeremyevans]) - `URLMap::INFINITY` and `URLMap::NEGATIVE_INFINITY`, in favor of `Float::INFINITY`. ([@ch1c0t](https://github.com/ch1c0t)) - Deprecation of `Rack::File`. It will be deprecated again in rack 2.2 or 3.0. ([@rafaelfranca](https://github.com/rafaelfranca)) -- Support for Ruby 2.2 as it is well past EOL. ([@ioquatix](https://github.com/ioquatix)) -- Remove `Rack::Files#response_body` as the implementation was broken. ([#1153](https://github.com/rack/rack/pull/1153), [@ioquatix](https://github.com/ioquatix)) -- Remove `SERVER_ADDR` which was never part of the original SPEC. ([#1573](https://github.com/rack/rack/pull/1573), [@ioquatix](https://github.com/ioquatix)) +- Support for Ruby 2.2 as it is well past EOL. ([@ioquatix]) +- Remove `Rack::Files#response_body` as the implementation was broken. ([#1153](https://github.com/rack/rack/pull/1153), [@ioquatix]) +- Remove `SERVER_ADDR` which was never part of the original SPEC. ([#1573](https://github.com/rack/rack/pull/1573), [@ioquatix]) ### Fixed -- `Directory` correctly handles root paths containing glob metacharacters. ([@jeremyevans](https://github.com/jeremyevans)) -- `Cascade` uses a new response object for each call if initialized with no apps. ([@jeremyevans](https://github.com/jeremyevans)) -- `BodyProxy` correctly delegates keyword arguments to the body object on Ruby 2.7+. ([@jeremyevans](https://github.com/jeremyevans)) -- `BodyProxy#method` correctly handles methods delegated to the body object. ([@jeremyevans](https://github.com/jeremyevans)) +- `Directory` correctly handles root paths containing glob metacharacters. ([@jeremyevans]) +- `Cascade` uses a new response object for each call if initialized with no apps. ([@jeremyevans]) +- `BodyProxy` correctly delegates keyword arguments to the body object on Ruby 2.7+. ([@jeremyevans]) +- `BodyProxy#method` correctly handles methods delegated to the body object. ([@jeremyevans]) - `Request#host` and `Request#host_with_port` handle IPv6 addresses correctly. ([@AlexWayfer](https://github.com/AlexWayfer)) -- `Lint` checks when response hijacking that `rack.hijack` is called with a valid object. ([@jeremyevans](https://github.com/jeremyevans)) -- `Response#write` correctly updates `Content-Length` if initialized with a body. ([@jeremyevans](https://github.com/jeremyevans)) +- `Lint` checks when response hijacking that `rack.hijack` is called with a valid object. ([@jeremyevans]) +- `Response#write` correctly updates `Content-Length` if initialized with a body. ([@jeremyevans]) - `CommonLogger` includes `SCRIPT_NAME` when logging. ([@Erol](https://github.com/Erol)) -- `Utils.parse_nested_query` correctly handles empty queries, using an empty instance of the params class instead of a hash. ([@jeremyevans](https://github.com/jeremyevans)) +- `Utils.parse_nested_query` correctly handles empty queries, using an empty instance of the params class instead of a hash. ([@jeremyevans]) - `Directory` correctly escapes paths in links. ([@yous](https://github.com/yous)) -- `Request#delete_cookie` and related `Utils` methods handle `:domain` and `:path` options in same call. ([@jeremyevans](https://github.com/jeremyevans)) -- `Request#delete_cookie` and related `Utils` methods do an exact match on `:domain` and `:path` options. ([@jeremyevans](https://github.com/jeremyevans)) +- `Request#delete_cookie` and related `Utils` methods handle `:domain` and `:path` options in same call. ([@jeremyevans]) +- `Request#delete_cookie` and related `Utils` methods do an exact match on `:domain` and `:path` options. ([@jeremyevans]) - `Static` no longer adds headers when a gzipped file request has a 304 response. ([@chooh](https://github.com/chooh)) -- `ContentLength` sets `Content-Length` response header even for bodies not responding to `to_ary`. ([@jeremyevans](https://github.com/jeremyevans)) -- Thin handler supports options passed directly to `Thin::Controllers::Controller`. ([@jeremyevans](https://github.com/jeremyevans)) -- WEBrick handler no longer ignores `:BindAddress` option. ([@jeremyevans](https://github.com/jeremyevans)) -- `ShowExceptions` handles invalid POST data. ([@jeremyevans](https://github.com/jeremyevans)) -- Basic authentication requires a password, even if the password is empty. ([@jeremyevans](https://github.com/jeremyevans)) -- `Lint` checks response is array with 3 elements, per SPEC. ([@jeremyevans](https://github.com/jeremyevans)) +- `ContentLength` sets `Content-Length` response header even for bodies not responding to `to_ary`. ([@jeremyevans]) +- Thin handler supports options passed directly to `Thin::Controllers::Controller`. ([@jeremyevans]) +- WEBrick handler no longer ignores `:BindAddress` option. ([@jeremyevans]) +- `ShowExceptions` handles invalid POST data. ([@jeremyevans]) +- Basic authentication requires a password, even if the password is empty. ([@jeremyevans]) +- `Lint` checks response is array with 3 elements, per SPEC. ([@jeremyevans]) - Support for using `:SSLEnable` option when using WEBrick handler. (Gregor Melhorn) -- Close response body after buffering it when buffering. ([@ioquatix](https://github.com/ioquatix)) +- Close response body after buffering it when buffering. ([@ioquatix]) - Only accept `;` as delimiter when parsing cookies. ([@mrageh](https://github.com/mrageh)) -- `Utils::HeaderHash#clear` clears the name mapping as well. ([@raxoft](https://github.com/raxoft)) -- Support for passing `nil` `Rack::Files.new`, which notably fixes Rails' current `ActiveStorage::FileServer` implementation. ([@ioquatix](https://github.com/ioquatix)) +- `Utils::HeaderHash#clear` clears the name mapping as well. ([@raxoft](https://github.com/raxoft)) +- Support for passing `nil` `Rack::Files.new`, which notably fixes Rails' current `ActiveStorage::FileServer` implementation. ([@ioquatix]) ### Documentation - CHANGELOG updates. ([@aupajo](https://github.com/aupajo)) - Added [CONTRIBUTING](CONTRIBUTING.md). ([@dblock](https://github.com/dblock)) +## [2.0.9] - 2020-02-08 + +- Handle case where session id key is requested but missing ([@jeremyevans]) +- Restore support for code relying on `SessionId#to_s`. ([@jeremyevans]) +- Add support for `SameSite=None` cookie value. ([@hennikul](https://github.com/hennikul)) + ## [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)) +- Skip deflating in Rack::Deflater if Content-Length is 0 ([@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)) +- Handle case where session id key is requested but missing ([@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)) -- Restore support for code relying on `SessionId#to_s`. ([@jeremyevans](https://github.com/jeremyevans)) +- Remove `Rack::Chunked` from `Rack::Server` default middleware. ([#1475](https://github.com/rack/rack/pull/1475), [@ioquatix]) +- Restore support for code relying on `SessionId#to_s`. ([@jeremyevans]) ## [2.1.0] - 2020-01-10 @@ -175,13 +294,13 @@ All notable changes to this project will be documented in this file. For info on - 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 `Builder#freeze_app` to freeze application and all middleware instances. ([@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)) +- Don't propagate nil values from middleware. ([@ioquatix]) +- Lazily initialize the response body and only buffer it if required. ([@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)) @@ -194,15 +313,15 @@ All notable changes to this project will be documented in this file. For info on - 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)) +- Add Falcon to the default handler fallbacks. ([@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)) +- Rename `Rack::File` -> `Rack::Files` and add deprecation notice. ([@postmodern](https://github.com/postmodern)) +- Prefer Base64 “strict encoding†for Base64 cookies. ([@ioquatix]) ### Removed -- Remove `to_ary` from Response ([@tenderlove](https://github.com/tenderlove)) +- BREAKING CHANGE: 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 @@ -283,7 +402,7 @@ All notable changes to this project will be documented in this file. For info on ### Added - Allow `Session::Abstract::SessionHash#fetch` to accept a block with a default value. ([@yannvanhalewyn](https://github.com/yannvanhalewyn)) -- Add `Builder#freeze_app` to freeze application and all middleware. ([@jeremyevans](https://github.com/jeremyevans)) +- Add `Builder#freeze_app` to freeze application and all middleware. ([@jeremyevans]) ### Changed @@ -295,9 +414,9 @@ All notable changes to this project will be documented in this file. For info on ### Fixed - Handle `NULL` bytes in multipart filenames. ([@casperisfine](https://github.com/casperisfine)) -- Remove warnings due to miscapitalized global. ([@ioquatix](https://github.com/ioquatix)) +- Remove warnings due to miscapitalized global. ([@ioquatix]) - Prevent exceptions caused by a race condition on multi-threaded servers. ([@sophiedeziel](https://github.com/sophiedeziel)) -- Add RDoc as an explicit depencency for `doc` group. ([@tonytonyjan](https://github.com/tonytonyjan)) +- Add RDoc as an explicit dependency for `doc` group. ([@tonytonyjan](https://github.com/tonytonyjan)) - Record errors originating from `Multipart::Parser` in the `MethodOverride` middleware instead of letting them bubble up. ([@carlzulauf](https://github.com/carlzulauf)) - Remove remaining use of removed `Utils#bytesize` method from the `File` middleware. ([@brauliomartinezlm](https://github.com/brauliomartinezlm)) @@ -615,7 +734,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md - 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 change to document rack.input binary mode in greater detail - SPEC define optional rack.logger specification - File servers support X-Cascade header - Imported Config middleware @@ -733,3 +852,9 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md - Removed Rails adapter, was too alpha. ## [0.1] 2007-03-03 + +[@ioquatix]: https://github.com/ioquatix "Samuel Williams" +[@jeremyevans]: https://github.com/jeremyevans "Jeremy Evans" +[@amatsuda]: https://github.com/amatsuda "Akira Matsuda" +[@wjordan]: https://github.com/wjordan "Will Jordan" +[@BlakeWilliams]: https://github.com/BlakeWilliams "Blake Williams" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70a27468e5f8ad03c5e7c036cad139c06c2a70b8..bd5a51207583e9c176e841fa75fa3fcb25663613 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,29 +1,33 @@ -Contributing to Rack -===================== +# Contributing to Rack -Rack is work of [hundreds of contributors](https://github.com/rack/rack/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rack/rack/pulls), [propose features and discuss issues](https://github.com/rack/rack/issues). When in doubt, post to the [rack-devel](http://groups.google.com/group/rack-devel) mailing list. +Rack is work of [hundreds of +contributors](https://github.com/rack/rack/graphs/contributors). You're +encouraged to submit [pull requests](https://github.com/rack/rack/pulls) and +[propose features and discuss issues](https://github.com/rack/rack/issues). -#### Fork the Project +## Fork the Project -Fork the [project on Github](https://github.com/rack/rack) and check out your copy. +Fork the [project on GitHub](https://github.com/rack/rack) and check out your +copy. ``` -git clone https://github.com/contributor/rack.git +git clone https://github.com/(your-github-username)/rack.git cd rack git remote add upstream https://github.com/rack/rack.git ``` -#### Create a Topic Branch +## Create a Topic Branch -Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. +Make sure your fork is up-to-date and create a topic branch for your feature or +bug fix. ``` -git checkout master -git pull upstream master +git checkout main +git pull upstream main git checkout -b my-feature-branch ``` -#### Bundle Install and Quick Test +## Bundle Install and Quick Test Ensure that you can build the project and run quick tests. @@ -32,7 +36,7 @@ bundle install --without extra bundle exec rake test ``` -#### Running All Tests +## Running All Tests Install all dependencies. @@ -46,39 +50,33 @@ Run all tests. rake test ``` -The test suite has no dependencies outside of the core Ruby installation and bacon. +## Write Tests -Some tests will be skipped if a dependency is not found. +Try to write a test that reproduces the problem you're trying to fix or +describes a feature that you want to build. -To run the test suite completely, you need: +We definitely appreciate pull requests that highlight or reproduce a problem, +even without a fix. - * fcgi - * dalli - * thin - -To test Memcache sessions, you need memcached (will be run on port 11211) and dalli installed. - -#### Write Tests - -Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. - -We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. - -#### Write Code +## Write Code Implement your feature or bug fix. -Make sure that `bundle exec rake fulltest` completes without errors. +Make sure that all tests pass: + +``` +bundle exec rake test +``` -#### Write Documentation +## Write Documentation -Document any external behavior in the [README](README.rdoc). +Document any external behavior in the [README](README.md). -#### Update Changelog +## Update Changelog Add a line to [CHANGELOG](CHANGELOG.md). -#### Commit Changes +## Commit Changes Make sure git knows your name and email address: @@ -87,34 +85,37 @@ git config --global user.name "Your Name" git config --global user.email "contributor@example.com" ``` -Writing good commit logs is important. A commit log should describe what changed and why. +Writing good commit logs is important. A commit log should describe what changed +and why. ``` git add ... git commit ``` -#### Push +## Push ``` git push origin my-feature-branch ``` -#### Make a Pull Request +## Make a Pull Request -Go to https://github.com/contributor/rack and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. +Go to your fork of rack on GitHub and select your feature branch. Click the +'Pull Request' button and fill out the form. Pull requests are usually +reviewed within a few days. -#### Rebase +## Rebase -If you've been working on a change for a while, rebase with upstream/master. +If you've been working on a change for a while, rebase with upstream/main. ``` git fetch upstream -git rebase upstream/master +git rebase upstream/main git push origin my-feature-branch -f ``` -#### Make Required Changes +## Make Required Changes Amend your previous commit and force push the changes. @@ -123,14 +124,19 @@ git commit --amend git push origin my-feature-branch -f ``` -#### Check on Your Pull Request +## Check on Your Pull Request -Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. +Go back to your pull request after a few minutes and see whether it passed +tests with GitHub Actions. Everything should look green, otherwise fix issues and +amend your commit as described above. -#### Be Patient +## Be Patient -It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! +It's likely that your change will not be merged and that the nitpicky +maintainers will ask you to do more, or fix seemingly benign problems. Hang in +there! -#### Thank You +## Thank You -Please do know that we really appreciate and value your time and work. We love you, really. +Please do know that we really appreciate and value your time and work. We love +you, really. diff --git a/Gemfile b/Gemfile index b6ce15e4b96a12cbb8b0424361af42a2ff55b7eb..2b58d070a6e5d00fd90eeb17c9abf168f856d115 100644 --- a/Gemfile +++ b/Gemfile @@ -4,31 +4,18 @@ 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 -# available, 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", require: false +gem "webrick" -group :test do - gem "webrick" # gemified in Ruby 3.1+ - gem "psych" -end - -# 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 +group :maintenance, optional: true do + gem "rubocop", require: false + gem "rubocop-packaging", require: false end group :doc do gem 'rdoc' end + +group :test do + gem 'minitest' + gem 'bake-test-external', '~> 0.1.3' +end diff --git a/MIT-LICENSE b/MIT-LICENSE index 703d118f9a293d561fd731038ffd994777c19ed2..fb33b7fee10e84cef0e3278583b19fc940f0daf6 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html> +Copyright (C) 2007-2021 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2d26ab44f1378eb004288b7e3b6a0c3dedf543f6 --- /dev/null +++ b/README.md @@ -0,0 +1,309 @@ +#  + +> **_NOTE:_** Rack v3.0.0 was recently released. Please check the [Upgrade +> Guide](UPGRADE-GUIDE.md) for more details about migrating your existing +> servers, middlewares and applications. For detailed information on specific +> changes, check the [Change Log](CHANGELOG.md). + +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 bridge between web servers, web +frameworks, and web application into a single method call. + +The exact details of this are described in the [Rack Specification], which all +Rack applications should conform to. + +## Installation + +Add the rack gem to your application bundle, or follow the instructions provided +by a [supported web framework](#supported-web-frameworks): + +```bash +# Install it generally: +$ gem install rack --pre + +# or, add it to your current application gemfile: +$ bundle add rack --version 3.0.0 +``` + +If you need features from `Rack::Session` or `bin/rackup` please add those gems separately. + +```bash +$ gem install rack-session rackup +``` + +## Usage + +Create a file called `config.ru` with the following contents: + +```ruby +run do |env| + [200, {}, ["Hello World"]] +end +``` + +Run this using the rackup gem or another [supported web +server](#supported-web-servers). + +```bash +$ gem install rackup +$ rackup +$ curl http://localhost:9292 +Hello World +``` + +## Supported web servers + +Rack is supported by a wide range of servers, including: + +* [Agoo](https://github.com/ohler55/agoo) +* [Falcon](https://github.com/socketry/falcon) **(Rack 3 Compatible)** +* [Iodine](https://github.com/boazsegev/iodine) +* [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/) +* [Thin](https://github.com/macournoyer/thin) +* [Unicorn](https://yhbt.net/unicorn/) +* [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) +* [Lamby](https://lamby.custominktech.com) (for AWS Lambda) + +You will need to consult the server documentation to find out what features and +limitations they may have. In general, any valid Rack app will run the same on +all these servers, without changing anything. + +### Rackup + +Rack provides a separate gem, [rackup](https://github.com/rack/rackup) which is +a generic interface for running a Rack application on supported servers, which +include `WEBRick`, `Puma`, `Falcon` and others. + +## Supported web frameworks + +These frameworks and many others support the [Rack Specification]: + +* [Camping](https://github.com/camping/camping) +* [Hanami](https://hanamirb.org/) +* [Padrino](https://padrinorb.com/) +* [Roda](https://github.com/jeremyevans/roda) **(Rack 3 Compatible)** +* [Ruby on Rails](https://rubyonrails.org/) +* [Sinatra](https://sinatrarb.com/) +* [Utopia](https://github.com/socketry/utopia) **(Rack 3 Compatible)** +* [WABuR](https://github.com/ohler55/wabur) + +### Older (possibly unsupported) web frameworks + +* [Ramaze](http://ramaze.net/) +* [Rum](https://github.com/leahneukirchen/rum) + +## Available middleware shipped with Rack + +Between the server and the framework, Rack can be customized to your +applications needs using middleware. Rack itself ships with the following +middleware: + +* `Rack::CommonLogger` for creating Apache-style logfiles. +* `Rack::ConditionalGet` for returning [Not + Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) + responses when the response has not changed. +* `Rack::Config` for modifying the environment before processing the request. +* `Rack::ContentLength` for setting a `content-length` header based on body + size. +* `Rack::ContentType` for setting a default `content-type` header for responses. +* `Rack::Deflater` for compressing responses with gzip. +* `Rack::ETag` for setting `etag` header on bodies that can be buffered. +* `Rack::Events` for providing easy hooks when a request is received and when + the response is sent. +* `Rack::Files` for serving static files. +* `Rack::Head` for returning an empty body for HEAD requests. +* `Rack::Lint` for checking conformance to the [Rack Specification]. +* `Rack::Lock` for serializing requests using a mutex. +* `Rack::Logger` for setting a logger to handle logging errors. +* `Rack::MethodOverride` for modifying the request method based on a submitted + parameter. +* `Rack::Recursive` for including data from other paths in the application, and + for performing internal redirects. +* `Rack::Reloader` for reloading files if they have been modified. +* `Rack::Runtime` for including a response header with the time taken to process + the request. +* `Rack::Sendfile` for working with web servers that can use optimized file + serving for file system paths. +* `Rack::ShowException` for catching unhandled exceptions and presenting them in + a nice and helpful way with clickable backtrace. +* `Rack::ShowStatus` for using nice error pages for empty client error + responses. +* `Rack::Static` for more configurable serving of static files. +* `Rack::TempfileReaper` for removing temporary files creating during a request. + +All these components use the same interface, which is described in detail in the +[Rack Specification]. These optional components can be used in any way you wish. + +### Convenience interfaces + +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 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. +* `Rack::Cascade` for trying additional Rack applications if an application + returns a not found or method not supported response. +* `Rack::Directory` for serving files under a given directory, with directory + indexes. +* `Rack::MediaType` for parsing content-type headers. +* `Rack::Mime` for determining content-type based on file extension. +* `Rack::RewindableInput` for making any IO object rewindable, using a temporary + file buffer. +* `Rack::URLMap` to route to multiple applications inside the same process. + +## Configuration + +Rack exposes several configuration parameters to control various features of the +implementation. + +### `param_depth_limit` + +```ruby +Rack::Utils.param_depth_limit = 32 # default +``` + +The maximum amount of nesting allowed in parameters. For example, if set to 3, +this query string would be allowed: + +``` +?a[b][c]=d +``` + +but this query string would not be allowed: + +``` +?a[b][c][d]=e +``` + +Limiting the depth prevents a possible stack overflow when parsing parameters. + +### `multipart_file_limit` + +```ruby +Rack::Utils.multipart_file_limit = 128 # default +``` + +The maximum number of parts with a filename a request can contain. Accepting +too many parts can lead to the server running out of file handles. + +The default is 128, which means that a single request can't upload more than 128 +files at once. Set to 0 for no limit. + +Can also be set via the `RACK_MULTIPART_FILE_LIMIT` environment variable. + +(This is also aliased as `multipart_part_limit` and `RACK_MULTIPART_PART_LIMIT` for compatibility) + + +### `multipart_total_part_limit` + +The maximum total number of parts a request can contain of any type, including +both file and non-file form fields. + +The default is 4096, which means that a single request can't contain more than +4096 parts. + +Set to 0 for no limit. + +Can also be set via the `RACK_MULTIPART_TOTAL_PART_LIMIT` environment variable. + + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for specific details about how to make a +contribution to Rack. + +Please post bugs, suggestions and patches to [GitHub +Issues](https://github.com/rack/rack/issues). + +Please check our [Security Policy](https://github.com/rack/rack/security/policy) +for responsible disclosure and security bug reporting process. 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. + +## See Also + +### `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. + +* https://github.com/rack/rack-contrib + +### `rack-session` + +Provides convenient session management for Rack. + +* https://github.com/rack/rack-session + +## Thanks + +The Rack Core Team, consisting of + +* Aaron Patterson [tenderlove](https://github.com/tenderlove) +* Samuel Williams [ioquatix](https://github.com/ioquatix) +* Jeremy Evans [jeremyevans](https://github.com/jeremyevans) +* Eileen Uchitelle [eileencodes](https://github.com/eileencodes) +* Matthew Draper [matthewd](https://github.com/matthewd) +* Rafael França [rafaelfranca](https://github.com/rafaelfranca) + +and the Rack Alumni + +* Ryan Tomayko [rtomayko](https://github.com/rtomayko) +* Scytrin dai Kinthra [scytrin](https://github.com/scytrin) +* 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) +* Michael Fellinger [manveru](https://github.com/manveru) +* Santiago Pastorino [spastorino](https://github.com/spastorino) +* Konstantin Haase [rkh](https://github.com/rkh) + +would like to thank: + +* Adrian Madrid, for the LiteSpeed handler. +* Christoffer Sawicki, for the first Rails adapter and `Rack::Deflater`. +* Tim Fletcher, for the HTTP authentication code. +* Luc Heinrich for the Cookie sessions, the static file handler and bugfixes. +* Armin Ronacher, for the logo and racktools. +* Alex Beregszaszi, Alexander Kahn, Anil Wadghule, Aredridel, Ben Alpert, Dan + Kubb, Daniel Roethlisberger, Matt Todd, Tom Robinson, Phil Hagelberg, S. Brent + Faulkner, Bosko Milekic, Daniel RodrÃguez Troitiño, Genki Takiuchi, Geoffrey + Grosenbach, Julien Sanchez, Kamal Fariz Mahyuddin, Masayoshi Takahashi, + Patrick Aljordm, Mig, Kazuhiro Nishiyama, Jon Bardin, Konstantin Haase, Larry + Siden, Matias Korhonen, Sam Ruby, Simon Chiang, Tim Connor, Timur Batyrshin, + and Zach Brock for bug fixing and other improvements. +* Eric Wong, Hongli Lai, Jeremy Kemper for their continuous support and API + improvements. +* Yehuda Katz and Carl Lerche for refactoring rackup. +* Brian Candler, for `Rack::ContentType`. +* Graham Batty, for improved handler loading. +* Stephen Bannasch, for bug reports and documentation. +* Gary Wright, for proposing a better `Rack::Response` interface. +* Jonathan Buch, for improvements regarding `Rack::Response`. +* Armin Röhrl, for tracking down bugs in the Cookie generator. +* 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. +* All bug reporters and patch contributors not mentioned above. + +## License + +Rack is released under the [MIT License](MIT-LICENSE). + +[Rack Specification]: SPEC.rdoc diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index cbb257239a3cb236a148034a2044a457ee7b0055..0000000000000000000000000000000000000000 --- a/README.rdoc +++ /dev/null @@ -1,320 +0,0 @@ -= \Rack, a modular Ruby webserver interface - -{<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] -{<img src="http://inch-ci.org/github/rack/rack.svg?branch=master" alt="Inline docs" />}[http://inch-ci.org/github/rack/rack] - -\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. - -== Supported web servers - -The included *handlers* connect all kinds of web servers to \Rack: - -* WEBrick[https://github.com/ruby/webrick] -* FCGI -* CGI -* SCGI -* 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] -* Iodine[https://github.com/boazsegev/iodine] -* {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://yhbt.net/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 and many others support the \Rack API: - -* Camping[http://www.ruby-camping.com/] -* Coset[http://leahneukirchen.org/repos/coset/] -* Hanami[https://hanamirb.org/] -* Padrino[http://padrinorb.com/] -* 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] - -== Available middleware shipped with \Rack - -Between the server and the framework, \Rack can be customized to your -applications needs using middleware. \Rack itself ships with the following -middleware: - -* Rack::Chunked, for streaming responses using chunked encoding. -* Rack::CommonLogger, for creating Apache-style logfiles. -* Rack::ConditionalGet, for returning not modified responses when the response - has not changed. -* Rack::Config, for modifying the environment before processing the request. -* Rack::ContentLength, for setting Content-Length header based on body size. -* Rack::ContentType, for setting default Content-Type header for responses. -* Rack::Deflater, for compressing responses with gzip. -* Rack::ETag, for setting ETag header on string bodies. -* Rack::Events, for providing easy hooks when a request is received - and when the response is sent. -* Rack::Files, for serving static files. -* Rack::Head, for returning an empty body for HEAD requests. -* Rack::Lint, for checking conformance to the \Rack API. -* Rack::Lock, for serializing requests using a mutex. -* Rack::Logger, for setting a logger to handle logging errors. -* Rack::MethodOverride, for modifying the request method based on a submitted - parameter. -* Rack::Recursive, for including data from other paths in the application, - and for performing internal redirects. -* Rack::Reloader, for reloading files if they have been modified. -* Rack::Runtime, for including a response header with the time taken to - process the request. -* Rack::Sendfile, for working with web servers that can use optimized - file serving for file system paths. -* Rack::ShowException, for catching unhandled exceptions and - presenting them in a nice and helpful way with clickable backtrace. -* Rack::ShowStatus, for using nice error pages for empty client error - responses. -* Rack::Static, for more configurable serving of static files. -* Rack::TempfileReaper, for removing temporary files creating during a - request. - -All these components use the same interface, which is described in -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 -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. -* Rack::Cascade, for trying additional \Rack applications if an - application returns a not found or method not supported response. -* Rack::Directory, for serving files under a given directory, with - directory indexes. -* Rack::MediaType, for parsing Content-Type headers. -* Rack::Mime, for determining Content-Type based on file extension. -* Rack::RewindableInput, for making any IO object rewindable, using - a temporary file buffer. -* Rack::URLMap, to route to multiple applications inside the same process. - -== 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. - -* https://github.com/rack/rack-contrib - -== rackup - -rackup is a useful tool for running \Rack applications, which uses the -Rack::Builder DSL to configure middleware and build up applications -easily. - -rackup automatically figures out the environment it is run in, and -runs your application as FastCGI, CGI, or WEBrick---all from the -same configuration. - -== Quick start - -Try the lobster! - -Either with the embedded WEBrick starter: - - ruby -Ilib lib/rack/lobster.rb - -Or with rackup: - - bin/rackup -Ilib example/lobster.ru - -By default, the lobster is found at http://localhost:9292. - -== Installing with RubyGems - -A Gem of \Rack is available at {rubygems.org}[https://rubygems.org/gems/rack]. You can install it with: - - gem install rack - -== Usage - -You should require the library: - - require 'rack' - -\Rack uses autoload to automatically load other files \Rack ships with on demand, -so you should not need require paths under +rack+. If you require paths under -+rack+ without requiring +rack+ itself, things may not work correctly. - -== Configuration - -Several parameters can be modified on Rack::Utils to configure \Rack behaviour. - -e.g: - - Rack::Utils.key_space_limit = 128 - -=== key_space_limit - -The default number of bytes to allow all parameters keys in a given parameter hash to take up. -Does not affect nested parameter hashes, so doesn't actually prevent an attacker from using -more than this many bytes for parameter keys. - -Defaults to 65536 characters. - -=== param_depth_limit - -The maximum amount of nesting allowed in parameters. -For example, if set to 3, this query string would be allowed: - - ?a[b][c]=d - -but this query string would not be allowed: - - ?a[b][c][d]=e - -Limiting the depth prevents a possible stack overflow when parsing parameters. - -Defaults to 100. - -=== multipart_file_limit - -The maximum number of parts with a filename a request can contain. -Accepting too many part can lead to the server running out of file handles. - -The default is 128, which means that a single request can't upload more than 128 files at once. - -Set to 0 for no limit. - -Can also be set via the +RACK_MULTIPART_FILE_LIMIT+ environment variable. - -(This is also aliased as +multipart_part_limit+ and +RACK_MULTIPART_PART_LIMIT+ for compatibility) - -=== multipart_total_part_limit - -The maximum total number of parts a request can contain of any type, including -both file and non-file form fields. - -The default is 4096, which means that a single request can't contain more than -4096 parts. - -Set to 0 for no limit. - -Can also be set via the +RACK_MULTIPART_TOTAL_PART_LIMIT+ environment variable. - -== Changelog - -See {CHANGELOG.md}[https://github.com/rack/rack/blob/master/CHANGELOG.md]. - -== Contributing - -See {CONTRIBUTING.md}[https://github.com/rack/rack/blob/master/CONTRIBUTING.md]. - -== Contact - -Please post bugs, suggestions and patches to -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/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/forum/#!forum/rack-devel>. - -Git repository (send Git patches to the mailing list): - -* https://github.com/rack/rack - -You are also welcome to join the #rack channel on irc.freenode.net. - -== Thanks - -The \Rack Core Team, consisting of - -* Aaron Patterson (tenderlove[https://github.com/tenderlove]) -* Samuel Williams (ioquatix[https://github.com/ioquatix]) -* Jeremy Evans (jeremyevans[https://github.com/jeremyevans]) -* Eileen Uchitelle (eileencodes[https://github.com/eileencodes]) -* Matthew Draper (matthewd[https://github.com/matthewd]) -* Rafael França (rafaelfranca[https://github.com/rafaelfranca]) - -and the \Rack Alumni - -* Ryan Tomayko (rtomayko[https://github.com/rtomayko]) -* Scytrin dai Kinthra (scytrin[https://github.com/scytrin]) -* 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]) -* Michael Fellinger (manveru[https://github.com/manveru]) -* Santiago Pastorino (spastorino[https://github.com/spastorino]) -* Konstantin Haase (rkh[https://github.com/rkh]) - -would like to thank: - -* Adrian Madrid, for the LiteSpeed handler. -* Christoffer Sawicki, for the first Rails adapter and Rack::Deflater. -* Tim Fletcher, for the HTTP authentication code. -* Luc Heinrich for the Cookie sessions, the static file handler and bugfixes. -* Armin Ronacher, for the logo and racktools. -* Alex Beregszaszi, Alexander Kahn, Anil Wadghule, Aredridel, Ben - Alpert, Dan Kubb, Daniel Roethlisberger, Matt Todd, Tom Robinson, - Phil Hagelberg, S. Brent Faulkner, Bosko Milekic, Daniel RodrÃguez - Troitiño, Genki Takiuchi, Geoffrey Grosenbach, Julien Sanchez, Kamal - Fariz Mahyuddin, Masayoshi Takahashi, Patrick Aljordm, Mig, Kazuhiro - Nishiyama, Jon Bardin, Konstantin Haase, Larry Siden, Matias - Korhonen, Sam Ruby, Simon Chiang, Tim Connor, Timur Batyrshin, and - Zach Brock for bug fixing and other improvements. -* Eric Wong, Hongli Lai, Jeremy Kemper for their continuous support - and API improvements. -* Yehuda Katz and Carl Lerche for refactoring rackup. -* Brian Candler, for Rack::ContentType. -* Graham Batty, for improved handler loading. -* Stephen Bannasch, for bug reports and documentation. -* Gary Wright, for proposing a better Rack::Response interface. -* Jonathan Buch, for improvements regarding Rack::Response. -* Armin Röhrl, for tracking down bugs in the Cookie generator. -* 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. -* All bug reporters and patch contributors not mentioned above. - -== Links - -\Rack:: <https://rack.github.io/> -Official \Rack repositories:: <https://github.com/rack> -\Rack Bug Tracking:: <https://github.com/rack/rack/issues> -rack-devel mailing list:: <https://groups.google.com/forum/#!forum/rack-devel> - -== License - -\Rack is released under the {MIT License}[https://opensource.org/licenses/MIT]. diff --git a/Rakefile b/Rakefile index 237c3f26160f60d13c2b462c3e61db4c147e8fa5..7df2fa82d010d2c3148bee72dbdd2196693018fe 100644 --- a/Rakefile +++ b/Rakefile @@ -98,8 +98,24 @@ task "test_cov" do Rake::Task['test:regular'].invoke end +desc "Run separate tests for each test file, to test directly requiring components" +task "test:separate" do + fails = [] + FileList["test/**/spec_*.rb"].each do |file| + puts "#{FileUtils::RUBY} -w #{file}" + fails << file unless system({'SEPARATE'=>'1'}, FileUtils::RUBY, '-w', file) + end + if fails.empty? + puts 'All test files passed' + else + puts "Failures in the following test files:" + puts fails + raise "At least one separate test failed" + end +end + desc "Run all the fast + platform agnostic tests" -task test: %w[spec test:regular] +task test: %w[spec test:regular test:separate] desc "Run all the tests we run on CI" task ci: :test @@ -118,13 +134,3 @@ task rdoc: %w[changelog spec] do `git ls-files lib/\*\*/\*.rb`.strip.split) cp "contrib/rdoc.css", "doc/rdoc.css" end - -task pushdoc: :rdoc do - sh "rsync -avz doc/ rack.rubyforge.org:/var/www/gforge-projects/rack/doc/" -end - -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" -end diff --git a/SECURITY_POLICY.md b/SECURITY.md similarity index 93% rename from SECURITY_POLICY.md rename to SECURITY.md index 3590fa4d508272dae33b028f8b3c81396bf82092..93e413c78f0bee9aefbefce4c134c65324241da3 100644 --- a/SECURITY_POLICY.md +++ b/SECURITY.md @@ -1,30 +1,30 @@ -# Rack maintenance +# Security Policy ## Supported versions ### New features -New features will only be added to the master branch and will not be made available in point releases. +New features will only be added to the main 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.1.x +* Current release series: 2.2.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.1.x -* Next most recent release series: 2.0.x +* Current release series: 2.2.x +* Next most recent release series: 2.1.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.1.x -* Next most recent release series: 2.0.x +* Current release series: 2.2.x +* Next most recent release series: 2.1.x * Last major release series: 1.6.x ### Unsupported Release Series diff --git a/SPEC.rdoc b/SPEC.rdoc index 277142376e1e24b1fc3f79accc41dcf815ae33a5..ddf474ae1d0aba06bf0d553d9494fdea6e38d0ea 100644 --- a/SPEC.rdoc +++ b/SPEC.rdoc @@ -1,23 +1,27 @@ -This specification aims to formalize the Rack protocol. You +This specification aims to formalize the Rack protocol. You can (and should) use Rack::Lint to enforce it. When you develop middleware, be sure to add a Lint before and after to catch all mistakes. + = Rack applications + A Rack application is a Ruby object (not a class) that responds to +call+. It takes exactly one argument, the *environment* -and returns an Array of exactly three values: +and returns a non-frozen Array of exactly three values: The *status*, the *headers*, and the *body*. + == The Environment + The environment must be an unfrozen instance of Hash that includes -CGI-like headers. The application is free to modify the +CGI-like headers. The Rack application is free to modify the environment. The environment is required to include these variables -(adopted from PEP333), except when they'd be empty, but see +(adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see below. <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as "GET" or "POST". This cannot ever @@ -42,17 +46,20 @@ below. <tt>QUERY_STRING</tt>:: The portion of the request URL that follows the <tt>?</tt>, if any. May be empty, but is always required! -<tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: - When combined with <tt>SCRIPT_NAME</tt> and +<tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and <tt>PATH_INFO</tt>, these variables can be used to complete the URL. Note, however, that <tt>HTTP_HOST</tt>, if present, should be used in preference to <tt>SERVER_NAME</tt> for reconstructing the request URL. - <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> - can never be empty strings, and so - are always required. + <tt>SERVER_NAME</tt> can never be an empty + string, and so is always required. +<tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the + server is running on. Should be specified if + the server is running on a non-standard port. +<tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used + for the request. <tt>HTTP_</tt> Variables:: Variables corresponding to the client-supplied HTTP request headers (i.e., variables whose @@ -66,40 +73,19 @@ below. 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 - See Rack::VERSION, that corresponds to - the version of this SPEC. <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL. <tt>rack.input</tt>:: See below, the input stream. <tt>rack.errors</tt>:: See below, the error stream. -<tt>rack.multithread</tt>:: true if the application object may be - simultaneously invoked by another thread - in the same process, false otherwise. -<tt>rack.multiprocess</tt>:: true if an equivalent application object - may be simultaneously invoked by another - process, false otherwise. -<tt>rack.run_once</tt>:: true if the server expects - (but does not guarantee!) that the - application will only be invoked this one - time during the life of its containing - process. Normally, this will only be true - for a server based on CGI - (or something similar). -<tt>rack.hijack?</tt>:: present and true if the server supports - connection hijacking. See below, hijacking. -<tt>rack.hijack</tt>:: an object responding to #call that must be - called at least once before using - rack.hijack_io. - It is recommended #call return rack.hijack_io - as well as setting it in env if necessary. -<tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack - has received #call, this will contain - an object resembling an IO. See hijacking. +<tt>rack.hijack?</tt>:: See below, if present and true, indicates + that the server supports partial hijacking. +<tt>rack.hijack</tt>:: See below, if present, an object responding + to +call+ that is used to perform a full + hijack. Additional environment specifications have approved to -standardized middleware APIs. None of these are required to +standardized middleware APIs. None of these are required to be implemented by the server. -<tt>rack.session</tt>:: A hash like interface for storing +<tt>rack.session</tt>:: A hash-like interface for storing request session data. The store must implement: store(key, value) (aliased as []=); @@ -122,6 +108,11 @@ and should be prefixed uniquely. The prefix <tt>rack.</tt> is reserved for use with the Rack core distribution and other accepted specifications and must not be used otherwise. +The <tt>SERVER_PORT</tt> must be an Integer if set. +The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540. +The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540. +The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>. +If the <tt>HTTP_VERSION</tt> is present, it must equal the <tt>SERVER_PROTOCOL</tt>. The environment must not contain the keys <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt> (use the versions without <tt>HTTP_</tt>). @@ -129,26 +120,32 @@ The CGI keys (named without a period) must have String values. If the string values for CGI keys contain non-ASCII characters, they should use ASCII-8BIT encoding. There are the following restrictions: -* <tt>rack.version</tt> must be an array of Integers. * <tt>rack.url_scheme</tt> must either be +http+ or +https+. * There must be a valid input stream in <tt>rack.input</tt>. * There must be a valid error stream in <tt>rack.errors</tt>. -* There may be a valid hijack stream in <tt>rack.hijack_io</tt> +* There may be a valid hijack callback in <tt>rack.hijack</tt> * The <tt>REQUEST_METHOD</tt> must be a valid token. * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt> * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt> * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only. * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be - set. <tt>PATH_INFO</tt> should be <tt>/</tt> if + set. <tt>PATH_INFO</tt> should be <tt>/</tt> if <tt>SCRIPT_NAME</tt> is empty. <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty. +<tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been +processed. This would typically be invoked after sending the response to the client, but it could also be +invoked if an error occurs while generating the response or sending the response; in that case, the error +argument will be a subclass of +Exception+. +The callables are invoked with +env, status, headers, error+ arguments and should not raise any +exceptions. They should be invoked in reverse order of registration. + === The Input Stream The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be "ASCII-8BIT" and it must be opened in binary mode, for Ruby 1.9 compatibility. -The input stream must respond to +gets+, +each+, +read+ and +rewind+. +The input stream must respond to +gets+, +each+, and +read+. * +gets+ must be called without arguments and return a string, or +nil+ on EOF. * +read+ behaves like IO#read. @@ -169,120 +166,175 @@ The input stream must respond to +gets+, +each+, +read+ and +rewind+. If +buffer+ is given, then the read data will be placed into +buffer+ instead of a newly created String object. * +each+ must be called without arguments and only yield Strings. -* +rewind+ must be called without arguments. It rewinds the input - stream back to the beginning. It must not raise Errno::ESPIPE: - that is, it may not be a pipe or a socket. Therefore, handler - developers must buffer the input data into some rewindable object - if the underlying input stream is not rewindable. -* +close+ must never be called on the input stream. +* +close+ can be called on the input stream to indicate that the +any remaining input is not needed. + === The Error Stream + The error stream must respond to +puts+, +write+ and +flush+. * +puts+ must be called with a single argument that responds to +to_s+. * +write+ must be called with a single argument that is a String. * +flush+ must be called without arguments and must be called in order to make the error appear for sure. * +close+ must never be called on the error stream. + === Hijacking -==== Request (before status) -If rack.hijack? is true then rack.hijack must respond to #call. -rack.hijack must return the io that will also be assigned (or is -already present, in rack.hijack_io. -rack.hijack_io must respond to: -<tt>read, write, read_nonblock, write_nonblock, flush, close, -close_read, close_write, closed?</tt> +The hijacking interfaces provides a means for an application to take +control of the HTTP connection. There are two distinct hijack +interfaces: full hijacking where the application takes over the raw +connection, and partial hijacking where the application takes over +just the response body stream. In both cases, the application is +responsible for closing the hijacked stream. -The semantics of these IO methods must be a best effort match to -those of a normal ruby IO or Socket object, using standard -arguments and raising standard exceptions. Servers are encouraged -to simply pass on real IO objects, although it is recognized that -this approach is not directly compatible with SPDY and HTTP 2.0. - -IO provided in rack.hijack_io should preference the -IO::WaitReadable and IO::WaitWritable APIs wherever supported. - -There is a deliberate lack of full specification around -rack.hijack_io, as semantics will change from server to server. -Users are encouraged to utilize this API with a knowledge of their -server choice, and servers may extend the functionality of -hijack_io to provide additional features to users. The purpose of -rack.hijack is for Rack to "get out of the way", as such, Rack only -provides the minimum of specification and support. - -If rack.hijack? is false, then rack.hijack should not be set. - -If rack.hijack? is false, then rack.hijack_io should not be set. -==== Response (after headers) -It is also possible to hijack a response after the status and headers -have been sent. -In order to do this, an application may set the special header -<tt>rack.hijack</tt> to an object that responds to <tt>call</tt> -accepting an argument that conforms to the <tt>rack.hijack_io</tt> -protocol. - -After the headers have been sent, and this hijack callback has been -called, the application is now responsible for the remaining lifecycle -of the IO. The application is also responsible for maintaining HTTP -semantics. Of specific note, in almost all cases in the current SPEC, -applications will have wanted to specify the header Connection:close in -HTTP/1.1, and not Connection:keep-alive, as there is no protocol for -returning hijacked sockets to the web server. For that purpose, use the -body streaming API instead (progressively yielding strings via each). - -Servers must ignore the <tt>body</tt> part of the response tuple when -the <tt>rack.hijack</tt> response API is in use. - -The special response header <tt>rack.hijack</tt> must only be set -if the request env has <tt>rack.hijack?</tt> <tt>true</tt>. -==== Conventions -* Middleware should not use hijack unless it is handling the whole - response. -* Middleware may wrap the IO object for the response pattern. -* Middleware should not wrap the IO object for the request pattern. The - request pattern is intended to provide the hijacker with "raw tcp". +Full hijacking only works with HTTP/1. Partial hijacking is functionally +equivalent to streaming bodies, and is still optionally supported for +backwards compatibility with older Rack versions. + +==== Full Hijack + +Full hijack is used to completely take over an HTTP/1 connection. It +occurs before any headers are written and causes the request to +ignores any response generated by the application. + +It is intended to be used when applications need access to raw HTTP/1 +connection. + +If +rack.hijack+ is present in +env+, it must respond to +call+ +and return an +IO+ instance which can be used to read and write +to the underlying connection using HTTP/1 semantics and +formatting. + +==== Partial Hijack + +Partial hijack is used for bi-directional streaming of the request and +response body. It occurs after the status and headers are written by +the server and causes the server to ignore the Body of the response. + +It is intended to be used when applications need bi-directional +streaming. + +If +rack.hijack?+ is present in +env+ and truthy, +an application may set the special response header +rack.hijack+ +to an object that responds to +call+, +accepting a +stream+ argument. + +After the response status and headers have been sent, this hijack +callback will be invoked with a +stream+ argument which follows the +same interface as outlined in "Streaming Body". Servers must +ignore the +body+ part of the response tuple when the ++rack.hijack+ response header is present. Using an empty +Array+ +instance is recommended. + +The special response header +rack.hijack+ must only be set +if the request +env+ has a truthy +rack.hijack?+. == The Response + === The Status -This is an HTTP status. When parsed as integer (+to_i+), it must be -greater than or equal to 100. + +This is an HTTP status. It must be an Integer greater than or equal to +100. + === The Headers -The header must respond to +each+, and yield values of key and value. + +The headers must be a unfrozen Hash. 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 must not contain a +Status+ key. -The header must conform to RFC7230 token specification, i.e. cannot +Header keys must conform to RFC7230 token specification, i.e. cannot contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}". -The values of the header must be Strings, -consisting of lines (for multiple header values, e.g. multiple -<tt>Set-Cookie</tt> values) separated by "\\n". -The lines must not contain characters below 037. -=== The Content-Type -There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, -204 or 304. -=== The Content-Length -There must not be a <tt>Content-Length</tt> header when the -+Status+ is 1xx, 204 or 304. +Header keys must not contain uppercase ASCII characters (A-Z). +Header values must be either a String instance, +or an Array of String instances, +such that each String instance must not contain characters below 037. + +=== The content-type + +There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx, +204, or 304. + +=== The content-length + +There must not be a <tt>content-length</tt> header key when the ++Status+ is 1xx, 204, or 304. + === The Body -The Body must respond to +each+ + +The Body is typically an +Array+ of +String+ instances, an enumerable +that yields +String+ instances, a +Proc+ instance, or a File-like +object. + +The Body must respond to +each+ or +call+. It may optionally respond +to +to_path+ or +to_ary+. A Body that responds to +each+ is considered +to be an Enumerable Body. A Body that responds to +call+ is considered +to be a Streaming Body. + +A Body that responds to both +each+ and +call+ must be treated as an +Enumerable Body, not a Streaming Body. If it responds to +each+, you +must call +each+ and not +call+. If the Body doesn't respond to ++each+, then you can assume it responds to +call+. + +The Body must either be consumed or returned. The Body is consumed by +optionally calling either +each+ or +call+. +Then, if the Body responds to +close+, it must be called to release +any resources associated with the generation of the body. +In other words, +close+ must always be called at least once; typically +after the web server has sent the response to the client, but also in +cases where the Rack application makes internal/virtual requests and +discards the response. + + +After calling +close+, the Body is considered closed and should not +be consumed again. +If the original Body is replaced by a new Body, the new Body must +also consume the original Body by calling +close+ if possible. + +If the Body responds to +to_path+, it must return a +String+ +path for the local file system whose contents are identical +to that produced by calling +each+; this may be used by the +server as an alternative, possibly more efficient way to +transport the response. The +to_path+ method does not consume +the body. + +==== Enumerable Body + +The Enumerable Body must respond to +each+. +It must only be called once. +It must not be called after being closed. and must only yield String values. The Body itself should not be an instance of String, as this will break in Ruby 1.9. -If the Body responds to +close+, it will be called after iteration. If -the body is replaced by a middleware after action, the original body -must be closed first, if it responds to close. +Middleware must not call +each+ directly on the Body. +Instead, middleware can return a new Body that calls +each+ on the +original Body, yielding at least once per iteration. -If the Body responds to +to_path+, it must return a String -identifying the location of a file whose contents are identical -to that produced by calling +each+; this may be used by the -server as an alternative, possibly more efficient way to -transport the response. +If the Body responds to +to_ary+, it must return an +Array+ whose +contents are identical to that produced by calling +each+. +Middleware may call +to_ary+ directly on the Body and return a new +Body in its place. In other words, middleware can only process the +Body directly if it responds to +to_ary+. If the Body responds to both ++to_ary+ and +close+, its implementation of +to_ary+ must call ++close+. + +==== Streaming Body + +The Streaming Body must respond to +call+. +It must only be called once. +It must not be called after being closed. +It takes a +stream+ argument. + +The +stream+ argument must implement: +<tt>read, write, <<, flush, close, close_read, close_write, closed?</tt> + +The semantics of these IO methods must be a best effort match to +those of a normal Ruby IO or Socket object, using standard arguments +and raising standard exceptions. Servers are encouraged to simply +pass on real IO objects, although it is recognized that this approach +is not directly compatible with HTTP/2. -The Body commonly is an Array of Strings, the application -instance itself, or a File-like object. == Thanks -Some parts of this specification are adopted from PEP333: Python -Web Server Gateway Interface -v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank -everyone involved in that effort. +Some parts of this specification are adopted from {PEP 333 – Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/] +I'd like to thank everyone involved in that effort. diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..290fac24d96636964206adcdaa26cdb601f39956 --- /dev/null +++ b/UPGRADE-GUIDE.md @@ -0,0 +1,328 @@ +# Rack 3 Upgrade Guide + +This document is a work in progress, but outlines some of the key changes in +Rack 3 which you should be aware of in order to update your server, middleware +and/or applications. + +## Interface Changes + +### Rack 2 & Rack 3 compatibility + +Most applications can be compatible with Rack 2 and 3 by following the strict intersection of the Rack Specifications, notably: + +- Response array must now be non-frozen. +- Response `status` must now be an integer greater than or equal to 100. +- Response `headers` must now be an unfrozen hash. +- Response header keys can no longer include uppercase characters. +- `rack.input` is no longer required to be rewindable. +- `rack.multithread`/`rack.multiprocess`/`rack.run_once`/`rack.version` are no longer required environment keys. +- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional. +- `rack.hijack_io` has been removed completely. +- `SERVER_PROTOCOL` is now a required key, matching the HTTP protocol used in the request. +- Middleware must no longer call `#each` on the body, but they can call `#to_ary` on the body if it responds to `#to_ary`. + +There is one changed feature in Rack 3 which is not backwards compatible: + +- Response header values can be an `Array` to handle multiple values (and no longer supports `\n` encoded headers). + +You can achieve compatibility by using `Rack::Response#add_header` which provides an interface for adding headers without concern for the underlying format. + +There is one new feature in Rack 3 which is not directly backwards compatible: + +- Response body can now respond to `#call` (streaming body) instead of `#each` (enumerable body), for the equivalent of response hijacking in previous versions. + +If supported by your server, you can use partial rack hijack instead (or wrap this behaviour in a middleware). + +### `config.ru` `Rack::Builder#run` now accepts block + +Previously, `Rack::Builder#run` method would only accept a callable argument: + +```ruby +run lambda{|env| [200, {}, ["Hello World"]]} +``` + +This can be rewritten more simply: + +```ruby +run do |env| + [200, {}, ["Hello World"]] +end +``` + +### Response bodies can be used for bi-directional streaming + +Previously, the `rack.hijack` response header could be used for implementing +bi-directional streaming (e.g. WebSockets). + +```ruby +def call(env) + stream_callback = proc do |stream| + stream.read(...) + stream.write(...) + ensure + stream.close(...) + end + + return [200, {'rack.hijack' => stream_callback}, []] +end +``` + +This feature was optional and tricky to use correctly. You can now achieve the +same thing by giving `stream_callback` as the response body: + +```ruby +def call(env) + stream_callback = proc do |stream| + stream.read(...) + stream.write(...) + ensure + stream.close(...) + end + + return [200, {}, stream_callback] +end +``` + +### `Rack::Session` was moved to a separate gem. + +Previously, `Rack::Session` was part of the `rack` gem. Not every application +needs it, and it increases the security surface area of the `rack`, so it was +decided to extract it into its own gem `rack-session` which can be updated +independently. + +Applications that make use of `rack-session` will need to add that gem as a +dependency: + +```ruby +gem 'rack-session' +``` + +This provides all the previously available functionality. + +### `bin/rackup` was moved to a separate gem. + +Previously, the `rackup` executable was included with Rack. Because WEBrick is +no longer a default gem with Ruby, we had to make a decision: either `rack` +should depend on `webrick` or we should move that functionality into a +separate gem. We chose the latter which will hopefully allow us to innovate +more rapidly on the design and implementation of `rackup` separately from +"rack the interface". + +In Rack 3, you will need to include: + +```ruby +gem 'rackup' +``` + +This provides all the previously available functionality. + +## Request Changes + +### `rack.version` is no longer required + +Previously, the "rack protocol version" was available in `rack.version` but it +was not practically useful, so it has been removed as a requirement. + +### `rack.multithread`/`rack.multiprocess`/`rack.run_once` are no longer required + +Previously, servers tried to provide these keys to reflect the execution +environment. These come too late to be useful, so they have been removed as a +requirement. + +### `rack.hijack?` now only applies to partial hijack + +Previously, both full and partial hijiack were controlled by the presence and +value of `rack.hijack?`. Now, it only applies to partial hijack (which now can +be replaced by streaming bodies). + +### `rack.hijack` alone indicates that you can execute a full hijack + +Previously, `rack.hijack?` had to be truthy, as well as having `rack.hijack` +present in the request environment. Now, the presence of the `rack.hijack` +callback is enough. + +### `rack.hijack_io` is removed + +Previously, the server would try to set `rack.hijack_io` into the request +environment when `rack.hijack` was invoked for a full hijack. This was often +impossible if a middleware had called `env.dup`, so this requirement has been +dropped entirely. + +### `rack.input` is no longer required to be rewindable + +Previously, `rack.input` was required to be rewindable, i.e. `io.seek(0)` but +this was only generally possible with a file based backing, which prevented +efficient streaming of request bodies. Now, `rack.input` is not required to be +rewindable. + +## Response Changes + +### Response must be mutable + +Rack 3 requires the response Array `[status, headers, body]` to be mutable. +Existing code that uses a frozen response will need to be changed: + +```ruby +NOT_FOUND = [404, {}, ["Not Found"]].freeze + +def call(env) + ... + return NOT_FOUND +end +``` + +should be rewritten as: + +```ruby +def not_found + [404, {}, ["Not Found"]] +end + +def call(env) + ... + return not_found +end +``` + +Note there is a subtle bug in the former version: the headers hash is mutable +and can be modified, and these modifications can leak into subsequent requests. + +### Response headers must be a mutable hash + +Rack 3 requires response headers to be a mutable hash. Previously it could be +any object that would respond to `#each` and yield `key`/`value` pairs. +Previously, the following was acceptable: + +```ruby +def call(env) + return [200, [['content-type', 'text/plain']], ["Hello World"]] +end +``` + +Now you must use a hash instance: + +```ruby +def call(env) + return [200, {'content-type' => 'text/plain'}, ["Hello World"]] +end +``` + +This ensures middleware can predictably update headers as needed. + +### Response Headers must be lower case + +Rack 3 requires all response headers to be lower case. This is to simplify +fetching and updating response headers. Previously you had to use something like +`Rack::HeadersHash` + +```ruby +def call(env) + response = @app.call(env) + # HeaderHash must allocate internal objects and compute lower case keys: + headers = Rack::Utils::HeaderHash[response[1]] + + cache_response(headers['ETag'], response) + + ... +end +``` + +but now you must just use the normal form for HTTP header: + +```ruby +def call(env) + response = @app.call(env) + # A plain hash with lower case keys: + headers = response[1] + + cache_response(headers['etag'], response) + + ... +end +``` + +If you want your code to work with Rack 3 without having to manually lowercase +each header key used, instead of using a plain hash for headers, you can use +`Rack::Headers` on Rack 3. + +```ruby + headers = defined?(Rack::Headers) ? Rack::Headers.new : {} +``` + +`Rack::Headers` is a subclass of Hash that will automatically lowercase keys: + +```ruby + headers = Rack::Headers.new + headers['Foo'] = 'bar' + headers['FOO'] # => 'bar' + headers.keys # => ['foo'] +``` + +### Multiple response header values are encoded using an `Array` + +Response header values can be an Array to handle multiple values (and no longer +supports `\n` encoded headers). If you use `Rack::Response`, you don't need to +do anything, but if manually append values to response headers, you will need to +promote them to an Array, e.g. + +```ruby +def set_cookie_header!(headers, key, value) + if header = headers[SET_COOKIE] + if header.is_a?(Array) + header << set_cookie_header(key, value) + else + headers[SET_COOKIE] = [header, set_cookie_header(key, value)] + end + else + headers[SET_COOKIE] = set_cookie_header(key, value) + end +end +``` + +### Response body might not respond to `#each` + +Rack 3 has more strict requirements on response bodies. Previously, response +body would only need to respond to `#each` and optionally `#close`. In addition, +there was no way to determine whether it was safe to call `#each` and buffer the +response. + +### Response bodies can be buffered if they expose `#to_ary` + +If your body responds to `#to_ary` then it must return an `Array` whose contents +are identical to that produced by calling `#each`. If the body responds to both +`#to_ary` and `#close` then its implementation of `#to_ary` must also call +`#close`. + +Previously, it was not possible to determine whether a response body was +immediately available (could be buffered) or was streaming chunks. This case is +now unambiguously exposed by `#to_ary`: + +```ruby +def call(env) + status, headers, body = @app.call(env) + + # Check if we can buffer the body into an Array, so we can compute a digest: + if body.respond_to?(:to_ary) + body = body.to_ary + digest = digest_body(body) + headers[ETAG_STRING] = %(W/"#{digest}") if digest + end + + return [status, headers, body] +end +``` + +### Middleware should not directly modify the response body + +Be aware that the response body might not respond to `#each` and you must now +check if the body responds to `#each` or not to determine if it is an enumerable +or streaming body. + +You must not call `#each` directly on the body and instead you should return a +new body that calls `#each` on the original body. + +### Status needs to be an `Integer` + +The response status is now required to be an `Integer` with a value greater or equal to 100. + +Previously any object that responded to `#to_i` was allowed, so a response like `["200", {}, ""]` will need to be replaced with `[200, {}, ""]` and so on. This can be done by calling `#to_i` on the status object yourself. diff --git a/bin/rackup b/bin/rackup deleted file mode 100755 index 58988a0b32d16a93e818e02533fd5745b943dc09..0000000000000000000000000000000000000000 --- a/bin/rackup +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "rack" -Rack::Server.start diff --git a/config/external.yaml b/config/external.yaml new file mode 100644 index 0000000000000000000000000000000000000000..92e64509bd461b00e8d8c1878affe5e6590387e4 --- /dev/null +++ b/config/external.yaml @@ -0,0 +1,3 @@ +protocol-rack: + url: https://github.com/socketry/protocol-rack + command: bundle exec bake test diff --git a/contrib/LICENSE.md b/contrib/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..fc9e093b9124e963a30ae484aa34932653b3aa6f --- /dev/null +++ b/contrib/LICENSE.md @@ -0,0 +1,7 @@ +# Contributed Materials + +## Logo + +Copyright, 2022, by Malene Laugesen. + +This work is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/). diff --git a/contrib/logo.webp b/contrib/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..62ef28f5b15fe88034d1285c802458005da07292 Binary files /dev/null and b/contrib/logo.webp differ diff --git a/contrib/rack.png b/contrib/rack.png deleted file mode 100644 index 0920c4fb0c127121f458abfb9d9a18f3bb8ce6a5..0000000000000000000000000000000000000000 Binary files a/contrib/rack.png and /dev/null differ diff --git a/contrib/rack.svg b/contrib/rack.svg deleted file mode 100644 index 0d3f961cc4f7a5dfa9c2627d8f3e3538eb3aee13..0000000000000000000000000000000000000000 --- a/contrib/rack.svg +++ /dev/null @@ -1,150 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> - -<svg - xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="744.09448819" - height="1052.3622047" - id="svg2" - version="1.1" - inkscape:version="0.48.3.1 r9886" - sodipodi:docname="rack.svg"> - <defs - id="defs4"> - <linearGradient - id="linearGradient3837" - osb:paint="solid"> - <stop - style="stop-color:#000000;stop-opacity:1;" - offset="0" - id="stop3839" /> - </linearGradient> - </defs> - <sodipodi:namedview - id="base" - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1.0" - inkscape:pageopacity="0.0" - inkscape:pageshadow="2" - inkscape:zoom="0.98994949" - inkscape:cx="230.49849" - inkscape:cy="656.46253" - inkscape:document-units="px" - inkscape:current-layer="layer1" - showgrid="false" - showguides="false" - inkscape:guide-bbox="true" - inkscape:window-width="1920" - inkscape:window-height="1056" - inkscape:window-x="1920" - inkscape:window-y="24" - inkscape:window-maximized="1"> - <sodipodi:guide - orientation="1,0" - position="645.99255,757.10933" - id="guide2995" /> - <sodipodi:guide - orientation="1,0" - position="488.40876,686.90373" - id="guide2997" /> - <sodipodi:guide - orientation="1,0" - position="176.7767,748.52304" - id="guide2999" /> - <sodipodi:guide - orientation="1,0" - position="355.71429,782.85714" - id="guide3005" /> - <sodipodi:guide - orientation="0,1" - position="527.14286,642.85714" - id="guide3007" /> - <sodipodi:guide - orientation="0,1" - position="431.42857,507.85714" - id="guide3009" /> - <sodipodi:guide - orientation="0,1" - position="488.40876,783.57143" - id="guide3011" /> - <sodipodi:guide - orientation="0,1" - position="505,372.85714" - id="guide3013" /> - </sodipodi:namedview> - <metadata - id="metadata7"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1"> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 176.28125,201.03125 0,0.625 0,395.125 0,0.375 0.34375,0.0937 312.34375,91.6875 0.625,0.1875 0,-0.65625 0.125,-419.09375 0,-0.40625 -0.40625,-0.0937 -312.4375,-67.71875 -0.59375,-0.125 z m 1,1.21875 311.4375,67.5 -0.125,418.0625 -311.3125,-91.375 0,-394.1875 z" - id="path2985" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 647.21875,206.59375 -0.6875,0.28125 -157.59375,62.21875 -0.3125,0.125 0,0.34375 0.1875,419.125 0,0.75 0.6875,-0.28125 156.0625,-63.1875 0.3125,-0.125 0,-0.34375 1.34375,-418.15625 0,-0.75 z m -1,1.4375 -1.34375,417.125 -155.0625,62.78125 -0.1875,-418.03125 156.59375,-61.875 z" - id="path2993" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.6875,137.40625 -0.15625,0.0625 L 176.96875,201.0625 177.3125,202 355.75,138.4375 646.78125,207.53125 647,206.5625 l -291.15625,-69.125 -0.15625,-0.0312 z" - id="path3003" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.71875,277.53125 -0.125,0.0312 -178.9375,47.96875 -1.8125,0.46875 1.8125,0.5 311.625,83.5 0.15625,0.0312 0.125,-0.0625 157.59375,-53.65625 1.5625,-0.53125 -1.59375,-0.4375 -290.28125,-77.78125 -0.125,-0.0312 z m 0,1.03125 L 644.3125,355.90625 488.375,409 178.71875,326 l 177,-47.4375 z" - id="path3015" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.21875,240.9375 0,37.125 1,0 0,-37.125 -1,0 z" - id="path3017" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 176.28125,202.65625 0,393.28125 1,0 0,-393.28125 -1,0 z" - id="path3019" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.71875,409 -0.125,0.0312 L 177,455.8125 l -1.78125,0.46875 1.78125,0.5 L 488.28125,545 l 0.15625,0.0312 0.125,-0.0625 156.71875,-56.25 1.46875,-0.53125 -1.53125,-0.40625 -289.375,-78.75 -0.125,-0.0312 z m 0,1.03125 287.6875,78.28125 L 488.375,544 179.03125,456.3125 355.71875,410.03125 z" - id="path3021" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.71875,544 -0.15625,0.0312 -178.5625,52.3125 0.28125,0.96875 178.4375,-52.28125 289.59375,80.25 0.28125,-0.96875 -289.75,-80.28125 -0.125,-0.0312 z" - id="path3023" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.21875,374.34375 0,35.15625 1,0 0,-35.15625 -1,0 z" - id="path3025" - inkscape:connector-curvature="0" /> - <path - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round" - d="m 355.1875,507.03125 0,37.4375 1.03125,0 0,-37.4375 -1.03125,0 z" - id="path3027" - inkscape:connector-curvature="0" /> - </g> -</svg> diff --git a/contrib/rack_logo.svg b/contrib/rack_logo.svg deleted file mode 100644 index 8287c9da09d7c331c4c173e20dc02ef17e509f75..0000000000000000000000000000000000000000 --- a/contrib/rack_logo.svg +++ /dev/null @@ -1,164 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://web.resource.org/cc/" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns="http://www.w3.org/2000/svg" - version="1.1" - x="0px" - y="0px" - viewBox="-480 209.5 750 375" - style="enable-background:new -480 209.5 750 375;" - xml:space="preserve"> -<style type="text/css"> - .st0{display:none;} - .st1{display:inline;} - .st2{fill:none;stroke:#000000;stroke-width:11.4452;stroke-linejoin:round;} - .st3{fill:none;stroke:#000000;stroke-width:11.6061;stroke-linejoin:round;} - .st4{fill:none;stroke:#000000;stroke-width:7.7374;stroke-linejoin:round;} - .st5{fill:none;stroke:#000000;stroke-width:7.7374;} - .st6{stroke:#000000;stroke-width:5.8905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:30;} -</style> -<g id="Logo_Text" inkscape:export-ydpi="360" inkscape:export-xdpi="360" inkscape:export-filename="/home/blackbird/Desktop/rack_logo_final.png" inkscape:version="0.44" sodipodi:version="0.32" sodipodi:docname="rack_logo.svg" sodipodi:docbase="/home/blackbird/Desktop" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"> - - <sodipodi:namedview width="200px" height="100px" inkscape:window-y="24" inkscape:window-x="0" inkscape:window-height="975" inkscape:window-width="1400" inkscape:current-layer="layer1" inkscape:document-units="px" inkscape:pageshadow="2" inkscape:pageopacity="0.0" inkscape:cy="67.007749" inkscape:cx="148.56163" inkscape:zoom="3.959798" objecttolerance="10" guidetolerance="10" gridtolerance="10000" borderopacity="1.0" bordercolor="#666666" pagecolor="#ffffff" id="base"> - </sodipodi:namedview> - <path id="text2980" d="M-84.9,419.5v27.1c-2-0.7-4.2-1.1-6.8-1.1c-3,0-5.8,0.7-8.6,2.1c-2.7,1.4-4.9,3.1-6.5,5.3 - c-1.2,1.6-2,3.6-2.6,6c-0.4,1.5-0.6,3.6-0.6,6.2v23.5h-33.7v-68.5h31.5v15.3c2.8-5.9,5.8-10.2,9.2-12.6c3.4-2.5,7.6-3.8,12.5-3.8 - C-88.7,419.1-86.9,419.2-84.9,419.5 M-33.2,488.7V479c-5.3,7.8-13.2,11.7-23.7,11.7c-7.8,0-13.8-2-18.1-5.9 - c-4.3-3.9-6.5-8.9-6.5-14.9c0-4.4,1-8.3,3-11.6c2.1-3.4,5-6.1,8.7-8.1c2.4-1.3,5.7-2.4,10-3.3c4.4-0.9,9.5-1.3,15.5-1.3 - c2.7,0,6.2,0.1,10.4,0.3c-0.1-3.1-1.3-5.6-3.7-7.4c-2.4-1.9-6.4-2.8-12-2.8c-3.6,0-7.4,0.4-11.2,1.3c-2.8,0.7-7.1,2.2-12.7,4.5v-19 - c5.2-1.5,9.9-2.5,14.3-3.1c4.4-0.6,9.3-0.9,14.6-0.9c8.4,0,15.6,0.8,21.6,2.3c3,0.7,5.7,1.9,8.3,3.4c2.5,1.5,4.5,3.2,6,5.1 - c1.5,1.8,2.7,4.2,3.7,7c1,2.8,1.5,6.7,1.5,11.7v40.7H-33.2 M-33,458.2c-5.4,0-9.1,0.1-11.2,0.6c-2.1,0.4-3.8,1.3-5,2.8 - c-1.2,1.4-1.8,3-1.8,4.8c0,1.9,0.7,3.6,2.2,5c1.5,1.4,3.3,2.1,5.4,2.1c2.6,0,5-1.1,7.2-3.4c2.2-2.3,3.3-5.5,3.3-9.7 - C-32.9,459.8-32.9,459.1-33,458.2 M70.9,421.1v20.4c-5.8-2.4-10.8-3.5-14.9-3.5c-5,0-9,1.5-12,4.4c-3,2.9-4.5,6.7-4.5,11.4 - c0,5.3,1.5,9.4,4.5,12.5c3.1,3,7.2,4.6,12.5,4.6c4.3,0,9.3-1.4,15-4.1v20.5c-8.5,2.4-16.9,3.5-25.1,3.5c-6.8,0-13.2-1-19.1-3 - c-4.4-1.5-8.3-3.7-11.6-6.5c-3.2-2.9-5.7-6.1-7.5-9.6c-2.5-5-3.8-10.7-3.8-17.1c0-12.1,4.5-21.5,13.4-28.3 - c7.1-5.4,16.7-8.1,28.8-8.1C55.1,418.1,63.2,419.1,70.9,421.1 M116.1,447.5l19.3-27.4H173l-23.3,30.4l24.2,38.2h-39l-18.9-33.3 - v33.3H82.4v-99.1h33.7V447.5"/> - <path id="text2985" d="M-142.2,535.9h2.4v-11h0.1c0.6,1.2,1.4,2.2,2.4,2.8c1,0.6,2.2,0.9,3.7,0.9c2.6,0,4.5-0.9,5.9-2.7 - c1.4-1.9,2.1-4.2,2.1-7.2c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4 - c-0.7,0.2-1.3,0.6-1.8,1c-0.5,0.4-0.8,0.8-1.1,1.2c-0.3,0.4-0.5,0.8-0.8,1.2l-0.1,0.1h-0.1l0.2-3.4h-2.4V535.9 M-128.3,518.7 - c0,1-0.1,2-0.3,2.9c-0.2,0.9-0.5,1.8-1,2.5c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7 - c-0.7-0.5-1.3-1.1-1.8-1.9c-0.5-0.8-0.8-1.6-1-2.6c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.8,0.3-2.7c0.2-0.9,0.6-1.8,1-2.6 - c0.5-0.8,1.1-1.4,1.8-1.9c0.8-0.5,1.7-0.7,2.7-0.7c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5 - C-128.4,516.7-128.3,517.6-128.3,518.7 M-122.7,518.7c0,1.3,0.2,2.6,0.5,3.8c0.4,1.2,0.9,2.2,1.7,3.2c0.7,0.9,1.7,1.6,2.8,2.2 - c1.2,0.5,2.5,0.8,4,0.8c1.5,0,2.9-0.3,4-0.8c1.2-0.5,2.1-1.3,2.9-2.2c0.7-0.9,1.3-2,1.6-3.2c0.4-1.2,0.6-2.4,0.6-3.7 - c0-1.3-0.2-2.6-0.6-3.7c-0.4-1.2-0.9-2.3-1.6-3.2c-0.7-0.9-1.7-1.6-2.9-2.2c-1.1-0.5-2.5-0.8-4-0.8c-1.5,0-2.9,0.3-4,0.8 - c-1.1,0.5-2.1,1.2-2.9,2.2c-0.7,0.9-1.3,2-1.7,3.2C-122.5,516.1-122.7,517.3-122.7,518.7 M-120.1,518.7c0-2.3,0.6-4.2,1.7-5.6 - c1.1-1.5,2.7-2.3,4.8-2.3c2,0,3.6,0.8,4.7,2.3c1.1,1.5,1.7,3.4,1.7,5.6c0,2.3-0.6,4.2-1.7,5.7c-1.1,1.5-2.7,2.2-4.7,2.2 - c-2,0-3.6-0.7-4.8-2.2C-119.5,522.8-120.1,520.9-120.1,518.7 M-74,509.2h-2.4l-5.4,16.4h-0.1l-5.2-16.4h-2.9l-5.4,16.4h-0.1 - l-5.1-16.4h-2.6l6.3,18.9h2.9l5.3-16.4h0.1l5.3,16.4h2.9L-74,509.2 M-58.3,525.1c-0.6,0.4-1.4,0.7-2.6,1c-1.1,0.3-2.1,0.4-2.9,0.4 - c-2,0-3.6-0.7-4.7-2c-1.1-1.4-1.7-3.1-1.7-5.2h13.3v-1.2c0-1.3-0.2-2.5-0.5-3.6c-0.3-1.1-0.8-2.1-1.4-3c-0.6-0.9-1.4-1.5-2.4-2 - c-1-0.5-2.1-0.7-3.5-0.7c-1.2,0-2.3,0.2-3.3,0.7c-1,0.5-1.9,1.1-2.6,2c-0.7,0.9-1.3,1.9-1.7,3.2c-0.4,1.2-0.6,2.5-0.6,4 - c0,1.5,0.2,2.8,0.5,4c0.3,1.2,0.8,2.2,1.5,3.1c0.7,0.9,1.6,1.6,2.7,2.1c1.1,0.5,2.5,0.7,4.1,0.7c0.9,0,1.9-0.1,2.9-0.3 - c1-0.2,1.9-0.4,2.7-0.7V525.1 M-70.2,517.3c0-0.8,0.1-1.6,0.4-2.3c0.3-0.8,0.7-1.5,1.1-2.2c0.5-0.6,1.1-1.1,1.8-1.5 - c0.7-0.4,1.5-0.6,2.4-0.6c0.9,0,1.6,0.2,2.3,0.6c0.6,0.3,1.1,0.8,1.5,1.4c0.4,0.6,0.7,1.3,0.9,2.1c0.2,0.8,0.3,1.6,0.3,2.4H-70.2 - M-52.6,528.1h2.4v-9.3c0-0.9,0.1-1.8,0.2-2.7c0.2-0.9,0.4-1.7,0.8-2.4c0.4-0.7,0.9-1.3,1.5-1.8c0.6-0.4,1.4-0.7,2.3-0.7 - c0.7,0,1.3,0.1,1.8,0.2V509c-0.2-0.1-0.5-0.1-0.8-0.1c-0.3,0-0.6-0.1-0.8-0.1c-1.1,0-2.1,0.4-3,1.1c-0.8,0.8-1.5,1.7-2,2.9h-0.1 - v-3.6h-2.4c0.1,0.6,0.1,1.2,0.1,1.7c0,0.5,0,1.3,0,2.5V528.1 M-41.9,527.7c1.5,0.6,3.3,0.9,5.5,0.9c0.8,0,1.6-0.1,2.4-0.3 - c0.8-0.2,1.5-0.5,2.2-0.9c0.7-0.4,1.2-1,1.6-1.7c0.4-0.7,0.6-1.5,0.6-2.5c0-0.8-0.2-1.6-0.5-2.2c-0.3-0.6-0.8-1.2-1.3-1.6 - c-0.5-0.5-1.1-0.9-1.7-1.1c-0.6-0.3-1.3-0.7-2.3-1.1c-1.3-0.6-2.3-1.1-2.9-1.5c-0.6-0.4-0.9-1-0.9-1.7c0-1.1,0.4-1.9,1.1-2.4 - c0.7-0.5,1.8-0.7,3.1-0.7c0.7,0,1.4,0.1,2.2,0.3c0.8,0.1,1.5,0.4,2.1,0.6l0.2-2c-0.8-0.3-1.6-0.5-2.6-0.6c-0.9-0.1-1.6-0.2-2.3-0.2 - c-0.8,0-1.6,0.1-2.4,0.3c-0.7,0.2-1.4,0.5-2,0.9c-0.6,0.4-1.1,1-1.4,1.6c-0.3,0.6-0.5,1.4-0.5,2.3c0,0.7,0.1,1.3,0.4,1.8 - c0.3,0.5,0.6,1,1.1,1.4c0.5,0.4,0.9,0.7,1.5,1c0.5,0.3,1.2,0.6,2.1,1c1.5,0.7,2.5,1.3,3.2,1.9c0.7,0.5,1.1,1.3,1.1,2.3 - c0,1-0.4,1.9-1.3,2.5c-0.8,0.6-1.9,0.9-3.1,0.9c-1.8,0-3.4-0.4-5.1-1.2L-41.9,527.7 M11.2,509.2H8.8l-5.4,16.4H3.3l-5.2-16.4h-2.9 - l-5.4,16.4h-0.1l-5.1-16.4H-18l6.3,18.9h2.9l5.3-16.4h0.1l5.3,16.4h2.9L11.2,509.2 M26.9,525.1c-0.6,0.4-1.4,0.7-2.6,1 - c-1.1,0.3-2.1,0.4-2.9,0.4c-2,0-3.6-0.7-4.7-2c-1.1-1.4-1.7-3.1-1.7-5.2h13.3v-1.2c0-1.3-0.2-2.5-0.5-3.6c-0.3-1.1-0.8-2.1-1.4-3 - c-0.6-0.9-1.4-1.5-2.4-2c-1-0.5-2.1-0.7-3.5-0.7c-1.2,0-2.3,0.2-3.3,0.7c-1,0.5-1.9,1.1-2.6,2c-0.7,0.9-1.3,1.9-1.7,3.2 - c-0.4,1.2-0.6,2.5-0.6,4c0,1.5,0.2,2.8,0.5,4c0.3,1.2,0.8,2.2,1.5,3.1c0.7,0.9,1.6,1.6,2.7,2.1c1.1,0.5,2.5,0.7,4.1,0.7 - c0.9,0,1.9-0.1,2.9-0.3c1-0.2,1.9-0.4,2.7-0.7V525.1 M15,517.3c0-0.8,0.1-1.6,0.4-2.3c0.3-0.8,0.7-1.5,1.1-2.2 - c0.5-0.6,1.1-1.1,1.8-1.5c0.7-0.4,1.5-0.6,2.4-0.6c0.9,0,1.6,0.2,2.3,0.6c0.6,0.3,1.1,0.8,1.5,1.4c0.4,0.6,0.7,1.3,0.9,2.1 - c0.2,0.8,0.3,1.6,0.3,2.4H15 M32.3,528.1h2.4v-3h0.1c1.2,2.3,3.3,3.4,6.1,3.4c2.6,0,4.5-0.9,5.9-2.7c1.4-1.9,2.1-4.2,2.1-7.2 - c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4c-0.7,0.3-1.3,0.6-1.8,1 - c-0.5,0.4-0.9,0.8-1.2,1.2c-0.3,0.4-0.5,0.7-0.6,1h-0.1v-12.1h-2.4V528.1 M46.2,518.7c0,1-0.1,2-0.3,2.9c-0.2,0.9-0.5,1.8-1,2.5 - c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.8-1.9c-0.5-0.8-0.8-1.6-1-2.6 - c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.9,0.3-2.8c0.2-0.9,0.5-1.8,1-2.5c0.5-0.8,1.1-1.4,1.8-1.9c0.8-0.5,1.7-0.7,2.7-0.7 - c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5C46.2,516.7,46.2,517.6,46.2,518.7 M74.9,528.1h2.4 - c-0.1-0.6-0.1-1.2-0.2-1.7c0-0.5,0-1.3,0-2.3v-8.3c0-2.5-0.5-4.3-1.6-5.4c-1.1-1.1-2.8-1.7-5.2-1.7c-0.8,0-1.8,0.1-2.9,0.4 - c-1.1,0.2-2,0.5-2.8,0.9v2.3c1.7-1,3.6-1.6,5.7-1.6c1.6,0,2.7,0.4,3.4,1.2c0.7,0.8,1,2,1,3.7v1h-0.5c-1.6,0-3.1,0.1-4.4,0.2 - c-1.3,0.1-2.5,0.4-3.7,0.8c-1.2,0.4-2.1,1.1-2.9,2c-0.7,0.9-1.1,2.1-1.1,3.6c0,0.5,0.1,1.1,0.2,1.7c0.2,0.6,0.5,1.2,1,1.7 - c0.5,0.5,1.1,1,2,1.4c0.9,0.4,1.9,0.6,3.2,0.6c1.3,0,2.5-0.3,3.7-0.9c1.2-0.6,2-1.5,2.5-2.6h0.1V528.1 M74.7,519.7 - c0,0.8-0.1,1.5-0.2,2.2c-0.1,0.7-0.3,1.4-0.7,2.1c-0.4,0.7-0.9,1.3-1.8,1.8c-0.8,0.5-1.8,0.7-3.2,0.7c-1.2,0-2.2-0.3-2.9-0.8 - c-0.7-0.6-1.1-1.4-1.1-2.6c0-1,0.3-1.8,0.8-2.4c0.5-0.6,1.2-1.1,2.1-1.4c0.9-0.3,1.8-0.5,2.9-0.6c1-0.1,2.1-0.1,3.4-0.1h0.7V519.7 - M82.2,535.9h2.4v-11h0.1c0.6,1.2,1.4,2.2,2.4,2.8c1,0.6,2.2,0.9,3.7,0.9c2.6,0,4.5-0.9,5.9-2.7c1.4-1.9,2.1-4.2,2.1-7.2 - c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4c-0.7,0.2-1.3,0.6-1.8,1 - c-0.5,0.4-0.8,0.8-1.1,1.2c-0.3,0.4-0.5,0.8-0.8,1.2l-0.1,0.1h-0.1l0.2-3.4h-2.4V535.9 M96.1,518.7c0,1-0.1,2-0.3,2.9 - c-0.2,0.9-0.5,1.8-1,2.5c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.8-1.9 - c-0.5-0.8-0.8-1.6-1-2.6c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.8,0.3-2.7c0.2-0.9,0.6-1.8,1-2.6c0.5-0.8,1.1-1.4,1.8-1.9 - c0.8-0.5,1.7-0.7,2.7-0.7c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5C96,516.7,96.1,517.6,96.1,518.7 - M103,535.9h2.4v-11h0.1c0.6,1.2,1.4,2.2,2.4,2.8c1,0.6,2.2,0.9,3.7,0.9c2.6,0,4.5-0.9,5.9-2.7c1.4-1.9,2.1-4.2,2.1-7.2 - c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4c-0.7,0.2-1.3,0.6-1.8,1 - c-0.5,0.4-0.8,0.8-1.1,1.2c-0.3,0.4-0.5,0.8-0.8,1.2l-0.1,0.1h-0.1l0.2-3.4H103V535.9 M116.9,518.7c0,1-0.1,2-0.3,2.9 - c-0.2,0.9-0.5,1.8-1,2.5c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.8-1.9 - c-0.5-0.8-0.8-1.6-1-2.6c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.8,0.3-2.7c0.2-0.9,0.6-1.8,1-2.6c0.5-0.8,1.1-1.4,1.8-1.9 - c0.8-0.5,1.7-0.7,2.7-0.7c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5C116.8,516.7,116.9,517.6,116.9,518.7 - M124,528.1h2.4v-27.8H124V528.1 M132.3,528.1h2.4v-18.9h-2.4V528.1 M134.7,501h-2.4v3.1h2.4V501 M153.5,509.5 - c-0.6-0.2-1.3-0.4-2.1-0.5c-0.8-0.1-1.6-0.2-2.6-0.2c-1.4,0-2.7,0.2-3.9,0.7c-1.2,0.5-2.2,1.1-3.1,2c-0.8,0.9-1.5,1.9-1.9,3.2 - c-0.5,1.2-0.7,2.5-0.7,4c0,1.3,0.2,2.6,0.6,3.8c0.4,1.2,1,2.2,1.8,3.2c0.8,0.9,1.8,1.6,2.9,2.2c1.2,0.5,2.5,0.8,4,0.8 - c1,0,1.9,0,2.7-0.1c0.8-0.1,1.6-0.2,2.3-0.5l-0.2-2.2c-1.6,0.6-3,0.9-4.4,0.9c-1.1,0-2-0.2-2.9-0.6c-0.9-0.4-1.6-1-2.2-1.7 - c-0.6-0.7-1.1-1.6-1.4-2.5c-0.3-1-0.5-2-0.5-3.1c0-2.3,0.6-4.3,1.9-5.7c1.3-1.5,3.1-2.2,5.4-2.2c0.6,0,1.3,0.1,2,0.3 - c0.7,0.2,1.5,0.4,2.1,0.7L153.5,509.5 M168.5,528.1h2.4c-0.1-0.6-0.1-1.2-0.2-1.7c0-0.5,0-1.3,0-2.3v-8.3c0-2.5-0.5-4.3-1.6-5.4 - c-1.1-1.1-2.8-1.7-5.2-1.7c-0.8,0-1.8,0.1-2.9,0.4c-1.1,0.2-2,0.5-2.8,0.9v2.3c1.7-1,3.6-1.6,5.7-1.6c1.6,0,2.7,0.4,3.4,1.2 - c0.7,0.8,1,2,1,3.7v1h-0.5c-1.6,0-3.1,0.1-4.4,0.2c-1.3,0.1-2.5,0.4-3.7,0.8c-1.2,0.4-2.1,1.1-2.9,2c-0.7,0.9-1.1,2.1-1.1,3.6 - c0,0.5,0.1,1.1,0.2,1.7c0.2,0.6,0.5,1.2,1,1.7c0.5,0.5,1.1,1,2,1.4c0.9,0.4,1.9,0.6,3.2,0.6c1.3,0,2.5-0.3,3.7-0.9 - c1.2-0.6,2-1.5,2.5-2.6h0.1V528.1 M168.3,519.7c0,0.8-0.1,1.5-0.2,2.2c-0.1,0.7-0.3,1.4-0.7,2.1c-0.4,0.7-0.9,1.3-1.8,1.8 - c-0.8,0.5-1.8,0.7-3.2,0.7c-1.2,0-2.2-0.3-2.9-0.8c-0.7-0.6-1.1-1.4-1.1-2.6c0-1,0.3-1.8,0.8-2.4c0.5-0.6,1.2-1.1,2.1-1.4 - c0.9-0.3,1.8-0.5,2.9-0.6c1-0.1,2.1-0.1,3.4-0.1h0.7V519.7 M184.4,509.2H180v-5.4l-2.4,0.8v4.6h-3.8v2h3.8v11c0,1.1,0,1.9,0.1,2.6 - c0.1,0.7,0.3,1.3,0.5,1.9c0.3,0.5,0.7,1,1.3,1.3c0.6,0.3,1.4,0.5,2.4,0.5c0.5,0,1.1-0.1,1.6-0.2c0.6-0.1,1-0.2,1.3-0.3l-0.2-1.9 - c-0.4,0.1-0.8,0.3-1.1,0.3c-0.3,0.1-0.7,0.1-1.1,0.1c-0.9,0-1.6-0.3-2-0.9c-0.4-0.6-0.6-1.3-0.6-2.3v-12.2h4.4V509.2 M188.5,528.1 - h2.4v-18.9h-2.4V528.1 M190.9,501h-2.4v3.1h2.4V501 M195.3,518.7c0,1.3,0.2,2.6,0.5,3.8c0.4,1.2,0.9,2.2,1.7,3.2 - c0.7,0.9,1.7,1.6,2.8,2.2c1.2,0.5,2.5,0.8,4,0.8c1.5,0,2.9-0.3,4-0.8c1.2-0.5,2.1-1.3,2.9-2.2c0.7-0.9,1.3-2,1.6-3.2 - c0.4-1.2,0.6-2.4,0.6-3.7s-0.2-2.6-0.6-3.7c-0.4-1.2-0.9-2.3-1.6-3.2c-0.7-0.9-1.7-1.6-2.9-2.2c-1.1-0.5-2.5-0.8-4-0.8 - c-1.5,0-2.9,0.3-4,0.8c-1.1,0.5-2.1,1.2-2.9,2.2c-0.7,0.9-1.3,2-1.7,3.2C195.5,516.1,195.3,517.3,195.3,518.7 M198,518.7 - c0-2.3,0.6-4.2,1.7-5.6c1.1-1.5,2.7-2.3,4.8-2.3c2,0,3.6,0.8,4.7,2.3c1.1,1.5,1.7,3.4,1.7,5.6c0,2.3-0.6,4.2-1.7,5.7 - c-1.1,1.5-2.7,2.2-4.7,2.2c-2,0-3.6-0.7-4.8-2.2C198.5,522.8,198,520.9,198,518.7 M217.6,528.1h2.4v-10.5c0-0.8,0.1-1.7,0.3-2.4 - c0.2-0.8,0.5-1.6,1-2.2c0.5-0.7,1.1-1.2,1.8-1.6c0.8-0.4,1.7-0.6,2.7-0.6c1.6,0,2.8,0.5,3.5,1.5c0.7,1,1.1,2.5,1.1,4.3v11.5h2.4 - V516c0-2.2-0.5-4-1.6-5.3c-1.1-1.3-2.7-2-5-2c-0.9,0-1.8,0.1-2.5,0.3c-0.7,0.2-1.3,0.5-1.8,0.9c-0.5,0.4-0.8,0.8-1.1,1.2 - c-0.3,0.4-0.6,0.9-0.8,1.4h-0.1v-3.4h-2.3c0.1,1,0.2,2.5,0.2,4.4V528.1 M236.8,527.7c1.5,0.6,3.3,0.9,5.5,0.9 - c0.8,0,1.6-0.1,2.4-0.3c0.8-0.2,1.5-0.5,2.2-0.9c0.7-0.4,1.2-1,1.6-1.7c0.4-0.7,0.6-1.5,0.6-2.5c0-0.8-0.2-1.6-0.5-2.2 - c-0.3-0.6-0.8-1.2-1.3-1.6c-0.5-0.5-1.1-0.9-1.7-1.1c-0.6-0.3-1.3-0.7-2.3-1.1c-1.3-0.6-2.3-1.1-2.9-1.5s-0.9-1-0.9-1.7 - c0-1.1,0.4-1.9,1.1-2.4c0.7-0.5,1.8-0.7,3.1-0.7c0.7,0,1.4,0.1,2.2,0.3c0.8,0.1,1.5,0.4,2.1,0.6l0.2-2c-0.8-0.3-1.6-0.5-2.6-0.6 - c-0.9-0.1-1.6-0.2-2.3-0.2c-0.8,0-1.6,0.1-2.4,0.3c-0.7,0.2-1.4,0.5-2,0.9c-0.6,0.4-1.1,1-1.4,1.6c-0.3,0.6-0.5,1.4-0.5,2.3 - c0,0.7,0.1,1.3,0.4,1.8c0.3,0.5,0.6,1,1.1,1.4c0.5,0.4,0.9,0.7,1.5,1c0.5,0.3,1.2,0.6,2.1,1c1.5,0.7,2.5,1.3,3.2,1.9 - c0.7,0.5,1.1,1.3,1.1,2.3c0,1-0.4,1.9-1.3,2.5c-0.8,0.6-1.9,0.9-3.1,0.9c-1.8,0-3.4-0.4-5.1-1.2L236.8,527.7"/> -</g> -<g id="Old_Logo" class="st0"> - <g class="st1"> - <path id="path2990_1_" class="st2" d="M-434.2,530l1.4-261.6l173.6,30.1l-1.5,258.2L-434.2,530z"/> - <path id="path3877_1_" class="st3" d="M-432,267.9l91.6-30.5l157.1,31.9l-80,26.3L-432,267.9z"/> - <path id="path3879_1_" sodipodi:nodetypes="ccccc" class="st3" d="M-259.8,556.5l75.4-26.6l1.1-260l-74.5,26.7L-259.8,556.5z"/> - <path id="path3881_1_" sodipodi:nodetypes="ccccc" class="st4" d="M-428.7,343.6l87.4-25l153.7,30.6l-70.8,25L-428.7,343.6z"/> - <path id="path3883_1_" sodipodi:nodetypes="ccccc" class="st4" d="M-429.4,448.2l87.4-25l157,25l-75.5,27.2L-429.4,448.2z"/> - <path id="path3887_1_" class="st5" d="M-430.7,528.4l82.4-26.4l161.3,24.5"/> - <path id="path3889_1_" class="st5" d="M-349.3,500l1.1-36.7"/> - <path id="path3891_1_" class="st5" d="M-344.3,421.7l1.5-61.6"/> - <path id="path3893_1_" sodipodi:nodetypes="cc" class="st5" d="M-341.9,317.1l0.3-32"/> - </g> -</g> -<g id="New_Logo"> - <g id="layer1_1_" inkscape:groupmode="layer" inkscape:label="Layer 1"> - <path id="path2985" inkscape:connector-curvature="0" class="st6" d="M-450.2,275v0.4v232.7v0.2l0.2,0.1l184,54l0.4,0.1v-0.4 - l0.1-246.9v-0.2l-0.2-0.1l-184-39.9L-450.2,275z M-449.6,275.8l183.5,39.8l-0.1,246.3L-449.6,508V275.8z"/> - <path id="path2993" inkscape:connector-curvature="0" class="st6" d="M-172.7,278.3l-0.4,0.2l-92.8,36.6l-0.2,0.1v0.2l0.1,246.9 - v0.4l0.4-0.2l91.9-37.2l0.2-0.1v-0.2l0.8-246.3V278.3z M-173.3,279.2l-0.8,245.7l-91.3,37l-0.1-246.2L-173.3,279.2z"/> - <path id="path3003" inkscape:connector-curvature="0" class="st6" d="M-344.5,237.6L-344.5,237.6l-105.3,37.5l0.2,0.6l105.1-37.4 - l171.4,40.7l0.1-0.6L-344.5,237.6L-344.5,237.6L-344.5,237.6z"/> - <path id="path3015" inkscape:connector-curvature="0" class="st6" d="M-344.5,320.1L-344.5,320.1l-105.5,28.3l-1.1,0.3l1.1,0.3 - l183.6,49.2l0.1,0l0.1,0l92.8-31.6l0.9-0.3l-0.9-0.3L-344.5,320.1L-344.5,320.1L-344.5,320.1z M-344.5,320.7l170,45.6l-91.9,31.3 - l-182.4-48.9L-344.5,320.7z"/> - <path id="path3017" inkscape:connector-curvature="0" class="st6" d="M-344.8,298.5v21.9h0.6v-21.9H-344.8z"/> - <path id="path3019" inkscape:connector-curvature="0" class="st6" d="M-450.2,276v231.7h0.6V276H-450.2z"/> - <path id="path3021" inkscape:connector-curvature="0" class="st6" d="M-344.5,397.5L-344.5,397.5l-105.3,27.6l-1,0.3l1,0.3 - l183.4,52l0.1,0l0.1,0l92.3-33.1l0.9-0.3l-0.9-0.2L-344.5,397.5L-344.5,397.5z M-344.5,398.1l169.5,46.1l-91.3,32.8l-182.2-51.7 - L-344.5,398.1z"/> - <path id="path3023" inkscape:connector-curvature="0" class="st6" d="M-344.5,477.1L-344.5,477.1l-105.3,30.8l0.2,0.6l105.1-30.8 - l170.6,47.3l0.2-0.6L-344.5,477.1L-344.5,477.1z"/> - <path id="path3025" inkscape:connector-curvature="0" class="st6" d="M-344.8,377.1v20.7h0.6v-20.7H-344.8z"/> - <path id="path3027" inkscape:connector-curvature="0" class="st6" d="M-344.8,455.3v22.1h0.6v-22.1H-344.8z"/> - </g> -</g> -</svg> diff --git a/debian/changelog b/debian/changelog index 850fcdac4b074d2d362e9d96e7551cd163638a36..aeb691b02e9057ccb64ed18222d7f2ae14c3e2d2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,29 +1,47 @@ -ruby-rack (2.2.6.4-1+deb12u1) bookworm-security; urgency=medium +ruby-rack (3.0.8-4) unstable; urgency=medium - * Non-maintainer upload. - * CVE-2024-25126: ReDoS in Content Type header parsing - * CVE-2024-26141: Reject Range headers which are too large - * CVE-2024-26146: ReDoS in Accept header parsing - * Closes: #1064516 + * Add Breaks on pre-rack3 ruby-adsf - -- Adrian Bunk <bunk@debian.org> Thu, 02 May 2024 23:39:36 +0300 + -- Antonio Terceiro <terceiro@debian.org> Wed, 05 Feb 2025 12:30:11 -0300 -ruby-rack (2.2.6.4-1) unstable; urgency=medium +ruby-rack (3.0.8-3) unstable; urgency=medium - * Team Upload - * New upstream version 2.2.6.4 (Fixes: CVE-2023-27530, CVE-2023-27539) - * Refresh patches (remove patches applied upstream) + * Add Breaks: against package versions that don't work with rack 3 - -- Pirate Praveen <praveen@debian.org> Fri, 24 Mar 2023 01:32:43 +0530 + -- Antonio Terceiro <terceiro@debian.org> Tue, 04 Feb 2025 20:35:02 -0300 -ruby-rack (2.2.4-3) unstable; urgency=high +ruby-rack (3.0.8-2) unstable; urgency=medium - * Team upload - * Fix test failures (Closes: #1030442) - * Fix CVE-2022-44570 CVE-2022-44571 CVE-2022-44572 (Closes: #1029832) - * Add Breaks for ruby-sinatra + * Re-upload to unstable. + + -- Utkarsh Gupta <utkarsh@debian.org> Tue, 28 Jan 2025 12:07:16 +0100 + +ruby-rack (3.0.8-1) experimental; urgency=medium - -- Sruthi Chandran <srud@debian.org> Thu, 09 Feb 2023 11:47:17 +0100 + * New upstream release. + * Declare compliance with Debian Policy 4.6.2 + * d/p/skip-random-failure.patch: removed, it does not seem to be failing + anymore. + + -- Lucas Kanashiro <kanashiro@debian.org> Mon, 26 Jun 2023 17:35:11 -0300 + +ruby-rack (3.0.0-1) experimental; urgency=medium + + * New upstream release. + * d/p/0002-Make-tests-pass-on-hosts-that-have-no-ipv4-connectiv.patch: + delete patch applied by upstream. + * Refresh patches. + * d/ruby-rack.docs: install README.md instead of README.rdoc. + * d/control: add myself to the Uploaders list. + * Do not install rackup manpage anymore. Remove: + - d/rackup.1 + - d/ruby-rack.manpages + * d/control: recommend ruby-rack-session and ruby-rackup. + * d/t/control: add ruby-rackup as a test dependency of smoke-test. + * d/t/smoke-test: content-type key needs to be in lowercase. + * Do not depend on thin to run tests during build and autopkgtest time. + + -- Lucas Kanashiro <kanashiro@debian.org> Wed, 09 Nov 2022 17:26:10 -0300 ruby-rack (2.2.4-2) unstable; urgency=medium diff --git a/debian/control b/debian/control index bdf08479deeda0dd88228714233b316b5e27bb07..bea1f6177cba18c60bfb5fd865322027d3800dea 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,8 @@ 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> + Utkarsh Gupta <utkarsh@debian.org>, + Lucas Kanashiro <kanashiro@debian.org> Section: ruby Priority: optional Build-Depends: debhelper-compat (= 13), @@ -14,8 +15,7 @@ Build-Depends: debhelper-compat (= 13), ruby-concurrent, ruby-dalli, ruby-minitest-global-expectations, - ruby-webrick (>= 1.7~), - thin + ruby-webrick (>= 1.7~) Standards-Version: 4.6.2 Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-rack Vcs-Git: https://salsa.debian.org/ruby-team/ruby-rack.git @@ -29,8 +29,15 @@ XB-Ruby-Versions: ${ruby:Versions} Depends: ruby:any, ${misc:Depends}, ${shlibs:Depends} +Recommends: ruby-rackup, + ruby-rack-session Breaks: ruby-rack-oauth2 (<< 1.11), - ruby-sinatra (<< 3.0.5-3) + ruby-adsf (<< 1.5), + ruby-capybara (<< 3.40.0+ds-2~), + ruby-sinatra (<< 4), + ruby-sprockets (<< 4), + ruby-omniauth (<< 2.1.1-4~), + unicorn (<< 6.1.0+git.20250131), Multi-Arch: foreign Description: modular Ruby webserver interface Rack provides a minimal, modular and adaptable interface for developing diff --git a/debian/patches/0001-Avoid-2nd-degree-polynomial-regexp-in-MediaType.patch b/debian/patches/0001-Avoid-2nd-degree-polynomial-regexp-in-MediaType.patch deleted file mode 100644 index 856364a568cabedd5e7746e4c0078133304dea11..0000000000000000000000000000000000000000 --- a/debian/patches/0001-Avoid-2nd-degree-polynomial-regexp-in-MediaType.patch +++ /dev/null @@ -1,51 +0,0 @@ -From 0dd2a6314a1677ba38d2f94b18ecf21a5fbfaa1d Mon Sep 17 00:00:00 2001 -From: Jean Boussier <jean.boussier@gmail.com> -Date: Wed, 6 Dec 2023 18:32:19 +0100 -Subject: Avoid 2nd degree polynomial regexp in MediaType - ---- - lib/rack/media_type.rb | 13 +++++++++---- - 1 file changed, 9 insertions(+), 4 deletions(-) - -diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb -index 41937c99..7fc1e39d 100644 ---- a/lib/rack/media_type.rb -+++ b/lib/rack/media_type.rb -@@ -4,7 +4,7 @@ module Rack - # Rack::MediaType parse media type and parameters out of content_type string - - class MediaType -- SPLIT_PATTERN = %r{\s*[;,]\s*} -+ SPLIT_PATTERN = /[;,]/ - - class << self - # The media type (type/subtype) portion of the CONTENT_TYPE header -@@ -15,7 +15,11 @@ 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.tap &:downcase! -+ if type = content_type.split(SPLIT_PATTERN, 2).first -+ type.rstrip! -+ type.downcase! -+ type -+ end - end - - # The media type parameters provided in CONTENT_TYPE as a Hash, or -@@ -27,9 +31,10 @@ module Rack - return {} if content_type.nil? - - content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh| -+ s.strip! - k, v = s.split('=', 2) -- -- hsh[k.tap(&:downcase!)] = strip_doublequotes(v) -+ k.downcase! -+ hsh[k] = strip_doublequotes(v) - end - end - --- -2.30.2 - diff --git a/debian/patches/0002-Make-tests-pass-on-hosts-that-have-no-ipv4-connectiv.patch b/debian/patches/0002-Make-tests-pass-on-hosts-that-have-no-ipv4-connectiv.patch deleted file mode 100644 index 095bd7ff1d82abf460e7727affb0e01cd5d52cac..0000000000000000000000000000000000000000 --- a/debian/patches/0002-Make-tests-pass-on-hosts-that-have-no-ipv4-connectiv.patch +++ /dev/null @@ -1,87 +0,0 @@ -From: Antonio Terceiro <terceiro@debian.org> -Date: Sat, 27 Feb 2021 09:25:18 -0300 -Subject: Make tests pass on hosts that have no ipv4 connectivity - -This is a backport of the patch sent upstream. - -Forwarded: https://github.com/rack/rack/pull/1738 ---- - test/spec_server.rb | 8 ++++---- - test/spec_thin.rb | 4 ++-- - test/spec_webrick.rb | 6 +++--- - 3 files changed, 9 insertions(+), 9 deletions(-) - ---- a/test/spec_server.rb -+++ b/test/spec_server.rb -@@ -350,8 +350,8 @@ - app: app, - environment: 'none', - pid: pidfile.path, -- Port: TCPServer.open('127.0.0.1', 0){|s| s.addr[1] }, -- Host: '127.0.0.1', -+ Port: TCPServer.open('localhost', 0){|s| s.addr[1] }, -+ Host: 'localhost', - Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - AccessLog: [], - daemonize: false, -@@ -360,9 +360,9 @@ - t = Thread.new { server.start { |s| Thread.current[:server] = s } } - t.join(0.01) until t[:server] && t[:server].status != :Stop - body = if URI.respond_to?(:open) -- URI.open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read } -+ URI.open("http://localhost:#{server.options[:Port]}/") { |f| f.read } - else -- open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read } -+ open("http://localhost:#{server.options[:Port]}/") { |f| f.read } - end - body.must_equal 'success' - ---- a/test/spec_thin.rb -+++ b/test/spec_thin.rb -@@ -15,7 +15,7 @@ - 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 = 'localhost', Port: @port = 9204, tag: "tag") do |server| - @server = server - end - end -@@ -41,7 +41,7 @@ - response["HTTP_VERSION"].must_equal "HTTP/1.1" - response["SERVER_PROTOCOL"].must_equal "HTTP/1.1" - response["SERVER_PORT"].must_equal "9204" -- response["SERVER_NAME"].must_equal "127.0.0.1" -+ response["SERVER_NAME"].must_equal "localhost" - end - - it "have rack headers" do ---- a/test/spec_webrick.rb -+++ b/test/spec_webrick.rb -@@ -10,7 +10,7 @@ - include TestRequest::Helpers - - before do -- @server = WEBrick::HTTPServer.new(Host: @host = '127.0.0.1', -+ @server = WEBrick::HTTPServer.new(Host: @host = 'localhost', - Port: @port = 9202, - Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - AccessLog: []) -@@ -45,7 +45,7 @@ - response["HTTP_VERSION"].must_equal "HTTP/1.1" - response["SERVER_PROTOCOL"].must_equal "HTTP/1.1" - response["SERVER_PORT"].must_equal "9202" -- response["SERVER_NAME"].must_equal "127.0.0.1" -+ response["SERVER_NAME"].must_equal "localhost" - end - - it "have rack headers" do -@@ -124,7 +124,7 @@ - - t = Thread.new do - Rack::Handler::WEBrick.run(lambda {}, -- Host: '127.0.0.1', -+ Host: 'localhost', - Port: 9210, - Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - AccessLog: []) { |server| diff --git a/debian/patches/0002-Return-an-empty-array-when-ranges-are-too-large.patch b/debian/patches/0002-Return-an-empty-array-when-ranges-are-too-large.patch deleted file mode 100644 index 60642868b22f9539007f0482b0f5e4c93204e230..0000000000000000000000000000000000000000 --- a/debian/patches/0002-Return-an-empty-array-when-ranges-are-too-large.patch +++ /dev/null @@ -1,46 +0,0 @@ -From ca18315cb37dffb378b56a64a6e0cefcb1df8fc0 Mon Sep 17 00:00:00 2001 -From: Aaron Patterson <tenderlove@ruby-lang.org> -Date: Tue, 13 Feb 2024 13:34:34 -0800 -Subject: Return an empty array when ranges are too large - -If the sum of the requested ranges is larger than the file itself, -return an empty array. In other words, refuse to respond with any bytes. - -[CVE-2024-26141] ---- - lib/rack/utils.rb | 3 +++ - test/spec_utils.rb | 4 ++++ - 2 files changed, 7 insertions(+) - -diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb -index c8e61ea1..72700503 100644 ---- a/lib/rack/utils.rb -+++ b/lib/rack/utils.rb -@@ -380,6 +380,9 @@ module Rack - end - ranges << (r0..r1) if r0 <= r1 - end -+ -+ return [] if ranges.map(&:size).sum > size -+ - ranges - end - -diff --git a/test/spec_utils.rb b/test/spec_utils.rb -index 90676258..6b069914 100644 ---- a/test/spec_utils.rb -+++ b/test/spec_utils.rb -@@ -590,6 +590,10 @@ describe Rack::Utils, "cookies" do - end - - describe Rack::Utils, "byte_range" do -+ it "returns an empty list if the sum of the ranges is too large" do -+ assert_equal [], Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-20,0-500" }, 500) -+ end -+ - 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 --- -2.30.2 - diff --git a/debian/patches/0003-Fixing-ReDoS-in-header-parsing.patch b/debian/patches/0003-Fixing-ReDoS-in-header-parsing.patch deleted file mode 100644 index 7094e878e87f3af8ccd44928a32d25d7a036b7e3..0000000000000000000000000000000000000000 --- a/debian/patches/0003-Fixing-ReDoS-in-header-parsing.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 3f0a5391ed7118f10bae56b369b2c525942f26c6 Mon Sep 17 00:00:00 2001 -From: Aaron Patterson <tenderlove@ruby-lang.org> -Date: Wed, 21 Feb 2024 11:05:06 -0800 -Subject: Fixing ReDoS in header parsing - -Thanks svalkanov - -[CVE-2024-26146] ---- - lib/rack/utils.rb | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb -index 72700503..ccf39e30 100644 ---- a/lib/rack/utils.rb -+++ b/lib/rack/utils.rb -@@ -142,8 +142,8 @@ module Rack - end - - def q_values(q_value_header) -- q_value_header.to_s.split(/\s*,\s*/).map do |part| -- value, parameters = part.split(/\s*;\s*/, 2) -+ q_value_header.to_s.split(',').map do |part| -+ value, parameters = part.split(';', 2).map(&:strip) - quality = 1.0 - if parameters && (md = /\Aq=([\d.]+)/.match(parameters)) - quality = md[1].to_f --- -2.30.2 - diff --git a/debian/patches/series b/debian/patches/series index c1ebda6c976d30d9b16b92a735e6a534b71525d7..dcb4bed77e1e20f9f03c36e4997e79250229de91 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1,6 +1 @@ -skip-random-failure.patch -0002-Make-tests-pass-on-hosts-that-have-no-ipv4-connectiv.patch skip-unreadable-dir-test.patch -0001-Avoid-2nd-degree-polynomial-regexp-in-MediaType.patch -0002-Return-an-empty-array-when-ranges-are-too-large.patch -0003-Fixing-ReDoS-in-header-parsing.patch diff --git a/debian/patches/skip-random-failure.patch b/debian/patches/skip-random-failure.patch deleted file mode 100644 index d6a301b8ba0877d5058d5d89ce6795db78c32a71..0000000000000000000000000000000000000000 --- a/debian/patches/skip-random-failure.patch +++ /dev/null @@ -1,69 +0,0 @@ -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 -@@ -261,6 +261,7 @@ - end - - it "strips leading unicode byte order mark when present" do -+ skip - enc = Encoding.default_external - begin - Encoding.default_external = 'UTF-8' ---- a/test/spec_thin.rb -+++ b/test/spec_thin.rb -@@ -30,11 +30,13 @@ - - - it "respond" do -+ skip - GET("/") - response.wont_be :nil? - end - - it "be a Thin" do -+ skip - GET("/") - status.must_equal 200 - response["SERVER_SOFTWARE"].must_match(/thin/) -@@ -45,6 +47,7 @@ - end - - it "have rack headers" do -+ skip - GET("/") - response["rack.version"].must_equal [1, 0] - response["rack.multithread"].must_equal false -@@ -53,6 +56,7 @@ - end - - it "have CGI headers on GET" do -+ skip - GET("/") - response["REQUEST_METHOD"].must_equal "GET" - response["REQUEST_PATH"].must_equal "/" -@@ -68,6 +72,7 @@ - end - - it "have CGI headers on POST" do -+ skip - POST("/", { "rack-form-data" => "23" }, { 'X-test-header' => '42' }) - status.must_equal 200 - response["REQUEST_METHOD"].must_equal "POST" -@@ -78,11 +83,13 @@ - end - - it "support HTTP auth" do -+ skip - GET("/test", { user: "ruth", passwd: "secret" }) - response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ=" - end - - it "set status" do -+ skip - GET("/test?secret") - status.must_equal 403 - response["rack.url_scheme"].must_equal "http" diff --git a/debian/patches/skip-unreadable-dir-test.patch b/debian/patches/skip-unreadable-dir-test.patch index 64a5ebe49b716462a606fcee5841154c47bc364f..58ec9e30aab0193baf21b5068093d8cce9a8b703 100644 --- a/debian/patches/skip-unreadable-dir-test.patch +++ b/debian/patches/skip-unreadable-dir-test.patch @@ -4,11 +4,9 @@ Description: skip unreadable directories test Author: HIGUCHI Daisuke (VDR dai) <dai@debian.org> Forwarded: not-needed -Index: ruby-rack/test/spec_directory.rb -=================================================================== ---- ruby-rack.orig/test/spec_directory.rb -+++ ruby-rack/test/spec_directory.rb -@@ -54,6 +54,7 @@ describe Rack::Directory do +--- a/test/spec_directory.rb ++++ b/test/spec_directory.rb +@@ -84,6 +84,7 @@ end it "return 404 for unreadable directories" do diff --git a/debian/rackup.1 b/debian/rackup.1 deleted file mode 100644 index 71b90f605173363725a1d10a07d0fee4e8eba7ab..0000000000000000000000000000000000000000 --- a/debian/rackup.1 +++ /dev/null @@ -1,107 +0,0 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "RACKUP" "1" "December 2010" "" "" -. -.SH "NAME" -\fBrackup\fR -. -.P -rackup(1) \-\- An utility for run Rack\-based applications -. -.SH "SYNOPSIS" -\fBrackup\fR [ruby options] [rack options] [rackup config] -. -.SH "DESCRIPTION" -Rackup is a useful tool for running Rack applications, which uses the Rack::Builder DSL to configure middleware and build up applications easily\. -. -.P -rackup automatically figures out the environment it is run in, and runs your application as FastCGI, CGI, or standalone with Mongrel or WEBrick \-all from the same configuration\. -. -.SH "OPTIONS" -Ruby options: -. -.TP -\fB\-e\fR, \fB\-\-eval\fR [LINE] -evaluate a LINE of code -. -.TP -\fB\-d\fR, \fB\-\-debug\fR -set debugging flags (set $DEBUG to true) -. -.TP -\fB\-w\fR, \fB\-\-warn\fR -turn warnings on for your script -. -.TP -\fB\-I\fR, \fB\-\-include\fR [PATH] -specify $LOAD_PATH (may be used more than once) -. -.TP -\fB\-r\fR, \fB\-\-require\fR [LIBRARY] -require the library, before executing your script -. -.P -Rack options: -. -.TP -\fB\-s\fR, \fB\-\-server\fR [SERVER] -serve using SERVER (webrick/mongrel) -. -.TP -\fB\-o\fR, \fB\-\-host\fR [HOST] -listen on HOST (default: 0\.0\.0\.0) -. -.TP -\fB\-p\fR, \fB\-\-port\fR [PORT] -use PORT (default: 9292) -. -.TP -\fB\-E\fR, \fB\-\-env\fR [ENVIRONMENT] -use ENVIRONMENT for defaults (default: development) -. -.TP -\fB\-D\fR, \fB\-\-daemonize\fR -run daemonized in the background -. -.TP -\fB\-P\fR, \fB\-\-pid\fR [FILE] -file to store PID (default: rack\.pid) -. -.P -Common options: -. -.TP -\fB\-h\fR, \fB\-\-help\fR -Show the help message -. -.TP -\fB\-\-version\fR -Show version -. -.SH "EXAMPLES" -This is a simple example on how to start an application based on Rack with rackup: -. -.P -$ rackup \-Ilib blog/config\.ru -. -.P -[2010\-12\-10 15:01:11] INFO WEBrick 1\.3\.1 -. -.P -[2010\-12\-10 15:01:11] INFO ruby 1\.9\.2 (2010\-08\-18) [x86_64\-linux] -. -.P -[2010\-12\-10 15:01:11] INFO WEBrick::HTTPServer#start: pid=4496 port=9292 -. -.SH "AUTHORS" -Copyright (C) 2007, 2008, 2009, 2010 \fBChristian Neukirchen\fR \fB<http://purl\.org/net/chneukirchen>\fR -. -.P -For a complete list of authors and contributors to the project, please take a look here \fIhttps://github\.com/rack/rack/contributors\fR -. -.P -This manual page was written by Ermenegildo Fiorito \fIfiorito\.g@gmail\.com\fR for the Debian Project -. -.SH "SEE ALSO" -ruby(1) \fIhttp://rack\.rubyforge\.org\fR diff --git a/debian/ruby-rack.docs b/debian/ruby-rack.docs index e475c53ed013742672b136c5dee9af35978d96c8..b43bf86b50fd8d3529a0dc062c30006ed38f309e 100644 --- a/debian/ruby-rack.docs +++ b/debian/ruby-rack.docs @@ -1 +1 @@ -README.rdoc +README.md diff --git a/debian/ruby-rack.manpages b/debian/ruby-rack.manpages deleted file mode 100644 index fecd28026a3e21d5da9d696368b3e092ff8ba05b..0000000000000000000000000000000000000000 --- a/debian/ruby-rack.manpages +++ /dev/null @@ -1 +0,0 @@ -debian/rackup.1 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 c47e3d35134b8086ddfbc49bf788fe5b703637ae..dc6a886a72290f49908f9beacd13cf67c780b670 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,6 +1,6 @@ Tests: smoke-test -Depends: @, curl, ruby-webrick +Depends: @, curl, ruby-rackup, ruby-webrick Test-Command: gem2deb-test-runner -Depends: @, gem2deb-test-runner, rake, ruby-bacon, thin, ruby-minitest-global-expectations, ruby-webrick +Depends: @, gem2deb-test-runner, rake, ruby-bacon, ruby-minitest-global-expectations, ruby-webrick Restrictions: allow-stderr diff --git a/debian/tests/smoke-test b/debian/tests/smoke-test index ad25a547d950fbcfa45b2fe143d4112498f23a0b..cf187a9a2ded1f6995e72c7a4c394e04327e55ee 100755 --- a/debian/tests/smoke-test +++ b/debian/tests/smoke-test @@ -9,7 +9,7 @@ mkdir $appdir cd $appdir cat > config.ru <<EOF -run(lambda { |request| [200, {'Content-Type' => 'text/html'}, ['Hello, world!']]}) +run(lambda { |request| [200, {'content-type' => 'text/html'}, ['Hello, world!']]}) EOF rackup --pid smoke.pid --daemonize diff --git a/lib/rack.rb b/lib/rack.rb index e4494e5bac40721ac9bd64aee047bc1c304f563b..b37c00cdee67492222d2a750c155daa39ecf4680 100644 --- a/lib/rack.rb +++ b/lib/rack.rb @@ -12,70 +12,9 @@ # so it should be enough just to <tt>require 'rack'</tt> in your code. require_relative 'rack/version' +require_relative 'rack/constants' module Rack - HTTP_HOST = 'HTTP_HOST' - HTTP_PORT = 'HTTP_PORT' - 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_PORT = 'SERVER_PORT' - CACHE_CONTROL = 'Cache-Control' - EXPIRES = 'Expires' - 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' - 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' - 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" autoload :Cascade, "rack/cascade" @@ -94,6 +33,7 @@ module Rack autoload :ForwardRequest, "rack/recursive" autoload :Handler, "rack/handler" autoload :Head, "rack/head" + autoload :Headers, "rack/headers" autoload :Lint, "rack/lint" autoload :Lock, "rack/lock" autoload :Logger, "rack/logger" @@ -101,6 +41,7 @@ module Rack autoload :MethodOverride, "rack/method_override" autoload :Mime, "rack/mime" autoload :NullLogger, "rack/null_logger" + autoload :QueryParser, "rack/query_parser" autoload :Recursive, "rack/recursive" autoload :Reloader, "rack/reloader" autoload :RewindableInput, "rack/rewindable_input" @@ -115,8 +56,8 @@ module Rack autoload :Utils, "rack/utils" autoload :Multipart, "rack/multipart" - autoload :MockRequest, "rack/mock" - autoload :MockResponse, "rack/mock" + autoload :MockRequest, "rack/mock_request" + autoload :MockResponse, "rack/mock_response" autoload :Request, "rack/request" autoload :Response, "rack/response" @@ -125,17 +66,6 @@ module Rack autoload :Basic, "rack/auth/basic" autoload :AbstractRequest, "rack/auth/abstract/request" autoload :AbstractHandler, "rack/auth/abstract/handler" - module Digest - autoload :MD5, "rack/auth/digest/md5" - autoload :Nonce, "rack/auth/digest/nonce" - autoload :Params, "rack/auth/digest/params" - autoload :Request, "rack/auth/digest/request" - end - end - - module Session - autoload :Cookie, "rack/session/cookie" - autoload :Pool, "rack/session/pool" - autoload :Memcache, "rack/session/memcache" + autoload :Digest, "rack/auth/digest" end end diff --git a/lib/rack/auth/abstract/handler.rb b/lib/rack/auth/abstract/handler.rb index 3ed87091c7e7a33f8bcaa76e523c254a66e4f9fe..4731ee8c85d7b9f3474a90a459db5ec770422241 100644 --- a/lib/rack/auth/abstract/handler.rb +++ b/lib/rack/auth/abstract/handler.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../../constants' + module Rack module Auth # Rack::Auth::AbstractHandler implements common authentication functionality. @@ -21,7 +23,7 @@ module Rack return [ 401, { CONTENT_TYPE => 'text/plain', CONTENT_LENGTH => '0', - 'WWW-Authenticate' => www_authenticate.to_s }, + 'www-authenticate' => www_authenticate.to_s }, [] ] end diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb index 34042c401b7b89a38734632134bef3459185f342..f872331563ebbd8bf9527c1cd83969a3553b2160 100644 --- a/lib/rack/auth/abstract/request.rb +++ b/lib/rack/auth/abstract/request.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../../request' + module Rack module Auth class AbstractRequest @@ -25,7 +27,7 @@ module Rack end def scheme - @scheme ||= parts.first && parts.first.downcase + @scheme ||= parts.first&.downcase end def params diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb index d5b4ea16da3d9ceef01bdd499062b7c0d3178a2f..019efde75e9db519ffa2651196ec90f390c4b120 100644 --- a/lib/rack/auth/basic.rb +++ b/lib/rack/auth/basic.rb @@ -10,8 +10,6 @@ module Rack # # Initialize with the Rack application that you want protecting, # and a block that checks if a username and password pair are valid. - # - # See also: <tt>example/protectedlobster.rb</tt> class Basic < AbstractHandler diff --git a/lib/rack/auth/digest.rb b/lib/rack/auth/digest.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9f818b9e58db9a1f30546c9cf64bc79e4a8c186 --- /dev/null +++ b/lib/rack/auth/digest.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +require_relative 'abstract/handler' +require_relative 'abstract/request' +require 'digest/md5' +require 'base64' + +module Rack + warn "Rack::Auth::Digest is deprecated and will be removed in Rack 3.1", uplevel: 1 + + module Auth + module Digest + # Rack::Auth::Digest::Nonce is the default nonce generator for the + # Rack::Auth::Digest::MD5 authentication handler. + # + # +private_key+ needs to set to a constant string. + # + # +time_limit+ can be optionally set to an integer (number of seconds), + # to limit the validity of the generated nonces. + + class Nonce + + class << self + attr_accessor :private_key, :time_limit + end + + def self.parse(string) + new(*Base64.decode64(string).split(' ', 2)) + end + + def initialize(timestamp = Time.now, given_digest = nil) + @timestamp, @given_digest = timestamp.to_i, given_digest + end + + def to_s + Base64.encode64("#{@timestamp} #{digest}").strip + end + + def digest + ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}") + end + + def valid? + digest == @given_digest + end + + def stale? + !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit + end + + def fresh? + !stale? + end + + end + + class Params < Hash + + def self.parse(str) + Params[*split_header_value(str).map do |param| + k, v = param.split('=', 2) + [k, dequote(v)] + end.flatten] + end + + def self.dequote(str) # From WEBrick::HTTPUtils + ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup + ret.gsub!(/\\(.)/, "\\1") + ret + end + + def self.split_header_value(str) + str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n) + end + + def initialize + super() + + yield self if block_given? + end + + def [](k) + super k.to_s + end + + def []=(k, v) + super k.to_s, v.to_s + end + + UNQUOTED = ['nc', 'stale'] + + def to_s + map do |k, v| + "#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}" + end.join(', ') + end + + def quote(str) # From WEBrick::HTTPUtils + '"' + str.gsub(/[\\\"]/o, "\\\1") + '"' + end + + end + + class Request < Auth::AbstractRequest + def method + @env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || @env[REQUEST_METHOD] + end + + def digest? + "digest" == scheme + end + + def correct_uri? + request.fullpath == uri + end + + def nonce + @nonce ||= Nonce.parse(params['nonce']) + end + + def params + @params ||= Params.parse(parts.last) + end + + def respond_to?(sym, *) + super or params.has_key? sym.to_s + end + + def method_missing(sym, *args) + return super unless params.has_key?(key = sym.to_s) + return params[key] if args.size == 0 + raise ArgumentError, "wrong number of arguments (#{args.size} for 0)" + end + end + + # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of + # HTTP Digest Authentication, as per RFC 2617. + # + # Initialize with the [Rack] application that you want protecting, + # and a block that looks up a plaintext password for a given username. + # + # +opaque+ needs to be set to a constant base64/hexadecimal string. + # + class MD5 < AbstractHandler + + attr_accessor :opaque + + attr_writer :passwords_hashed + + 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 + end + super(app, realm, &authenticator) + @opaque = opaque + end + + def passwords_hashed? + !!@passwords_hashed + end + + def call(env) + auth = Request.new(env) + + unless auth.provided? + return unauthorized + end + + if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth) + return bad_request + end + + if valid?(auth) + if auth.nonce.stale? + return unauthorized(challenge(stale: true)) + else + env['REMOTE_USER'] = auth.username + + return @app.call(env) + end + end + + unauthorized + end + + + private + + QOP = 'auth' + + def params(hash = {}) + Params.new do |params| + params['realm'] = realm + params['nonce'] = Nonce.new.to_s + params['opaque'] = H(opaque) + params['qop'] = QOP + + hash.each { |k, v| params[k] = v } + end + end + + def challenge(hash = {}) + "Digest #{params(hash)}" + end + + def valid?(auth) + valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth) + end + + def valid_qop?(auth) + QOP == auth.qop + end + + def valid_opaque?(auth) + H(opaque) == auth.opaque + end + + def valid_nonce?(auth) + auth.nonce.valid? + end + + def valid_digest?(auth) + pw = @authenticator.call(auth.username) + pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response) + end + + def md5(data) + ::Digest::MD5.hexdigest(data) + end + + alias :H :md5 + + def KD(secret, data) + H "#{secret}:#{data}" + end + + def A1(auth, password) + "#{auth.username}:#{auth.realm}:#{password}" + end + + def A2(auth) + "#{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)}" + end + + end + end + end +end + diff --git a/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb index 04b103e2583fc41157a2720b6b04ed8c1643df5f..828eccac87e90520be4781b1765fdd0a0100fda5 100644 --- a/lib/rack/auth/digest/md5.rb +++ b/lib/rack/auth/digest/md5.rb @@ -1,131 +1 @@ -# frozen_string_literal: true - -require_relative '../abstract/handler' -require_relative 'request' -require_relative 'params' -require_relative 'nonce' -require 'digest/md5' - -module Rack - module Auth - module Digest - # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of - # HTTP Digest Authentication, as per RFC 2617. - # - # Initialize with the [Rack] application that you want protecting, - # and a block that looks up a plaintext password for a given username. - # - # +opaque+ needs to be set to a constant base64/hexadecimal string. - # - class MD5 < AbstractHandler - - attr_accessor :opaque - - attr_writer :passwords_hashed - - 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 - end - super(app, realm, &authenticator) - @opaque = opaque - end - - def passwords_hashed? - !!@passwords_hashed - end - - def call(env) - auth = Request.new(env) - - unless auth.provided? - return unauthorized - end - - if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth) - return bad_request - end - - if valid?(auth) - if auth.nonce.stale? - return unauthorized(challenge(stale: true)) - else - env['REMOTE_USER'] = auth.username - - return @app.call(env) - end - end - - unauthorized - end - - - private - - QOP = 'auth' - - def params(hash = {}) - Params.new do |params| - params['realm'] = realm - params['nonce'] = Nonce.new.to_s - params['opaque'] = H(opaque) - params['qop'] = QOP - - hash.each { |k, v| params[k] = v } - end - end - - def challenge(hash = {}) - "Digest #{params(hash)}" - end - - def valid?(auth) - valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth) - end - - def valid_qop?(auth) - QOP == auth.qop - end - - def valid_opaque?(auth) - H(opaque) == auth.opaque - end - - def valid_nonce?(auth) - auth.nonce.valid? - end - - def valid_digest?(auth) - pw = @authenticator.call(auth.username) - pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response) - end - - def md5(data) - ::Digest::MD5.hexdigest(data) - end - - alias :H :md5 - - def KD(secret, data) - H "#{secret}:#{data}" - end - - def A1(auth, password) - "#{auth.username}:#{auth.realm}:#{password}" - end - - def A2(auth) - "#{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)}" - end - - end - end - end -end +require_relative '../digest' diff --git a/lib/rack/auth/digest/nonce.rb b/lib/rack/auth/digest/nonce.rb index 3216d973e0daddfe0493588fed27d35c4f46a665..828eccac87e90520be4781b1765fdd0a0100fda5 100644 --- a/lib/rack/auth/digest/nonce.rb +++ b/lib/rack/auth/digest/nonce.rb @@ -1,54 +1 @@ -# frozen_string_literal: true - -require 'digest/md5' -require 'base64' - -module Rack - module Auth - module Digest - # Rack::Auth::Digest::Nonce is the default nonce generator for the - # Rack::Auth::Digest::MD5 authentication handler. - # - # +private_key+ needs to set to a constant string. - # - # +time_limit+ can be optionally set to an integer (number of seconds), - # to limit the validity of the generated nonces. - - class Nonce - - class << self - attr_accessor :private_key, :time_limit - end - - def self.parse(string) - new(*Base64.decode64(string).split(' ', 2)) - end - - def initialize(timestamp = Time.now, given_digest = nil) - @timestamp, @given_digest = timestamp.to_i, given_digest - end - - def to_s - Base64.encode64("#{@timestamp} #{digest}").strip - end - - def digest - ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}") - end - - def valid? - digest == @given_digest - end - - def stale? - !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit - end - - def fresh? - !stale? - end - - end - end - end -end +require_relative '../digest' diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb index f611b3c35eb4dee2a05a1cfae8c7ceb66e5feb0d..828eccac87e90520be4781b1765fdd0a0100fda5 100644 --- a/lib/rack/auth/digest/params.rb +++ b/lib/rack/auth/digest/params.rb @@ -1,54 +1 @@ -# frozen_string_literal: true - -module Rack - module Auth - module Digest - class Params < Hash - - def self.parse(str) - Params[*split_header_value(str).map do |param| - k, v = param.split('=', 2) - [k, dequote(v)] - end.flatten] - end - - def self.dequote(str) # From WEBrick::HTTPUtils - ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup - ret.gsub!(/\\(.)/, "\\1") - ret - end - - def self.split_header_value(str) - str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n) - end - - def initialize - super() - - yield self if block_given? - end - - def [](k) - super k.to_s - end - - def []=(k, v) - super k.to_s, v.to_s - end - - UNQUOTED = ['nc', 'stale'] - - def to_s - map do |k, v| - "#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}" - end.join(', ') - end - - def quote(str) # From WEBrick::HTTPUtils - '"' + str.gsub(/[\\\"]/o, "\\\1") + '"' - end - - end - end - end -end +require_relative '../digest' diff --git a/lib/rack/auth/digest/request.rb b/lib/rack/auth/digest/request.rb index 7b89b7605227294788c340ac63e8a3fab7a28229..828eccac87e90520be4781b1765fdd0a0100fda5 100644 --- a/lib/rack/auth/digest/request.rb +++ b/lib/rack/auth/digest/request.rb @@ -1,43 +1 @@ -# frozen_string_literal: true - -require_relative '../abstract/request' -require_relative 'params' -require_relative 'nonce' - -module Rack - module Auth - module Digest - class Request < Auth::AbstractRequest - def method - @env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || @env[REQUEST_METHOD] - end - - def digest? - "digest" == scheme - end - - def correct_uri? - request.fullpath == uri - end - - def nonce - @nonce ||= Nonce.parse(params['nonce']) - end - - def params - @params ||= Params.parse(parts.last) - end - - def respond_to?(sym, *) - super or params.has_key? sym.to_s - end - - def method_missing(sym, *args) - return super unless params.has_key?(key = sym.to_s) - return params[key] if args.size == 0 - raise ArgumentError, "wrong number of arguments (#{args.size} for 0)" - end - end - end - end -end +require_relative '../digest' diff --git a/lib/rack/body_proxy.rb b/lib/rack/body_proxy.rb index cfc0796a612e0c980ab58e809cbee93509430c9b..fbb344b81029f9284628d636be35fb2c0982be81 100644 --- a/lib/rack/body_proxy.rb +++ b/lib/rack/body_proxy.rb @@ -24,7 +24,7 @@ module Rack return if @closed @closed = true begin - @body.close if @body.respond_to? :close + @body.close if @body.respond_to?(:close) ensure @block.call end @@ -40,6 +40,8 @@ module Rack def method_missing(method_name, *args, &block) @body.__send__(method_name, *args, &block) end + # :nocov: ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true) + # :nocov: end end diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index 816ecf62085eff3518a0c7efd3a5122406777e4a..0b9c3d24a2d493219e79dfd9f86bd8100bb61e71 100644 --- a/lib/rack/builder.rb +++ b/lib/rack/builder.rb @@ -1,35 +1,35 @@ # frozen_string_literal: true +require_relative 'urlmap' + module Rack - # Rack::Builder implements a small DSL to iteratively construct Rack - # applications. + # Rack::Builder provides a domain-specific language (DSL) to construct Rack + # applications. It is primarily used to parse +config.ru+ files which + # instantiate several middleware and a final application which are hosted + # by a Rack-compatible web server. # # Example: # - # require 'rack/lobster' - # app = Rack::Builder.new do - # use Rack::CommonLogger - # use Rack::ShowExceptions - # map "/lobster" do - # use Rack::Lint - # run Rack::Lobster.new - # end - # end + # app = Rack::Builder.new do + # use Rack::CommonLogger + # map "/ok" do + # run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] } + # end + # end # - # run app + # run app # # Or # - # app = Rack::Builder.app do - # use Rack::CommonLogger - # run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] } - # end + # app = Rack::Builder.app do + # use Rack::CommonLogger + # run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] } + # end # - # run app + # run app # # +use+ adds middleware to the stack, +run+ dispatches to an application. # You can use +map+ to construct a Rack::URLMap in a convenient way. - class Builder # https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom @@ -39,13 +39,11 @@ module Rack # # If the config file ends in +.ru+, it is treated as a # rackup file and the contents will be treated as if - # specified inside a Rack::Builder block, using the given - # options. + # specified inside a Rack::Builder block. # # If the config file does not end in +.ru+, it is # required and Rack will use the basename of the file # to guess which constant will be the Rack application to run. - # The options given will be ignored in this case. # # Examples: # @@ -61,23 +59,18 @@ module Rack # # requires ./my_app.rb, which should be in the # # process's current directory. After requiring, # # assumes MyApp constant contains Rack application - def self.parse_file(config, opts = Server::Options.new) - if config.end_with?('.ru') - return self.load_file(config, opts) + def self.parse_file(path) + if path.end_with?('.ru') + return self.load_file(path) else - require config - app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join('')) - return app, {} + require path + return Object.const_get(::File.basename(path, '.rb').split('_').map(&:capitalize).join('')) end end # Load the given file as a rackup file, treating the # contents as if specified inside a Rack::Builder block. # - # Treats the first comment at the beginning of a line - # that starts with a backslash as options similar to - # options passed on a rackup command line. - # # Ignores content in the file after +__END__+, so that # use of +__END__+ will not result in a syntax error. # @@ -85,26 +78,20 @@ module Rack # # $ cat config.ru # - # #\ -p 9393 - # # use Rack::ContentLength # require './app.rb' # run App - 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 + def self.load_file(path) + config = ::File.read(path) + config.slice!(/\A#{UTF_8_BOM}/) if config.encoding == Encoding::UTF_8 - if cfgfile[/^#\\(.*)/] && opts - warn "Parsing options from the first comment line is deprecated!" - options = opts.parse! $1.split(/\s+/) + if config[/^#\\(.*)/] + fail "Parsing options from the first comment line is no longer supported: #{path}" end - cfgfile.sub!(/^__END__\n.*\Z/m, '') - app = new_from_string cfgfile, path + config.sub!(/^__END__\n.*\Z/m, '') - return app, options + return new_from_string(config, path) end # Evaluate the given +builder_script+ string in the context of @@ -114,14 +101,20 @@ module Rack # We cannot use instance_eval(String) as that would resolve constants differently. binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }') eval builder_script, binding, file - builder.to_app + + return builder.to_app end # Initialize a new Rack::Builder instance. +default_app+ specifies the # default application if +run+ is not called later. If a block - # is given, it is evaluted in the context of the instance. + # is given, it is evaluated in the context of the instance. def initialize(default_app = nil, &block) - @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false + @use = [] + @map = nil + @run = default_app + @warmup = nil + @freeze_app = false + instance_eval(&block) if block_given? end @@ -145,7 +138,7 @@ module Rack # end # # use Middleware - # run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] } + # run lambda { |env| [200, { "content-type" => "text/plain" }, ["OK"]] } # # All requests through to this application will first be processed by the middleware class. # The +call+ method in this example sets an additional environment key which then can be @@ -157,24 +150,37 @@ module Rack end @use << proc { |app| middleware.new(app, *args, &block) } end + # :nocov: ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true) + # :nocov: - # 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: + # Takes a block or argument that is an object that responds to #call and + # returns a Rack response. + # + # You can use a block: + # + # run do |env| + # [200, { "content-type" => "text/plain" }, ["Hello World!"]] + # end + # + # You can also provide a lambda: # - # run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] } + # run lambda { |env| [200, { "content-type" => "text/plain" }, ["OK"]] } # - # However this could also be a class: + # You can also provide a class instance: # # class Heartbeat - # def self.call(env) - # [200, { "Content-Type" => "text/plain" }, ["OK"]] + # def call(env) + # [200, { "content-type" => "text/plain" }, ["OK"]] # end # end # - # run Heartbeat - def run(app) - @run = app + # run Heartbeat.new + # + def run(app = nil, &block) + raise ArgumentError, "Both app and block given!" if app && block_given? + + @run = app || block end # Takes a lambda or block that is used to warm-up the application. This block is called @@ -195,21 +201,35 @@ module Rack # the Rack application specified by run inside the block. Other requests will be sent to the # default application specified by run outside the block. # - # Rack::Builder.app do + # class App + # def call(env) + # [200, {'content-type' => 'text/plain'}, ["Hello World"]] + # end + # end + # + # class Heartbeat + # def call(env) + # [200, { "content-type" => "text/plain" }, ["OK"]] + # end + # end + # + # app = Rack::Builder.app do # map '/heartbeat' do - # run Heartbeat + # run Heartbeat.new # end - # run App + # run App.new # end # + # run app + # # The +use+ method can also be used inside the block to specify middleware to run under a specific path: # - # Rack::Builder.app do + # app = Rack::Builder.app do # map '/heartbeat' do # use Middleware - # run Heartbeat + # run Heartbeat.new # end - # run App + # run App.new # end # # This example includes a piece of middleware which will run before +/heartbeat+ requests hit +Heartbeat+. diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb index d71274c2b7cca26d65a455e1635662f543053fc9..027d7e4045fb87d8f718f74c5ec92f66a2405ebe 100644 --- a/lib/rack/cascade.rb +++ b/lib/rack/cascade.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'constants' + 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 configured diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb index 84c6600140524118800b87918cc8fed8dcff33a0..47fb36ac1fe0e96bac7ef05eebd0951527eb1a08 100644 --- a/lib/rack/chunked.rb +++ b/lib/rack/chunked.rb @@ -1,22 +1,26 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' + module Rack + warn "Rack::Chunked is deprecated and will be removed in Rack 3.1", uplevel: 1 # Middleware that applies chunked transfer encoding to response bodies - # when the response does not include a Content-Length header. + # when the response does not include a content-length header. # - # This supports the Trailer response header to allow the use of trailing + # This supports the trailer response header to allow the use of trailing # headers in the chunked encoding. However, using this requires you manually # specify a response body that supports a +trailers+ method. Example: # - # [200, { 'Trailer' => 'Expires'}, ["Hello", "World"]] + # [200, { 'trailer' => 'expires'}, ["Hello", "World"]] # # error raised # # body = ["Hello", "World"] # def body.trailers - # { 'Expires' => Time.now.to_s } + # { 'expires' => Time.now.to_s } # end - # [200, { 'Trailer' => 'Expires'}, body] + # [200, { 'trailer' => 'expires'}, body] # # No exception raised class Chunked include Rack::Utils @@ -92,11 +96,10 @@ module Rack end # If the rack app returns a response that should have a body, - # but does not have Content-Length or Transfer-Encoding headers, - # modify the response to use chunked Transfer-Encoding. + # but does not have content-length or transfer-encoding headers, + # modify the response to use chunked transfer-encoding. def call(env) - status, headers, body = @app.call(env) - headers = HeaderHash[headers] + status, headers, body = response = @app.call(env) if chunkable_version?(env[SERVER_PROTOCOL]) && !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && @@ -104,14 +107,14 @@ module Rack !headers[TRANSFER_ENCODING] headers[TRANSFER_ENCODING] = 'chunked' - if headers['Trailer'] - body = TrailerBody.new(body) + if headers['trailer'] + response[2] = TrailerBody.new(body) else - body = Body.new(body) + response[2] = Body.new(body) end end - [status, headers, body] + response end end end diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb index 9c6f92147d946ce5af73a819722e1819a3f774a5..2feb0674649e716e50064a86bae7f663c77b391e 100644 --- a/lib/rack/common_logger.rb +++ b/lib/rack/common_logger.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' +require_relative 'body_proxy' +require_relative 'request' + module Rack # Rack::CommonLogger forwards every request to the given +app+, and # logs a line in the @@ -35,35 +40,35 @@ module Rack # cause the request not to be logged. def call(env) began_at = Utils.clock_time - status, headers, body = @app.call(env) - headers = Utils::HeaderHash[headers] - body = BodyProxy.new(body) { log(env, status, headers, began_at) } - [status, headers, body] + status, headers, body = response = @app.call(env) + + response[2] = BodyProxy.new(body) { log(env, status, headers, began_at) } + response end private # Log the request to the configured logger. - def log(env, status, header, began_at) - length = extract_content_length(header) + def log(env, status, response_headers, began_at) + request = Rack::Request.new(env) + length = extract_content_length(response_headers) - msg = FORMAT % [ - env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", - env["REMOTE_USER"] || "-", + msg = sprintf(FORMAT, + request.ip || "-", + request.get_header("REMOTE_USER") || "-", Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"), - env[REQUEST_METHOD], - env[SCRIPT_NAME], - env[PATH_INFO], - env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}", - env[SERVER_PROTOCOL], + request.request_method, + request.script_name, + request.path_info, + request.query_string.empty? ? "" : "?#{request.query_string}", + request.get_header(SERVER_PROTOCOL), status.to_s[0..3], length, - Utils.clock_time - began_at ] - - msg.gsub!(/[^[:print:]\n]/) { |c| "\\x#{c.ord}" } + Utils.clock_time - began_at) - logger = @logger || env[RACK_ERRORS] + msg.gsub!(/[^[:print:]\n]/) { |c| sprintf("\\x%x", c.ord) } + logger = @logger || request.get_header(RACK_ERRORS) # Standard library logger doesn't support write but it supports << which actually # calls to write on the log device without formatting if logger.respond_to?(:write) diff --git a/lib/rack/conditional_get.rb b/lib/rack/conditional_get.rb index 7b7808ac1f77095264c90650d8878de1d736e9c0..c3b334a2e2b4d227fffb29a410c6a30efd3e046d 100644 --- a/lib/rack/conditional_get.rb +++ b/lib/rack/conditional_get.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' +require_relative 'body_proxy' + module Rack - # Middleware that enables conditional GET using If-None-Match and - # If-Modified-Since. The application should set either or both of the - # Last-Modified or Etag response headers according to RFC 2616. When + # Middleware that enables conditional GET using if-none-match and + # if-modified-since. The application should set either or both of the + # last-modified or etag response headers according to RFC 2616. When # either of the conditions is met, the response body is set to be zero # length and the response status is set to 304 Not Modified. # @@ -24,18 +28,17 @@ module Rack def call(env) case env[REQUEST_METHOD] when "GET", "HEAD" - status, headers, body = @app.call(env) - headers = Utils::HeaderHash[headers] + status, headers, body = response = @app.call(env) + if status == 200 && fresh?(env, headers) - status = 304 + response[0] = 304 headers.delete(CONTENT_TYPE) headers.delete(CONTENT_LENGTH) - original_body = body - body = Rack::BodyProxy.new([]) do - original_body.close if original_body.respond_to?(:close) + response[2] = Rack::BodyProxy.new([]) do + body.close if body.respond_to?(:close) end end - [status, headers, body] + response else @app.call(env) end @@ -46,7 +49,7 @@ module Rack # Return whether the response has not been modified since the # last request. def fresh?(env, headers) - # If-None-Match has priority over If-Modified-Since per RFC 7232 + # if-none-match has priority over if-modified-since per RFC 7232 if none_match = env['HTTP_IF_NONE_MATCH'] etag_matches?(none_match, headers) elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since)) @@ -54,16 +57,16 @@ module Rack end end - # Whether the ETag response header matches the If-None-Match request header. + # Whether the etag response header matches the if-none-match request header. # If so, the request has not been modified. def etag_matches?(none_match, headers) - headers['ETag'] == none_match + headers[ETAG] == none_match end - # Whether the Last-Modified response header matches the If-Modified-Since + # Whether the last-modified response header matches the if-modified-since # request header. If so, the request has not been modified. def modified_since?(modified_since, headers) - last_modified = to_rfc2822(headers['Last-Modified']) and + last_modified = to_rfc2822(headers['last-modified']) and modified_since >= last_modified end diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb new file mode 100644 index 0000000000000000000000000000000000000000..13365935b1b5b8d679598823f776f1b9ee5a4a89 --- /dev/null +++ b/lib/rack/constants.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Rack + # Request env keys + HTTP_HOST = 'HTTP_HOST' + HTTP_PORT = 'HTTP_PORT' + 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_PORT = 'SERVER_PORT' + HTTP_COOKIE = 'HTTP_COOKIE' + + # Response Header Keys + CACHE_CONTROL = 'cache-control' + CONTENT_LENGTH = 'content-length' + CONTENT_TYPE = 'content-type' + ETAG = 'etag' + EXPIRES = 'expires' + SET_COOKIE = 'set-cookie' + TRANSFER_ENCODING = 'transfer-encoding' + + # HTTP method verbs + 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' + 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_URL_SCHEME = 'rack.url_scheme' + RACK_HIJACK = 'rack.hijack' + RACK_IS_HIJACK = 'rack.hijack?' + RACK_RECURSIVE_INCLUDE = 'rack.recursive.include' + RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size' + RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory' + RACK_RESPONSE_FINISHED = 'rack.response_finished' + RACK_REQUEST_FORM_INPUT = 'rack.request.form_input' + RACK_REQUEST_FORM_HASH = 'rack.request.form_hash' + RACK_REQUEST_FORM_VARS = 'rack.request.form_vars' + RACK_REQUEST_FORM_ERROR = 'rack.request.form_error' + RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash' + RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string' + RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash' + RACK_REQUEST_QUERY_STRING = 'rack.request.query_string' + RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method' +end diff --git a/lib/rack/content_length.rb b/lib/rack/content_length.rb index 9e2b5fc42a1c15c4bf36e6d741ff1a98ed45b88c..cbac93abcf37ec442babf308794206d2ff1524e7 100644 --- a/lib/rack/content_length.rb +++ b/lib/rack/content_length.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' + module Rack - # Sets the Content-Length header on responses that do not specify - # a Content-Length or Transfer-Encoding header. Note that this - # does not fix responses that have an invalid Content-Length + # Sets the content-length header on responses that do not specify + # a content-length or transfer-encoding header. Note that this + # does not fix responses that have an invalid content-length # header specified. class ContentLength include Rack::Utils @@ -14,25 +17,18 @@ module Rack end def call(env) - status, headers, body = @app.call(env) - headers = HeaderHash[headers] + status, headers, body = response = @app.call(env) if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) && !headers[CONTENT_LENGTH] && - !headers[TRANSFER_ENCODING] - - obody = body - body, length = [], 0 - obody.each { |part| body << part; length += part.bytesize } - - body = BodyProxy.new(body) do - obody.close if obody.respond_to?(:close) - end + !headers[TRANSFER_ENCODING] && + body.respond_to?(:to_ary) - headers[CONTENT_LENGTH] = length.to_s + response[2] = body = body.to_ary + headers[CONTENT_LENGTH] = body.sum(&:bytesize).to_s end - [status, headers, body] + response end end end diff --git a/lib/rack/content_type.rb b/lib/rack/content_type.rb index 503f7070621e7e86ae9d31914f5931e364d0a3a3..19f07824f563bc197c6f92a695d50f29f687ee55 100644 --- a/lib/rack/content_type.rb +++ b/lib/rack/content_type.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' + module Rack - # Sets the Content-Type header on responses which don't have one. + # Sets the content-type header on responses which don't have one. # # Builder Usage: # use Rack::ContentType, "text/plain" @@ -13,18 +16,18 @@ module Rack include Rack::Utils def initialize(app, content_type = "text/html") - @app, @content_type = app, content_type + @app = app + @content_type = content_type end def call(env) - status, headers, body = @app.call(env) - headers = Utils::HeaderHash[headers] + status, headers, _ = response = @app.call(env) unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) headers[CONTENT_TYPE] ||= @content_type end - [status, headers, body] + response end end end diff --git a/lib/rack/core_ext/regexp.rb b/lib/rack/core_ext/regexp.rb deleted file mode 100644 index a32fcdf629a3fff24c5d680ed26ae23ac2ae29d2..0000000000000000000000000000000000000000 --- a/lib/rack/core_ext/regexp.rb +++ /dev/null @@ -1,14 +0,0 @@ -# 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 e177fabb017c966f1a0fda7234db7956e56da39c..cc01c32a0ad572ed795ac43cf954e7a79fcd5868 100644 --- a/lib/rack/deflater.rb +++ b/lib/rack/deflater.rb @@ -3,6 +3,11 @@ require "zlib" require "time" # for Time.httpdate +require_relative 'constants' +require_relative 'utils' +require_relative 'request' +require_relative 'body_proxy' + module Rack # This middleware enables content encoding of http responses, # usually for purposes of compression. @@ -21,8 +26,6 @@ module Rack # Note that despite the name, Deflater does not support the +deflate+ # encoding. class Deflater - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - # Creates Rack::Deflater middleware. Options: # # :if :: a lambda enabling / disabling deflation based on returned boolean value @@ -41,11 +44,10 @@ module Rack end def call(env) - status, headers, body = @app.call(env) - headers = Utils::HeaderHash[headers] + status, headers, body = response = @app.call(env) unless should_deflate?(env, status, headers, body) - return [status, headers, body] + return response end request = Request.new(env) @@ -54,21 +56,23 @@ module Rack request.accept_encoding) # Set the Vary HTTP header. - vary = headers["Vary"].to_s.split(",").map(&:strip) - unless vary.include?("*") || vary.include?("Accept-Encoding") - headers["Vary"] = vary.push("Accept-Encoding").join(",") + vary = headers["vary"].to_s.split(",").map(&:strip) + unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'} + headers["vary"] = vary.push("Accept-Encoding").join(",") end case encoding when "gzip" - headers['Content-Encoding'] = "gzip" + headers['content-encoding'] = "gzip" headers.delete(CONTENT_LENGTH) - mtime = headers["Last-Modified"] + mtime = headers["last-modified"] mtime = Time.httpdate(mtime).to_i if mtime - [status, headers, GzipStream.new(body, mtime, @sync)] + response[2] = GzipStream.new(body, mtime, @sync) + response when "identity" - [status, headers, body] - when nil + response + else # when nil + # Only possible encoding values here are 'gzip', 'identity', and 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] @@ -77,6 +81,9 @@ module Rack # Body class used for gzip encoded responses. class GzipStream + + BUFFER_LENGTH = 128 * 1_024 + # Initialize the gzip stream. Arguments: # body :: Response body to compress with gzip # mtime :: The modification time of the body, used to set the @@ -93,19 +100,26 @@ module Rack @writer = block gzip = ::Zlib::GzipWriter.new(self) gzip.mtime = @mtime if @mtime - @body.each { |part| - # Skip empty strings, as they would result in no output, - # and flushing empty parts would raise Zlib::BufError. - next if part.empty? - - gzip.write(part) - gzip.flush if @sync - } + # @body.each is equivalent to @body.gets (slow) + if @body.is_a? ::File # XXX: Should probably be ::IO + while part = @body.read(BUFFER_LENGTH) + gzip.write(part) + gzip.flush if @sync + end + else + @body.each { |part| + # Skip empty strings, as they would result in no output, + # and flushing empty parts would raise Zlib::BufError. + next if part.empty? + gzip.write(part) + gzip.flush if @sync + } + end ensure - gzip.close + gzip.finish end - # Call the block passed to #each with the the gzipped data. + # Call the block passed to #each with the gzipped data. def write(data) @writer.call(data) end @@ -123,13 +137,13 @@ module Rack # Skip compressing empty entity body responses and responses with # no-transform set. if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) || - /\bno-transform\b/.match?(headers['Cache-Control'].to_s) || - headers['Content-Encoding']&.!~(/\bidentity\b/) + /\bno-transform\b/.match?(headers[CACHE_CONTROL].to_s) || + headers['content-encoding']&.!~(/\bidentity\b/) return false end # Skip if @compressible_types are given and does not include request's content type - return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/])) + return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/])) # Skip if @condition lambda is given and evaluates to false return false if @condition && !@condition.call(env, status, headers, body) diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb index be72be0144405996c18993a750f9de822a8a28ba..089623f91d934ec0a958df901745f6c5c726697d 100644 --- a/lib/rack/directory.rb +++ b/lib/rack/directory.rb @@ -2,6 +2,12 @@ require 'time' +require_relative 'constants' +require_relative 'utils' +require_relative 'head' +require_relative 'mime' +require_relative 'files' + module Rack # Rack::Directory serves entries below the +root+ given, according to the # path info of the Rack request. If a directory is found, the file's contents @@ -106,7 +112,7 @@ table { width:100%%; } body = "Bad Request\n" [400, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => body.bytesize.to_s, - "X-Cascade" => "pass" }, [body]] + "x-cascade" => "pass" }, [body]] end # Rack response to use for requests with paths outside the root, or nil if path is inside the root. @@ -117,7 +123,7 @@ table { width:100%%; } body = "Forbidden\n" [403, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => body.bytesize.to_s, - "X-Cascade" => "pass" }, [body]] + "x-cascade" => "pass" }, [body]] end # Rack response to use for directories under the root. @@ -176,7 +182,7 @@ table { width:100%%; } body = "Entity not found: #{path_info}\n" [404, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => body.bytesize.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 5039437e1c13e6ad458d071fb4d3c438247589ad..fa78b472fd91469e82274c83bc8ec611318b837c 100644 --- a/lib/rack/etag.rb +++ b/lib/rack/etag.rb @@ -1,17 +1,19 @@ # frozen_string_literal: true -require_relative '../rack' require 'digest/sha2' +require_relative 'constants' +require_relative 'utils' + module Rack - # Automatically sets the ETag header on all String bodies. + # Automatically sets the etag header on all String bodies. # - # The ETag header is skipped if ETag or Last-Modified headers are sent or if + # The etag header is skipped if etag or last-modified headers are sent or if # a sendfile body (body.responds_to :to_path) is given (since such cases # should be handled by apache/nginx). # - # On initialization, you can pass two parameters: a Cache-Control directive - # used when Etag is absent and a directive when it is present. The first + # On initialization, you can pass two parameters: a cache-control directive + # used when etag is absent and a directive when it is present. The first # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate" class ETag ETAG_STRING = Rack::ETAG @@ -24,16 +26,11 @@ module Rack end def call(env) - status, headers, body = @app.call(env) - - headers = Utils::HeaderHash[headers] + status, headers, body = response = @app.call(env) - if etag_status?(status) && etag_body?(body) && !skip_caching?(headers) - original_body = body - digest, new_body = digest_body(body) - body = Rack::BodyProxy.new(new_body) do - original_body.close if original_body.respond_to?(:close) - end + if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers) + body = body.to_ary + digest = digest_body(body) headers[ETAG_STRING] = %(W/"#{digest}") if digest end @@ -45,7 +42,7 @@ module Rack end end - [status, headers, body] + response end private @@ -54,24 +51,18 @@ module Rack status == 200 || status == 201 end - def etag_body?(body) - !body.respond_to?(:to_path) - end - def skip_caching?(headers) - headers.key?(ETAG_STRING) || headers.key?('Last-Modified') + headers.key?(ETAG_STRING) || headers.key?('last-modified') end def digest_body(body) - parts = [] digest = nil body.each do |part| - parts << part (digest ||= Digest::SHA256.new) << part unless part.empty? end - [digest && digest.hexdigest.byteslice(0, 32), parts] + digest && digest.hexdigest.byteslice(0,32) end end end diff --git a/lib/rack/events.rb b/lib/rack/events.rb index 65055fdc51e6d8e8a77d66d013cceb414f06a334..c7bb201f05aee7924b7cc583a0476cf2c15004b2 100644 --- a/lib/rack/events.rb +++ b/lib/rack/events.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative 'body_proxy' +require_relative 'request' +require_relative 'response' + 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 diff --git a/lib/rack/file.rb b/lib/rack/file.rb index fdcf9b3ec064550319844363e57d0031061ed112..52c7b4166793f24a41922a1e0cf41e9068e039a9 100644 --- a/lib/rack/file.rb +++ b/lib/rack/file.rb @@ -3,5 +3,7 @@ require_relative 'files' module Rack + warn "Rack::File is deprecated and will be removed in Rack 3.1", uplevel: 1 + File = Files end diff --git a/lib/rack/files.rb b/lib/rack/files.rb index e745eb3984373dde5dded309376f722b5f697cdf..5b8353f5b5257a240f70ecafe2f57b099236dfd1 100644 --- a/lib/rack/files.rb +++ b/lib/rack/files.rb @@ -2,6 +2,12 @@ require 'time' +require_relative 'constants' +require_relative 'head' +require_relative 'utils' +require_relative 'request' +require_relative 'mime' + module Rack # Rack::Files serves files below the +root+ directory given, according to the # path info of the Rack request. @@ -16,14 +22,6 @@ module Rack ALLOW_HEADER = ALLOWED_VERBS.join(', ') MULTIPART_BOUNDARY = 'AaB03x' - # @todo remove in 3.0 - def self.method_added(name) - if name == :response_body - raise "#{self.class}\#response_body is no longer supported." - end - super - end - attr_reader :root def initialize(root, headers = {}, default_mime = 'text/plain') @@ -41,7 +39,7 @@ module Rack def get(env) request = Rack::Request.new env unless ALLOWED_VERBS.include? request.request_method - return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER }) + return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER }) end path_info = Utils.unescape_path request.path_info @@ -69,12 +67,12 @@ module Rack def serving(request, path) if request.options? - return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []] + return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []] end last_modified = ::File.mtime(path).httpdate return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified - headers = { "Last-Modified" => last_modified } + headers = { "last-modified" => last_modified } mime_type = mime_type path, @default_mime headers[CONTENT_TYPE] = mime_type if mime_type @@ -91,15 +89,15 @@ module Rack elsif ranges.empty? # Unsatisfiable. Return error, and file size: response = fail(416, "Byte range unsatisfiable") - response[1]["Content-Range"] = "bytes */#{size}" + response[1]["content-range"] = "bytes */#{size}" return response - elsif ranges.size >= 1 + else # Partial content partial_content = true if ranges.size == 1 range = ranges[0] - headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}" + headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}" else headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}" end @@ -164,8 +162,8 @@ module Rack <<-EOF \r --#{MULTIPART_BOUNDARY}\r -Content-Type: #{options[:mime_type]}\r -Content-Range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r +content-type: #{options[:mime_type]}\r +content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r \r EOF end @@ -197,7 +195,7 @@ EOF { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => body.size.to_s, - "X-Cascade" => "pass" + "x-cascade" => "pass" }.merge!(headers), [body] ] diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb deleted file mode 100644 index df17b238ddb73d599fce01a934149b539e4e695f..0000000000000000000000000000000000000000 --- a/lib/rack/handler.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -module Rack - # *Handlers* connect web servers with Rack. - # - # Rack includes Handlers for Thin, WEBrick, FastCGI, CGI, SCGI - # and LiteSpeed. - # - # Handlers usually are activated by calling <tt>MyHandler.run(myapp)</tt>. - # A second optional hash can be passed to include server-specific - # configuration. - module Handler - def self.get(server) - return unless server - server = server.to_s - - unless @handlers.include? server - load_error = try_require('rack/handler', server) - end - - if klass = @handlers[server] - const_get(klass) - else - const_get(server, false) - end - - rescue NameError => name_error - raise load_error || name_error - end - - # Select first available Rack handler given an `Array` of server names. - # Raises `LoadError` if no handler was found. - # - # > pick ['thin', 'webrick'] - # => Rack::Handler::WEBrick - def self.pick(server_names) - server_names = Array(server_names) - server_names.each do |server_name| - begin - return get(server_name.to_s) - rescue LoadError, NameError - end - end - - 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") - Rack::Handler::FastCGI - elsif ENV.include?(REQUEST_METHOD) - Rack::Handler::CGI - elsif ENV.include?("RACK_HANDLER") - self.get(ENV["RACK_HANDLER"]) - else - pick SERVER_NAMES - end - end - - # Transforms server-name constants to their canonical form as filenames, - # then tries to require them but silences the LoadError if not found - # - # Naming convention: - # - # Foo # => 'foo' - # FooBar # => 'foo_bar.rb' - # FooBAR # => 'foobar.rb' - # FOObar # => 'foobar.rb' - # FOOBAR # => 'foobar.rb' - # FooBarBaz # => 'foo_bar_baz.rb' - def self.try_require(prefix, const_name) - file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }. - gsub(/[A-Z]+[^A-Z]/, '_\&').downcase - - require(::File.join(prefix, file)) - nil - rescue LoadError => error - error - end - - def self.register(server, klass) - @handlers ||= {} - @handlers[server.to_s] = klass.to_s - end - - autoload :CGI, "rack/handler/cgi" - autoload :FastCGI, "rack/handler/fastcgi" - autoload :WEBrick, "rack/handler/webrick" - autoload :LSWS, "rack/handler/lsws" - autoload :SCGI, "rack/handler/scgi" - autoload :Thin, "rack/handler/thin" - - register 'cgi', 'Rack::Handler::CGI' - register 'fastcgi', 'Rack::Handler::FastCGI' - register 'webrick', 'Rack::Handler::WEBrick' - register 'lsws', 'Rack::Handler::LSWS' - register 'scgi', 'Rack::Handler::SCGI' - register 'thin', 'Rack::Handler::Thin' - end -end diff --git a/lib/rack/handler/cgi.rb b/lib/rack/handler/cgi.rb deleted file mode 100644 index 1c11ab360622c22068bf353d2b80d7a288344833..0000000000000000000000000000000000000000 --- a/lib/rack/handler/cgi.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module Rack - module Handler - class CGI - def self.run(app, **options) - $stdin.binmode - serve app - end - - def self.serve(app) - env = ENV.to_hash - env.delete "HTTP_CONTENT_LENGTH" - - env[SCRIPT_NAME] = "" if env[SCRIPT_NAME] == "/" - - env.update( - RACK_VERSION => Rack::VERSION, - RACK_INPUT => Rack::RewindableInput.new($stdin), - RACK_ERRORS => $stderr, - RACK_MULTITHREAD => false, - RACK_MULTIPROCESS => true, - RACK_RUNONCE => true, - RACK_URL_SCHEME => ["yes", "on", "1"].include?(ENV[HTTPS]) ? "https" : "http" - ) - - env[QUERY_STRING] ||= "" - env[HTTP_VERSION] ||= env[SERVER_PROTOCOL] - env[REQUEST_PATH] ||= "/" - - status, headers, body = app.call(env) - begin - send_headers status, headers - send_body body - ensure - body.close if body.respond_to? :close - end - end - - def self.send_headers(status, headers) - $stdout.print "Status: #{status}\r\n" - headers.each { |k, vs| - vs.split("\n").each { |v| - $stdout.print "#{k}: #{v}\r\n" - } - } - $stdout.print "\r\n" - $stdout.flush - end - - def self.send_body(body) - body.each { |part| - $stdout.print part - $stdout.flush - } - end - end - end -end diff --git a/lib/rack/handler/fastcgi.rb b/lib/rack/handler/fastcgi.rb deleted file mode 100644 index 1df123e02a65a73c2a365215082ffe1787562122..0000000000000000000000000000000000000000 --- a/lib/rack/handler/fastcgi.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'fcgi' -require 'socket' - -if defined? FCGI::Stream - class FCGI::Stream - alias _rack_read_without_buffer read - - def read(n, buffer = nil) - buf = _rack_read_without_buffer n - buffer.replace(buf.to_s) if buffer - buf - end - end -end - -module Rack - module Handler - class FastCGI - def self.run(app, **options) - if options[:File] - STDIN.reopen(UNIXServer.new(options[:File])) - elsif options[:Port] - STDIN.reopen(TCPServer.new(options[:Host], options[:Port])) - end - FCGI.each { |request| - serve request, app - } - end - - def self.valid_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - "Host=HOST" => "Hostname to listen on (default: #{default_host})", - "Port=PORT" => "Port to listen on (default: 8080)", - "File=PATH" => "Creates a Domain socket at PATH instead of a TCP socket. Ignores Host and Port if set.", - } - end - - def self.serve(request, app) - env = request.env - env.delete "HTTP_CONTENT_LENGTH" - - env[SCRIPT_NAME] = "" if env[SCRIPT_NAME] == "/" - - rack_input = RewindableInput.new(request.in) - - env.update( - RACK_VERSION => Rack::VERSION, - RACK_INPUT => rack_input, - RACK_ERRORS => request.err, - RACK_MULTITHREAD => false, - RACK_MULTIPROCESS => true, - RACK_RUNONCE => false, - RACK_URL_SCHEME => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http" - ) - - env[QUERY_STRING] ||= "" - env[HTTP_VERSION] ||= env[SERVER_PROTOCOL] - env[REQUEST_PATH] ||= "/" - env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == "" - env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == "" - - begin - status, headers, body = app.call(env) - begin - send_headers request.out, status, headers - send_body request.out, body - ensure - body.close if body.respond_to? :close - end - ensure - rack_input.close - request.finish - end - end - - def self.send_headers(out, status, headers) - out.print "Status: #{status}\r\n" - headers.each { |k, vs| - vs.split("\n").each { |v| - out.print "#{k}: #{v}\r\n" - } - } - out.print "\r\n" - out.flush - end - - def self.send_body(out, body) - body.each { |part| - out.print part - out.flush - } - end - end - end -end diff --git a/lib/rack/handler/lsws.rb b/lib/rack/handler/lsws.rb deleted file mode 100644 index f12090bd62df9968784d1f769f6aaf8ae50eef0b..0000000000000000000000000000000000000000 --- a/lib/rack/handler/lsws.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require 'lsapi' - -module Rack - module Handler - class LSWS - def self.run(app, **options) - while LSAPI.accept != nil - serve app - end - end - def self.serve(app) - env = ENV.to_hash - env.delete "HTTP_CONTENT_LENGTH" - env[SCRIPT_NAME] = "" if env[SCRIPT_NAME] == "/" - - rack_input = RewindableInput.new($stdin.read.to_s) - - env.update( - RACK_VERSION => Rack::VERSION, - RACK_INPUT => rack_input, - RACK_ERRORS => $stderr, - RACK_MULTITHREAD => false, - RACK_MULTIPROCESS => true, - RACK_RUNONCE => false, - RACK_URL_SCHEME => ["yes", "on", "1"].include?(ENV[HTTPS]) ? "https" : "http" - ) - - env[QUERY_STRING] ||= "" - env[HTTP_VERSION] ||= env[SERVER_PROTOCOL] - env[REQUEST_PATH] ||= "/" - status, headers, body = app.call(env) - begin - send_headers status, headers - send_body body - ensure - body.close if body.respond_to? :close - end - ensure - rack_input.close - end - def self.send_headers(status, headers) - print "Status: #{status}\r\n" - headers.each { |k, vs| - vs.split("\n").each { |v| - print "#{k}: #{v}\r\n" - } - } - print "\r\n" - STDOUT.flush - end - def self.send_body(body) - body.each { |part| - print part - STDOUT.flush - } - end - end - end -end diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb deleted file mode 100644 index e3b8d3c6f240a9c6b426b28b84eabefc8a36c9e3..0000000000000000000000000000000000000000 --- a/lib/rack/handler/scgi.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'scgi' -require 'stringio' - -module Rack - module Handler - class SCGI < ::SCGI::Processor - attr_accessor :app - - def self.run(app, **options) - options[:Socket] = UNIXServer.new(options[:File]) if options[:File] - new(options.merge(app: app, - host: options[:Host], - port: options[:Port], - socket: options[:Socket])).listen - end - - def self.valid_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - "Host=HOST" => "Hostname to listen on (default: #{default_host})", - "Port=PORT" => "Port to listen on (default: 8080)", - } - end - - def initialize(settings = {}) - @app = settings[:app] - super(settings) - end - - def process_request(request, input_body, socket) - env = Hash[request] - env.delete "HTTP_CONTENT_TYPE" - env.delete "HTTP_CONTENT_LENGTH" - env[REQUEST_PATH], env[QUERY_STRING] = env["REQUEST_URI"].split('?', 2) - env[HTTP_VERSION] ||= env[SERVER_PROTOCOL] - env[PATH_INFO] = env[REQUEST_PATH] - env[QUERY_STRING] ||= "" - env[SCRIPT_NAME] = "" - - rack_input = StringIO.new(input_body) - rack_input.set_encoding(Encoding::BINARY) - - env.update( - RACK_VERSION => Rack::VERSION, - RACK_INPUT => rack_input, - RACK_ERRORS => $stderr, - RACK_MULTITHREAD => true, - RACK_MULTIPROCESS => true, - RACK_RUNONCE => false, - RACK_URL_SCHEME => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http" - ) - - status, headers, body = app.call(env) - begin - socket.write("Status: #{status}\r\n") - headers.each do |k, vs| - vs.split("\n").each { |v| socket.write("#{k}: #{v}\r\n")} - end - socket.write("\r\n") - body.each {|s| socket.write(s)} - ensure - body.close if body.respond_to? :close - end - end - end - end -end diff --git a/lib/rack/handler/thin.rb b/lib/rack/handler/thin.rb deleted file mode 100644 index 393a6e98699dca62de2b928e8183f9f56eeafbf9..0000000000000000000000000000000000000000 --- a/lib/rack/handler/thin.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require "thin" -require "thin/server" -require "thin/logging" -require "thin/backends/tcp_server" - -module Rack - module Handler - class Thin - def self.run(app, **options) - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - host = options.delete(:Host) || default_host - port = options.delete(:Port) || 8080 - args = [host, port, app, options] - # Thin versions below 0.8.0 do not support additional options - args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8 - server = ::Thin::Server.new(*args) - yield server if block_given? - server.start - end - - def self.valid_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - "Host=HOST" => "Hostname to listen on (default: #{default_host})", - "Port=PORT" => "Port to listen on (default: 8080)", - } - end - end - end -end diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb deleted file mode 100644 index d2f389758a33c48e125a4dc4af76dc041b9830de..0000000000000000000000000000000000000000 --- a/lib/rack/handler/webrick.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -require 'webrick' -require 'stringio' - -# This monkey patch allows for applications to perform their own chunking -# through WEBrick::HTTPResponse if rack is set to true. -class WEBrick::HTTPResponse - attr_accessor :rack - - alias _rack_setup_header setup_header - def setup_header - app_chunking = rack && @header['transfer-encoding'] == 'chunked' - - @chunked = app_chunking if app_chunking - - _rack_setup_header - - @chunked = false if app_chunking - end -end - -module Rack - module Handler - class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet - def self.run(app, **options) - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : nil - - if !options[:BindAddress] || options[:Host] - options[:BindAddress] = options.delete(:Host) || default_host - end - options[:Port] ||= 8080 - if options[:SSLEnable] - require 'webrick/https' - end - - @server = ::WEBrick::HTTPServer.new(options) - @server.mount "/", Rack::Handler::WEBrick, app - yield @server if block_given? - @server.start - end - - def self.valid_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - "Host=HOST" => "Hostname to listen on (default: #{default_host})", - "Port=PORT" => "Port to listen on (default: 8080)", - } - end - - def self.shutdown - if @server - @server.shutdown - @server = nil - end - end - - def initialize(server, app) - super server - @app = app - end - - def service(req, res) - res.rack = true - env = req.meta_vars - env.delete_if { |k, v| v.nil? } - - rack_input = StringIO.new(req.body.to_s) - rack_input.set_encoding(Encoding::BINARY) - - env.update( - RACK_VERSION => Rack::VERSION, - RACK_INPUT => rack_input, - RACK_ERRORS => $stderr, - RACK_MULTITHREAD => true, - RACK_MULTIPROCESS => false, - RACK_RUNONCE => false, - RACK_URL_SCHEME => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http", - RACK_IS_HIJACK => true, - RACK_HIJACK => lambda { raise NotImplementedError, "only partial hijack is supported."}, - RACK_HIJACK_IO => nil - ) - - env[HTTP_VERSION] ||= env[SERVER_PROTOCOL] - 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] - end - env[REQUEST_PATH] ||= [env[SCRIPT_NAME], env[PATH_INFO]].join - - status, headers, body = @app.call(env) - begin - res.status = status.to_i - io_lambda = nil - headers.each { |k, vs| - if k == RACK_HIJACK - io_lambda = vs - elsif k.downcase == "set-cookie" - res.cookies.concat vs.split("\n") - else - # Since WEBrick won't accept repeated headers, - # merge the values per RFC 1945 section 4.2. - res[k] = vs.split("\n").join(", ") - end - } - - if io_lambda - rd, wr = IO.pipe - res.body = rd - res.chunked = true - io_lambda.call wr - elsif body.respond_to?(:to_path) - res.body = ::File.open(body.to_path, 'rb') - else - body.each { |part| - res.body << part - } - end - ensure - body.close if body.respond_to? :close - end - end - end - end -end diff --git a/lib/rack/head.rb b/lib/rack/head.rb index 8025a27d514c2fe57ad8436718639da532f2a996..c1c430f653e603320fcc2f691af18bfa7b326811 100644 --- a/lib/rack/head.rb +++ b/lib/rack/head.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'body_proxy' + module Rack # Rack::Head returns an empty body for all HEAD requests. It leaves # all other requests unchanged. @@ -9,17 +12,15 @@ module Rack end def call(env) - status, headers, body = @app.call(env) + _, _, body = response = @app.call(env) if env[REQUEST_METHOD] == HEAD - [ - status, headers, Rack::BodyProxy.new([]) do - body.close if body.respond_to? :close - end - ] - else - [status, headers, body] + response[2] = Rack::BodyProxy.new([]) do + body.close if body.respond_to? :close + end end + + response end end end diff --git a/lib/rack/headers.rb b/lib/rack/headers.rb new file mode 100644 index 0000000000000000000000000000000000000000..ae1a89d12fceeae76b085292e6fd9cc7fcb1645f --- /dev/null +++ b/lib/rack/headers.rb @@ -0,0 +1,154 @@ +module Rack + # Rack::Headers is a Hash subclass that downcases all keys. It's designed + # to be used by rack applications that don't implement the Rack 3 SPEC + # (by using non-lowercase response header keys), automatically handling + # the downcasing of keys. + class Headers < Hash + def self.[](*items) + if items.length % 2 != 0 + if items.length == 1 && items.first.is_a?(Hash) + new.merge!(items.first) + else + raise ArgumentError, "odd number of arguments for Rack::Headers" + end + else + hash = new + loop do + break if items.length == 0 + key = items.shift + value = items.shift + hash[key] = value + end + hash + end + end + + def [](key) + super(downcase_key(key)) + end + + def []=(key, value) + super(key.downcase.freeze, value) + end + alias store []= + + def assoc(key) + super(downcase_key(key)) + end + + def compare_by_identity + raise TypeError, "Rack::Headers cannot compare by identity, use regular Hash" + end + + def delete(key) + super(downcase_key(key)) + end + + def dig(key, *a) + super(downcase_key(key), *a) + end + + def fetch(key, *default, &block) + key = downcase_key(key) + super + end + + def fetch_values(*a) + super(*a.map!{|key| downcase_key(key)}) + end + + def has_key?(key) + super(downcase_key(key)) + end + alias include? has_key? + alias key? has_key? + alias member? has_key? + + def invert + hash = self.class.new + each{|key, value| hash[value] = key} + hash + end + + def merge(hash, &block) + dup.merge!(hash, &block) + end + + def reject(&block) + hash = dup + hash.reject!(&block) + hash + end + + def replace(hash) + clear + update(hash) + end + + def select(&block) + hash = dup + hash.select!(&block) + hash + end + + def to_proc + lambda{|x| self[x]} + end + + def transform_values(&block) + dup.transform_values!(&block) + end + + def update(hash, &block) + hash.each do |key, value| + self[key] = if block_given? && include?(key) + block.call(key, self[key], value) + else + value + end + end + self + end + alias merge! update + + def values_at(*keys) + keys.map{|key| self[key]} + end + + # :nocov: + if RUBY_VERSION >= '2.5' + # :nocov: + def slice(*a) + h = self.class.new + a.each{|k| h[k] = self[k] if has_key?(k)} + h + end + + def transform_keys(&block) + dup.transform_keys!(&block) + end + + def transform_keys! + hash = self.class.new + each do |k, v| + hash[yield k] = v + end + replace(hash) + end + end + + # :nocov: + if RUBY_VERSION >= '3.0' + # :nocov: + def except(*a) + super(*a.map!{|key| downcase_key(key)}) + end + end + + private + + def downcase_key(key) + key.is_a?(String) ? key.downcase : key + end + end +end diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb old mode 100644 new mode 100755 index 67d2eb1294c27005e097d9d82aa50094b2d68fbc..ee3ec7161a83bfb4346650d114c87645a929a438 --- a/lib/rack/lint.rb +++ b/lib/rack/lint.rb @@ -2,6 +2,9 @@ require 'forwardable' +require_relative 'constants' +require_relative 'utils' + module Rack # Rack::Lint validates your application and the requests and # responses according to the Rack spec. @@ -9,798 +12,896 @@ module Rack class Lint def initialize(app) @app = app - @content_length = nil end # :stopdoc: class LintError < RuntimeError; end - module Assertion - def assert(message) - unless yield - raise LintError, message - end - end - end - include Assertion - - ## This specification aims to formalize the Rack protocol. You + # AUTHORS: n.b. The trailing whitespace between paragraphs is important and + # should not be removed. The whitespace creates paragraphs in the RDoc + # output. + # + ## This specification aims to formalize the Rack protocol. You ## can (and should) use Rack::Lint to enforce it. ## ## When you develop middleware, be sure to add a Lint before and ## after to catch all mistakes. - + ## ## = Rack applications - + ## ## A Rack application is a Ruby object (not a class) that ## responds to +call+. def call(env = nil) - dup._call(env) + Wrapper.new(@app, env).response end - def _call(env) - ## It takes exactly one argument, the *environment* - assert("No env given") { env } - check_env env - - env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT]) - env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS]) - - ## and returns an Array of exactly three values: - ary = @app.call(env) - assert("response is not an Array, but #{ary.class}") { - ary.kind_of? Array - } - assert("response array has #{ary.size} elements instead of 3") { - ary.size == 3 - } - - status, headers, @body = ary - ## The *status*, - check_status status - ## the *headers*, - check_headers headers - - hijack_proc = check_hijack_response headers, env - if hijack_proc && headers.is_a?(Hash) - headers[RACK_HIJACK] = hijack_proc + class Wrapper + def initialize(app, env) + @app = app + @env = env + @response = nil + @head_request = false + + @status = nil + @headers = nil + @body = nil + @invoked = nil + @content_length = nil + @closed = false + @size = 0 end - ## and the *body*. - check_content_type status, headers - check_content_length status, headers - @head_request = env[REQUEST_METHOD] == HEAD - [status, headers, self] - end + def response + ## It takes exactly one argument, the *environment* + raise LintError, "No env given" unless @env + check_environment(@env) - ## == The Environment - def check_env(env) - ## The environment must be an unfrozen instance of Hash that includes - ## CGI-like headers. The application is free to modify the - ## environment. - assert("env #{env.inspect} is not a Hash, but #{env.class}") { - env.kind_of? Hash - } - assert("env should not be frozen, but is") { - !env.frozen? - } - - ## - ## The environment is required to include these variables - ## (adopted from PEP333), except when they'd be empty, but see - ## below. - - ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as - ## "GET" or "POST". This cannot ever - ## be an empty string, and so is - ## always required. - - ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request - ## URL's "path" that corresponds to the - ## application object, so that the - ## application knows its virtual - ## "location". This may be an empty - ## string, if the application corresponds - ## to the "root" of the server. - - ## <tt>PATH_INFO</tt>:: The remainder of the request URL's - ## "path", designating the virtual - ## "location" of the request's target - ## within the application. This may be an - ## empty string, if the request URL targets - ## the application root and does not have a - ## trailing slash. This value may be - ## percent-encoded when originating from - ## a URL. - - ## <tt>QUERY_STRING</tt>:: The portion of the request URL that - ## follows the <tt>?</tt>, if any. May be - ## empty, but is always required! - - ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and - ## <tt>PATH_INFO</tt>, these variables can be - ## used to complete the URL. Note, however, - ## that <tt>HTTP_HOST</tt>, if present, - ## should be used in preference to - ## <tt>SERVER_NAME</tt> for reconstructing - ## the request URL. - ## <tt>SERVER_NAME</tt> can never be an empty - ## string, and so is always required. - - ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the - ## server is running on. Should be specified if - ## the server is running on a non-standard port. - - ## <tt>HTTP_</tt> Variables:: Variables corresponding to the - ## client-supplied HTTP request - ## headers (i.e., variables whose - ## names begin with <tt>HTTP_</tt>). The - ## presence or absence of these - ## variables should correspond with - ## the presence or absence of the - ## appropriate HTTP header in the - ## request. See - ## {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 - ## See Rack::VERSION, that corresponds to - ## the version of this SPEC. - - ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the - ## request URL. - - ## <tt>rack.input</tt>:: See below, the input stream. - - ## <tt>rack.errors</tt>:: See below, the error stream. - - ## <tt>rack.multithread</tt>:: true if the application object may be - ## simultaneously invoked by another thread - ## in the same process, false otherwise. - - ## <tt>rack.multiprocess</tt>:: true if an equivalent application object - ## may be simultaneously invoked by another - ## process, false otherwise. - - ## <tt>rack.run_once</tt>:: true if the server expects - ## (but does not guarantee!) that the - ## application will only be invoked this one - ## time during the life of its containing - ## process. Normally, this will only be true - ## for a server based on CGI - ## (or something similar). - - ## <tt>rack.hijack?</tt>:: present and true if the server supports - ## connection hijacking. See below, hijacking. - - ## <tt>rack.hijack</tt>:: an object responding to #call that must be - ## called at least once before using - ## rack.hijack_io. - ## It is recommended #call return rack.hijack_io - ## as well as setting it in env if necessary. - - ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack - ## has received #call, this will contain - ## an object resembling an IO. See hijacking. - - ## Additional environment specifications have approved to - ## standardized middleware APIs. None of these are required to - ## be implemented by the server. - - ## <tt>rack.session</tt>:: A hash like interface for storing - ## request session data. - ## The store must implement: - if session = env[RACK_SESSION] - ## store(key, value) (aliased as []=); - assert("session #{session.inspect} must respond to store and []=") { - session.respond_to?(:store) && session.respond_to?(:[]=) - } + @env[RACK_INPUT] = InputWrapper.new(@env[RACK_INPUT]) + @env[RACK_ERRORS] = ErrorWrapper.new(@env[RACK_ERRORS]) - ## fetch(key, default = nil) (aliased as []); - assert("session #{session.inspect} must respond to fetch and []") { - session.respond_to?(:fetch) && session.respond_to?(:[]) - } + ## and returns a non-frozen Array of exactly three values: + @response = @app.call(@env) + raise LintError, "response is not an Array, but #{@response.class}" unless @response.kind_of? Array + raise LintError, "response is frozen" if @response.frozen? + raise LintError, "response array has #{@response.size} elements instead of 3" unless @response.size == 3 - ## delete(key); - assert("session #{session.inspect} must respond to delete") { - session.respond_to?(:delete) - } + @status, @headers, @body = @response + ## The *status*, + check_status(@status) - ## clear; - assert("session #{session.inspect} must respond to clear") { - session.respond_to?(:clear) - } + ## the *headers*, + check_headers(@headers) - ## to_hash (returning unfrozen Hash instance); - assert("session #{session.inspect} must respond to to_hash and return unfrozen Hash instance") { - session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen? - } + hijack_proc = check_hijack_response(@headers, @env) + if hijack_proc + @headers[RACK_HIJACK] = hijack_proc + end + + ## and the *body*. + check_content_type(@status, @headers) + check_content_length(@status, @headers) + @head_request = @env[REQUEST_METHOD] == HEAD + + @lint = (@env['rack.lint'] ||= []) << self + + if (@env['rack.lint.body_iteration'] ||= 0) > 0 + raise LintError, "Middleware must not call #each directly" + end + + return [@status, @headers, self] end - ## <tt>rack.logger</tt>:: A common object interface for logging messages. - ## The object must implement: - if logger = env[RACK_LOGGER] - ## info(message, &block) - assert("logger #{logger.inspect} must respond to info") { - logger.respond_to?(:info) - } + ## + ## == The Environment + ## + def check_environment(env) + ## The environment must be an unfrozen instance of Hash that includes + ## CGI-like headers. The Rack application is free to modify the + ## environment. + raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash + raise LintError, "env should not be frozen, but is" if env.frozen? - ## debug(message, &block) - assert("logger #{logger.inspect} must respond to debug") { - logger.respond_to?(:debug) - } + ## + ## The environment is required to include these variables + ## (adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see + ## below. + + ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as + ## "GET" or "POST". This cannot ever + ## be an empty string, and so is + ## always required. + + ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request + ## URL's "path" that corresponds to the + ## application object, so that the + ## application knows its virtual + ## "location". This may be an empty + ## string, if the application corresponds + ## to the "root" of the server. + + ## <tt>PATH_INFO</tt>:: The remainder of the request URL's + ## "path", designating the virtual + ## "location" of the request's target + ## within the application. This may be an + ## empty string, if the request URL targets + ## the application root and does not have a + ## trailing slash. This value may be + ## percent-encoded when originating from + ## a URL. + + ## <tt>QUERY_STRING</tt>:: The portion of the request URL that + ## follows the <tt>?</tt>, if any. May be + ## empty, but is always required! + + ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and + ## <tt>PATH_INFO</tt>, these variables can be + ## used to complete the URL. Note, however, + ## that <tt>HTTP_HOST</tt>, if present, + ## should be used in preference to + ## <tt>SERVER_NAME</tt> for reconstructing + ## the request URL. + ## <tt>SERVER_NAME</tt> can never be an empty + ## string, and so is always required. + + ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the + ## server is running on. Should be specified if + ## the server is running on a non-standard port. + + ## <tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used + ## for the request. + + ## <tt>HTTP_</tt> Variables:: Variables corresponding to the + ## client-supplied HTTP request + ## headers (i.e., variables whose + ## names begin with <tt>HTTP_</tt>). The + ## presence or absence of these + ## variables should correspond with + ## the presence or absence of the + ## appropriate HTTP header in the + ## request. See + ## {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.url_scheme</tt>:: +http+ or +https+, depending on the + ## request URL. + + ## <tt>rack.input</tt>:: See below, the input stream. + + ## <tt>rack.errors</tt>:: See below, the error stream. + + ## <tt>rack.hijack?</tt>:: See below, if present and true, indicates + ## that the server supports partial hijacking. + + ## <tt>rack.hijack</tt>:: See below, if present, an object responding + ## to +call+ that is used to perform a full + ## hijack. + + ## Additional environment specifications have approved to + ## standardized middleware APIs. None of these are required to + ## be implemented by the server. + + ## <tt>rack.session</tt>:: A hash-like interface for storing + ## request session data. + ## The store must implement: + if session = env[RACK_SESSION] + ## store(key, value) (aliased as []=); + unless session.respond_to?(:store) && session.respond_to?(:[]=) + raise LintError, "session #{session.inspect} must respond to store and []=" + end - ## warn(message, &block) - assert("logger #{logger.inspect} must respond to warn") { - logger.respond_to?(:warn) - } + ## fetch(key, default = nil) (aliased as []); + unless session.respond_to?(:fetch) && session.respond_to?(:[]) + raise LintError, "session #{session.inspect} must respond to fetch and []" + end - ## error(message, &block) - assert("logger #{logger.inspect} must respond to error") { - logger.respond_to?(:error) - } + ## delete(key); + unless session.respond_to?(:delete) + raise LintError, "session #{session.inspect} must respond to delete" + end - ## fatal(message, &block) - assert("logger #{logger.inspect} must respond to fatal") { - logger.respond_to?(:fatal) - } - end + ## clear; + unless session.respond_to?(:clear) + raise LintError, "session #{session.inspect} must respond to clear" + end - ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes. - if bufsize = env[RACK_MULTIPART_BUFFER_SIZE] - assert("rack.multipart.buffer_size must be an Integer > 0 if specified") { - bufsize.is_a?(Integer) && bufsize > 0 - } - end + ## to_hash (returning unfrozen Hash instance); + unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen? + raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance" + end + end + + ## <tt>rack.logger</tt>:: A common object interface for logging messages. + ## The object must implement: + if logger = env[RACK_LOGGER] + ## info(message, &block) + unless logger.respond_to?(:info) + raise LintError, "logger #{logger.inspect} must respond to info" + end + + ## debug(message, &block) + unless logger.respond_to?(:debug) + raise LintError, "logger #{logger.inspect} must respond to debug" + end + + ## warn(message, &block) + unless logger.respond_to?(:warn) + raise LintError, "logger #{logger.inspect} must respond to warn" + end + + ## error(message, &block) + unless logger.respond_to?(:error) + raise LintError, "logger #{logger.inspect} must respond to error" + end - ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile. - if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY] - assert("rack.multipart.tempfile_factory must respond to #call") { tempfile_factory.respond_to?(:call) } - env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type| - io = tempfile_factory.call(filename, content_type) - assert("rack.multipart.tempfile_factory return value must respond to #<<") { io.respond_to?(:<<) } - io + ## fatal(message, &block) + unless logger.respond_to?(:fatal) + raise LintError, "logger #{logger.inspect} must respond to fatal" + end end - end - ## The server or the application can store their own data in the - ## environment, too. The keys must contain at least one dot, - ## and should be prefixed uniquely. The prefix <tt>rack.</tt> - ## is reserved for use with the Rack core distribution and other - ## accepted specifications and must not be used otherwise. - ## + ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes. + if bufsize = env[RACK_MULTIPART_BUFFER_SIZE] + unless bufsize.is_a?(Integer) && bufsize > 0 + raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified" + end + end + + ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile. + if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY] + raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call) + env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type| + io = tempfile_factory.call(filename, content_type) + raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<) + io + end + end - %w[REQUEST_METHOD SERVER_NAME QUERY_STRING - rack.version rack.input rack.errors - rack.multithread rack.multiprocess rack.run_once].each { |header| - assert("env missing required key #{header}") { env.include? header } - } + ## The server or the application can store their own data in the + ## environment, too. The keys must contain at least one dot, + ## and should be prefixed uniquely. The prefix <tt>rack.</tt> + ## is reserved for use with the Rack core distribution and other + ## accepted specifications and must not be used otherwise. + ## + + %w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL + rack.input rack.errors].each { |header| + raise LintError, "env missing required key #{header}" unless env.include? header + } - ## The <tt>SERVER_PORT</tt> must be an Integer if set. - assert("env[SERVER_PORT] is not an Integer") do + ## The <tt>SERVER_PORT</tt> must be an Integer if set. server_port = env["SERVER_PORT"] - server_port.nil? || (Integer(server_port) rescue false) - end + unless server_port.nil? || (Integer(server_port) rescue false) + raise LintError, "env[SERVER_PORT] is not an Integer" + end - ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540. - assert("#{env[SERVER_NAME]} must be a valid authority") do - URI.parse("http://#{env[SERVER_NAME]}/") rescue false - end + ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540. + unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false) + raise LintError, "#{env[SERVER_NAME]} must be a valid authority" + end - ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540. - assert("#{env[HTTP_HOST]} must be a valid authority") do - URI.parse("http://#{env[HTTP_HOST]}/") rescue false - end + ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540. + unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false) + raise LintError, "#{env[HTTP_HOST]} must be a valid authority" + end - ## The environment must not contain the keys - ## <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]}") { - not env.include? header - } - } - - ## The CGI keys (named without a period) must have String values. - ## If the string values for CGI keys contain non-ASCII characters, - ## they should use ASCII-8BIT encoding. - env.each { |key, value| - next if key.include? "." # Skip extensions - assert("env variable #{key} has non-string value #{value.inspect}") { - value.kind_of? String - } - next if value.encoding == Encoding::ASCII_8BIT - assert("env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}") { - value.b !~ /[\x80-\xff]/n + ## The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>. + server_protocol = env['SERVER_PROTOCOL'] + unless %r{HTTP/\d(\.\d)?}.match?(server_protocol) + raise LintError, "env[SERVER_PROTOCOL] does not match HTTP/\\d(\\.\\d)?" + end + + ## If the <tt>HTTP_VERSION</tt> is present, it must equal the <tt>SERVER_PROTOCOL</tt>. + if env['HTTP_VERSION'] && env['HTTP_VERSION'] != server_protocol + raise LintError, "env[HTTP_VERSION] does not equal env[SERVER_PROTOCOL]" + end + + ## The environment must not contain the keys + ## <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| + if env.include? header + raise LintError, "env contains #{header}, must use #{header[5..-1]}" + end } - } - - ## There are the following restrictions: - - ## * <tt>rack.version</tt> must be an array of Integers. - assert("rack.version must be an Array, was #{env[RACK_VERSION].class}") { - env[RACK_VERSION].kind_of? Array - } - ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+. - assert("rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}") { - %w[http https].include?(env[RACK_URL_SCHEME]) - } - - ## * There must be a valid input stream in <tt>rack.input</tt>. - check_input env[RACK_INPUT] - ## * There must be a valid error stream in <tt>rack.errors</tt>. - check_error env[RACK_ERRORS] - ## * There may be a valid hijack stream in <tt>rack.hijack_io</tt> - check_hijack env - - ## * The <tt>REQUEST_METHOD</tt> must be a valid token. - assert("REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}") { - env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/ - } - - ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt> - assert("SCRIPT_NAME must start with /") { - !env.include?(SCRIPT_NAME) || - env[SCRIPT_NAME] == "" || - env[SCRIPT_NAME] =~ /\A\// - } - ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt> - assert("PATH_INFO must start with /") { - !env.include?(PATH_INFO) || - env[PATH_INFO] == "" || - env[PATH_INFO] =~ /\A\// - } - ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only. - assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") { - !env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/ - } - - ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be - ## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if - ## <tt>SCRIPT_NAME</tt> is empty. - assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") { - env[SCRIPT_NAME] || env[PATH_INFO] - } - ## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty. - assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") { - env[SCRIPT_NAME] != "/" - } - end - ## === The Input Stream - ## - ## The input stream is an IO-like object which contains the raw HTTP - ## POST data. - def check_input(input) - ## When applicable, its external encoding must be "ASCII-8BIT" and it - ## must be opened in binary mode, for Ruby 1.9 compatibility. - assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") { - input.external_encoding == Encoding::ASCII_8BIT - } if input.respond_to?(:external_encoding) - assert("rack.input #{input} is not opened in binary mode") { - input.binmode? - } if input.respond_to?(:binmode?) - - ## The input stream must respond to +gets+, +each+, +read+ and +rewind+. - [:gets, :each, :read, :rewind].each { |method| - assert("rack.input #{input} does not respond to ##{method}") { - input.respond_to? method + ## The CGI keys (named without a period) must have String values. + ## If the string values for CGI keys contain non-ASCII characters, + ## they should use ASCII-8BIT encoding. + env.each { |key, value| + next if key.include? "." # Skip extensions + unless value.kind_of? String + raise LintError, "env variable #{key} has non-string value #{value.inspect}" + end + next if value.encoding == Encoding::ASCII_8BIT + unless value.b !~ /[\x80-\xff]/n + raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}" + end } - } - end - class InputWrapper - include Assertion + ## There are the following restrictions: - def initialize(input) - @input = input - end + ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+. + unless %w[http https].include?(env[RACK_URL_SCHEME]) + raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}" + end - ## * +gets+ must be called without arguments and return a string, - ## or +nil+ on EOF. - def gets(*args) - assert("rack.input#gets called with arguments") { args.size == 0 } - v = @input.gets - assert("rack.input#gets didn't return a String") { - v.nil? or v.kind_of? String - } - v + ## * There must be a valid input stream in <tt>rack.input</tt>. + check_input env[RACK_INPUT] + ## * There must be a valid error stream in <tt>rack.errors</tt>. + check_error env[RACK_ERRORS] + ## * There may be a valid hijack callback in <tt>rack.hijack</tt> + check_hijack env + + ## * The <tt>REQUEST_METHOD</tt> must be a valid token. + unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/ + raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}" + end + + ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt> + if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\// + raise LintError, "SCRIPT_NAME must start with /" + end + ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt> + if env.include?(PATH_INFO) && env[PATH_INFO] != "" && env[PATH_INFO] !~ /\A\// + raise LintError, "PATH_INFO must start with /" + end + ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only. + if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/ + raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}" + end + + ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be + ## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if + ## <tt>SCRIPT_NAME</tt> is empty. + unless env[SCRIPT_NAME] || env[PATH_INFO] + raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)" + end + ## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty. + unless env[SCRIPT_NAME] != "/" + raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'" + end + + ## <tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been + ## processed. This would typically be invoked after sending the response to the client, but it could also be + ## invoked if an error occurs while generating the response or sending the response; in that case, the error + ## argument will be a subclass of +Exception+. + ## The callables are invoked with +env, status, headers, error+ arguments and should not raise any + ## exceptions. They should be invoked in reverse order of registration. + if callables = env[RACK_RESPONSE_FINISHED] + raise LintError, "rack.response_finished must be an array of callable objects" unless callables.is_a?(Array) + + callables.each do |callable| + raise LintError, "rack.response_finished values must respond to call(env, status, headers, error)" unless callable.respond_to?(:call) + end + end end - ## * +read+ behaves like IO#read. - ## Its signature is <tt>read([length, [buffer]])</tt>. - ## - ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+, - ## and +buffer+ must be a String and may not be nil. ## - ## If +length+ is given and not nil, then this method reads at most - ## +length+ bytes from the input stream. + ## === The Input Stream ## - ## If +length+ is not given or nil, then this method reads - ## all data until EOF. - ## - ## When EOF is reached, this method returns nil if +length+ is given - ## and not nil, or "" if +length+ is not given or is nil. - ## - ## If +buffer+ is given, then the read data will be placed - ## into +buffer+ instead of a newly created String object. - def read(*args) - assert("rack.input#read called with too many arguments") { - args.size <= 2 - } - if args.size >= 1 - assert("rack.input#read called with non-integer and non-nil length") { - args.first.kind_of?(Integer) || args.first.nil? - } - assert("rack.input#read called with a negative length") { - args.first.nil? || args.first >= 0 - } + ## The input stream is an IO-like object which contains the raw HTTP + ## POST data. + def check_input(input) + ## When applicable, its external encoding must be "ASCII-8BIT" and it + ## must be opened in binary mode, for Ruby 1.9 compatibility. + if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT + raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding" end - if args.size >= 2 - assert("rack.input#read called with non-String buffer") { - args[1].kind_of?(String) - } + if input.respond_to?(:binmode?) && !input.binmode? + raise LintError, "rack.input #{input} is not opened in binary mode" end - v = @input.read(*args) - - assert("rack.input#read didn't return nil or a String") { - v.nil? or v.kind_of? String + ## The input stream must respond to +gets+, +each+, and +read+. + [:gets, :each, :read].each { |method| + unless input.respond_to? method + raise LintError, "rack.input #{input} does not respond to ##{method}" + end } - if args[0].nil? - assert("rack.input#read(nil) returned nil on EOF") { - !v.nil? - } + end + + class InputWrapper + def initialize(input) + @input = input end - v - end + ## * +gets+ must be called without arguments and return a string, + ## or +nil+ on EOF. + def gets(*args) + raise LintError, "rack.input#gets called with arguments" unless args.size == 0 + v = @input.gets + unless v.nil? or v.kind_of? String + raise LintError, "rack.input#gets didn't return a String" + end + v + end - ## * +each+ must be called without arguments and only yield Strings. - def each(*args) - assert("rack.input#each called with arguments") { args.size == 0 } - @input.each { |line| - assert("rack.input#each didn't yield a String") { - line.kind_of? String + ## * +read+ behaves like IO#read. + ## Its signature is <tt>read([length, [buffer]])</tt>. + ## + ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+, + ## and +buffer+ must be a String and may not be nil. + ## + ## If +length+ is given and not nil, then this method reads at most + ## +length+ bytes from the input stream. + ## + ## If +length+ is not given or nil, then this method reads + ## all data until EOF. + ## + ## When EOF is reached, this method returns nil if +length+ is given + ## and not nil, or "" if +length+ is not given or is nil. + ## + ## If +buffer+ is given, then the read data will be placed + ## into +buffer+ instead of a newly created String object. + def read(*args) + unless args.size <= 2 + raise LintError, "rack.input#read called with too many arguments" + end + if args.size >= 1 + unless args.first.kind_of?(Integer) || args.first.nil? + raise LintError, "rack.input#read called with non-integer and non-nil length" + end + unless args.first.nil? || args.first >= 0 + raise LintError, "rack.input#read called with a negative length" + end + end + if args.size >= 2 + unless args[1].kind_of?(String) + raise LintError, "rack.input#read called with non-String buffer" + end + end + + v = @input.read(*args) + + unless v.nil? or v.kind_of? String + raise LintError, "rack.input#read didn't return nil or a String" + end + if args[0].nil? + unless !v.nil? + raise LintError, "rack.input#read(nil) returned nil on EOF" + end + end + + v + end + + ## * +each+ must be called without arguments and only yield Strings. + def each(*args) + raise LintError, "rack.input#each called with arguments" unless args.size == 0 + @input.each { |line| + unless line.kind_of? String + raise LintError, "rack.input#each didn't yield a String" + end + yield line } - yield line - } + end + + ## * +close+ can be called on the input stream to indicate that the + ## any remaining input is not needed. + def close(*args) + @input.close(*args) + end end - ## * +rewind+ must be called without arguments. It rewinds the input - ## stream back to the beginning. It must not raise Errno::ESPIPE: - ## that is, it may not be a pipe or a socket. Therefore, handler - ## developers must buffer the input data into some rewindable object - ## if the underlying input stream is not rewindable. - def rewind(*args) - assert("rack.input#rewind called with arguments") { args.size == 0 } - assert("rack.input#rewind raised Errno::ESPIPE") { - begin - @input.rewind - true - rescue Errno::ESPIPE - false + ## + ## === The Error Stream + ## + def check_error(error) + ## The error stream must respond to +puts+, +write+ and +flush+. + [:puts, :write, :flush].each { |method| + unless error.respond_to? method + raise LintError, "rack.error #{error} does not respond to ##{method}" end } end - ## * +close+ must never be called on the input stream. - def close(*args) - assert("rack.input#close must not be called") { false } - end - end + class ErrorWrapper + def initialize(error) + @error = error + end - ## === The Error Stream - def check_error(error) - ## The error stream must respond to +puts+, +write+ and +flush+. - [:puts, :write, :flush].each { |method| - assert("rack.error #{error} does not respond to ##{method}") { - error.respond_to? method - } - } - end + ## * +puts+ must be called with a single argument that responds to +to_s+. + def puts(str) + @error.puts str + end - class ErrorWrapper - include Assertion + ## * +write+ must be called with a single argument that is a String. + def write(str) + raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String + @error.write str + end - def initialize(error) - @error = error - end + ## * +flush+ must be called without arguments and must be called + ## in order to make the error appear for sure. + def flush + @error.flush + end - ## * +puts+ must be called with a single argument that responds to +to_s+. - def puts(str) - @error.puts str + ## * +close+ must never be called on the error stream. + def close(*args) + raise LintError, "rack.errors#close must not be called" + end end - ## * +write+ must be called with a single argument that is a String. - def write(str) - assert("rack.errors#write not called with a String") { str.kind_of? String } - @error.write str + ## + ## === Hijacking + ## + ## The hijacking interfaces provides a means for an application to take + ## control of the HTTP connection. There are two distinct hijack + ## interfaces: full hijacking where the application takes over the raw + ## connection, and partial hijacking where the application takes over + ## just the response body stream. In both cases, the application is + ## responsible for closing the hijacked stream. + ## + ## Full hijacking only works with HTTP/1. Partial hijacking is functionally + ## equivalent to streaming bodies, and is still optionally supported for + ## backwards compatibility with older Rack versions. + ## + ## ==== Full Hijack + ## + ## Full hijack is used to completely take over an HTTP/1 connection. It + ## occurs before any headers are written and causes the request to + ## ignores any response generated by the application. + ## + ## It is intended to be used when applications need access to raw HTTP/1 + ## connection. + ## + def check_hijack(env) + ## If +rack.hijack+ is present in +env+, it must respond to +call+ + if original_hijack = env[RACK_HIJACK] + raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call) + + env[RACK_HIJACK] = proc do + io = original_hijack.call + + ## and return an +IO+ instance which can be used to read and write + ## to the underlying connection using HTTP/1 semantics and + ## formatting. + raise LintError, "rack.hijack must return an IO instance" unless io.is_a?(IO) + + io + end + end end - ## * +flush+ must be called without arguments and must be called - ## in order to make the error appear for sure. - def flush - @error.flush + ## + ## ==== Partial Hijack + ## + ## Partial hijack is used for bi-directional streaming of the request and + ## response body. It occurs after the status and headers are written by + ## the server and causes the server to ignore the Body of the response. + ## + ## It is intended to be used when applications need bi-directional + ## streaming. + ## + def check_hijack_response(headers, env) + ## If +rack.hijack?+ is present in +env+ and truthy, + if env[RACK_IS_HIJACK] + ## an application may set the special response header +rack.hijack+ + if original_hijack = headers[RACK_HIJACK] + ## to an object that responds to +call+, + unless original_hijack.respond_to?(:call) + raise LintError, 'rack.hijack header must respond to #call' + end + ## accepting a +stream+ argument. + return proc do |io| + original_hijack.call StreamWrapper.new(io) + end + end + ## + ## After the response status and headers have been sent, this hijack + ## callback will be invoked with a +stream+ argument which follows the + ## same interface as outlined in "Streaming Body". Servers must + ## ignore the +body+ part of the response tuple when the + ## +rack.hijack+ response header is present. Using an empty +Array+ + ## instance is recommended. + else + ## + ## The special response header +rack.hijack+ must only be set + ## if the request +env+ has a truthy +rack.hijack?+. + if headers.key?(RACK_HIJACK) + raise LintError, 'rack.hijack header must not be present if server does not support hijacking' + end + end + + nil end - ## * +close+ must never be called on the error stream. - def close(*args) - assert("rack.errors#close must not be called") { false } + ## == The Response + ## + ## === The Status + ## + def check_status(status) + ## This is an HTTP status. It must be an Integer greater than or equal to + ## 100. + unless status.is_a?(Integer) && status >= 100 + raise LintError, "Status must be an Integer >=100" + end end - end - class HijackWrapper - include Assertion - extend Forwardable + ## + ## === The Headers + ## + def check_headers(headers) + ## The headers must be a unfrozen Hash. + unless headers.kind_of?(Hash) + raise LintError, "headers object should be a hash, but isn't (got #{headers.class} as headers)" + end - REQUIRED_METHODS = [ - :read, :write, :read_nonblock, :write_nonblock, :flush, :close, - :close_read, :close_write, :closed? - ] + if headers.frozen? + raise LintError, "headers object should not be frozen, but is" + end - def_delegators :@io, *REQUIRED_METHODS + headers.each do |key, value| + ## The header keys must be Strings. + unless key.kind_of? String + raise LintError, "header key must be a string, was #{key.class}" + end - def initialize(io) - @io = io - REQUIRED_METHODS.each do |meth| - assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth } + ## Special headers starting "rack." are for communicating with the + ## server, and must not be sent back to the client. + next if key.start_with?("rack.") + + ## The header must not contain a +Status+ key. + raise LintError, "header must not contain status" if key == "status" + ## Header keys must conform to RFC7230 token specification, i.e. cannot + ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}". + raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ + ## Header keys must not contain uppercase ASCII characters (A-Z). + raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/ + + ## Header values must be either a String instance, + if value.kind_of?(String) + check_header_value(key, value) + elsif value.kind_of?(Array) + ## or an Array of String instances, + value.each{|value| check_header_value(key, value)} + else + raise LintError, "a header value must be a String or Array of Strings, but the value of '#{key}' is a #{value.class}" + end end end - end - ## === Hijacking - # - # AUTHORS: n.b. The trailing whitespace between paragraphs is important and - # should not be removed. The whitespace creates paragraphs in the RDoc - # output. - # - ## ==== Request (before status) - def check_hijack(env) - if env[RACK_IS_HIJACK] - ## If rack.hijack? is true then rack.hijack must respond to #call. - original_hijack = env[RACK_HIJACK] - assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) } - env[RACK_HIJACK] = proc do - ## rack.hijack must return the io that will also be assigned (or is - ## already present, in rack.hijack_io. - io = original_hijack.call - HijackWrapper.new(io) - ## - ## rack.hijack_io must respond to: - ## <tt>read, write, read_nonblock, write_nonblock, flush, close, - ## close_read, close_write, closed?</tt> - ## - ## The semantics of these IO methods must be a best effort match to - ## those of a normal ruby IO or Socket object, using standard - ## arguments and raising standard exceptions. Servers are encouraged - ## to simply pass on real IO objects, although it is recognized that - ## this approach is not directly compatible with SPDY and HTTP 2.0. - ## - ## IO provided in rack.hijack_io should preference the - ## IO::WaitReadable and IO::WaitWritable APIs wherever supported. - ## - ## There is a deliberate lack of full specification around - ## rack.hijack_io, as semantics will change from server to server. - ## Users are encouraged to utilize this API with a knowledge of their - ## server choice, and servers may extend the functionality of - ## hijack_io to provide additional features to users. The purpose of - ## rack.hijack is for Rack to "get out of the way", as such, Rack only - ## provides the minimum of specification and support. - env[RACK_HIJACK_IO] = HijackWrapper.new(env[RACK_HIJACK_IO]) - io - end - else - ## - ## If rack.hijack? is false, then rack.hijack should not be set. - assert("rack.hijack? is false, but rack.hijack is present") { env[RACK_HIJACK].nil? } - ## - ## If rack.hijack? is false, then rack.hijack_io should not be set. - assert("rack.hijack? is false, but rack.hijack_io is present") { env[RACK_HIJACK_IO].nil? } + def check_header_value(key, value) + ## such that each String instance must not contain characters below 037. + if value =~ /[\000-\037]/ + raise LintError, "invalid header value #{key}: #{value.inspect}" + end end - end - ## ==== Response (after headers) - ## It is also possible to hijack a response after the status and headers - ## have been sent. - def check_hijack_response(headers, env) - - # this check uses headers like a hash, but the spec only requires - # headers respond to #each - headers = Rack::Utils::HeaderHash[headers] - - ## In order to do this, an application may set the special header - ## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt> - ## accepting an argument that conforms to the <tt>rack.hijack_io</tt> - ## protocol. - ## - ## After the headers have been sent, and this hijack callback has been - ## called, the application is now responsible for the remaining lifecycle - ## of the IO. The application is also responsible for maintaining HTTP - ## semantics. Of specific note, in almost all cases in the current SPEC, - ## applications will have wanted to specify the header Connection:close in - ## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for - ## returning hijacked sockets to the web server. For that purpose, use the - ## body streaming API instead (progressively yielding strings via each). - ## - ## Servers must ignore the <tt>body</tt> part of the response tuple when - ## the <tt>rack.hijack</tt> response API is in use. - - if env[RACK_IS_HIJACK] && headers[RACK_HIJACK] - assert('rack.hijack header must respond to #call') { - headers[RACK_HIJACK].respond_to? :call + ## + ## === The content-type + ## + def check_content_type(status, headers) + headers.each { |key, value| + ## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx, + ## 204, or 304. + if key == "content-type" + if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i + raise LintError, "content-type header found in #{status} response, not allowed" + end + return + end } - original_hijack = headers[RACK_HIJACK] - proc do |io| - original_hijack.call HijackWrapper.new(io) - end - else - ## - ## The special response header <tt>rack.hijack</tt> must only be set - ## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>. - assert('rack.hijack header must not be present if server does not support hijacking') { - headers[RACK_HIJACK].nil? + end + + ## + ## === The content-length + ## + def check_content_length(status, headers) + headers.each { |key, value| + if key == 'content-length' + ## There must not be a <tt>content-length</tt> header key when the + ## +Status+ is 1xx, 204, or 304. + if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i + raise LintError, "content-length header found in #{status} response, not allowed" + end + @content_length = value + end } + end - nil + def verify_content_length(size) + if @head_request + unless size == 0 + raise LintError, "Response body was given for HEAD request, but should be empty" + end + elsif @content_length + unless @content_length == size.to_s + raise LintError, "content-length header was #{@content_length}, but should be #{size}" + end + end end - end - ## ==== Conventions - ## * Middleware should not use hijack unless it is handling the whole - ## response. - ## * Middleware may wrap the IO object for the response pattern. - ## * Middleware should not wrap the IO object for the request pattern. The - ## request pattern is intended to provide the hijacker with "raw tcp". - - ## == The Response - - ## === The Status - def check_status(status) - ## This is an HTTP status. When parsed as integer (+to_i+), it must be - ## greater than or equal to 100. - assert("Status must be >=100 seen as integer") { status.to_i >= 100 } - end - ## === The Headers - def check_headers(header) - ## The header must respond to +each+, and yield values of key and value. - assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") { - header.respond_to? :each - } - - header.each { |key, value| - ## The header keys must be Strings. - assert("header key must be a string, was #{key.class}") { - key.kind_of? String - } + ## + ## === The Body + ## + ## The Body is typically an +Array+ of +String+ instances, an enumerable + ## that yields +String+ instances, a +Proc+ instance, or a File-like + ## object. + ## + ## The Body must respond to +each+ or +call+. It may optionally respond + ## to +to_path+ or +to_ary+. A Body that responds to +each+ is considered + ## to be an Enumerable Body. A Body that responds to +call+ is considered + ## to be a Streaming Body. + ## + ## A Body that responds to both +each+ and +call+ must be treated as an + ## Enumerable Body, not a Streaming Body. If it responds to +each+, you + ## must call +each+ and not +call+. If the Body doesn't respond to + ## +each+, then you can assume it responds to +call+. + ## + ## The Body must either be consumed or returned. The Body is consumed by + ## optionally calling either +each+ or +call+. + ## Then, if the Body responds to +close+, it must be called to release + ## any resources associated with the generation of the body. + ## In other words, +close+ must always be called at least once; typically + ## after the web server has sent the response to the client, but also in + ## cases where the Rack application makes internal/virtual requests and + ## discards the response. + ## + def close + ## + ## After calling +close+, the Body is considered closed and should not + ## be consumed again. + @closed = true - ## 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 - ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}". - assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ } - - ## The values of the header must be Strings, - assert("a header value must be a String, but the value of " + - "'#{key}' is a #{value.class}") { value.kind_of? String } - ## consisting of lines (for multiple header values, e.g. multiple - ## <tt>Set-Cookie</tt> values) separated by "\\n". - value.split("\n").each { |item| - ## The lines must not contain characters below 037. - assert("invalid header value #{key}: #{item.inspect}") { - item !~ /[\000-\037]/ - } - } - } - end + ## If the original Body is replaced by a new Body, the new Body must + ## also consume the original Body by calling +close+ if possible. + @body.close if @body.respond_to?(:close) - ## === The Content-Type - def check_content_type(status, headers) - headers.each { |key, value| - ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, - ## 204 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.key? status.to_i - } - return + index = @lint.index(self) + unless @env['rack.lint'][0..index].all? {|lint| lint.instance_variable_get(:@closed)} + raise LintError, "Body has not been closed" end - } - end + end - ## === The Content-Length - def check_content_length(status, headers) - headers.each { |key, value| - if key.downcase == 'content-length' - ## There must not be a <tt>Content-Length</tt> header when the - ## +Status+ is 1xx, 204 or 304. - assert("Content-Length header found in #{status} response, not allowed") { - not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i - } - @content_length = value + def verify_to_path + ## + ## If the Body responds to +to_path+, it must return a +String+ + ## path for the local file system whose contents are identical + ## to that produced by calling +each+; this may be used by the + ## server as an alternative, possibly more efficient way to + ## transport the response. The +to_path+ method does not consume + ## the body. + if @body.respond_to?(:to_path) + unless ::File.exist? @body.to_path + raise LintError, "The file identified by body.to_path does not exist" + end end - } - end + end - def verify_content_length(bytes) - if @head_request - assert("Response body was given for HEAD request, but should be empty") { - bytes == 0 - } - elsif @content_length - assert("Content-Length header was #{@content_length}, but should be #{bytes}") { - @content_length == bytes.to_s - } + ## + ## ==== Enumerable Body + ## + def each + ## The Enumerable Body must respond to +each+. + raise LintError, "Enumerable Body must respond to each" unless @body.respond_to?(:each) + + ## It must only be called once. + raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil? + + ## It must not be called after being closed. + raise LintError, "Response body is already closed" if @closed + + @invoked = :each + + @body.each do |chunk| + ## and must only yield String values. + unless chunk.kind_of? String + raise LintError, "Body yielded non-string value #{chunk.inspect}" + end + + ## + ## The Body itself should not be an instance of String, as this will + ## break in Ruby 1.9. + ## + ## Middleware must not call +each+ directly on the Body. + ## Instead, middleware can return a new Body that calls +each+ on the + ## original Body, yielding at least once per iteration. + if @lint[0] == self + @env['rack.lint.body_iteration'] += 1 + else + if (@env['rack.lint.body_iteration'] -= 1) > 0 + raise LintError, "New body must yield at least once per iteration of old body" + end + end + + @size += chunk.bytesize + yield chunk + end + + verify_content_length(@size) + + verify_to_path end - end - ## === The Body - def each - @closed = false - bytes = 0 + BODY_METHODS = {to_ary: true, each: true, call: true, to_path: true} - ## The Body must respond to +each+ - assert("Response body must respond to each") do - @body.respond_to?(:each) + def to_path + @body.to_path end - @body.each { |part| - ## and must only yield String values. - assert("Body yielded non-string value #{part.inspect}") { - part.kind_of? String - } - bytes += part.bytesize - yield part - } - verify_content_length(bytes) + def respond_to?(name, *) + if BODY_METHODS.key?(name) + @body.respond_to?(name) + else + super + end + end + + ## + ## If the Body responds to +to_ary+, it must return an +Array+ whose + ## contents are identical to that produced by calling +each+. + ## Middleware may call +to_ary+ directly on the Body and return a new + ## Body in its place. In other words, middleware can only process the + ## Body directly if it responds to +to_ary+. If the Body responds to both + ## +to_ary+ and +close+, its implementation of +to_ary+ must call + ## +close+. + def to_ary + @body.to_ary.tap do |content| + unless content == @body.enum_for.to_a + raise LintError, "#to_ary not identical to contents produced by calling #each" + end + end + ensure + close + end ## - ## The Body itself should not be an instance of String, as this will - ## break in Ruby 1.9. + ## ==== Streaming Body ## - ## If the Body responds to +close+, it will be called after iteration. If - ## the body is replaced by a middleware after action, the original body - ## must be closed first, if it responds to close. - # XXX howto: assert("Body has not been closed") { @closed } + def call(stream) + ## The Streaming Body must respond to +call+. + raise LintError, "Streaming Body must respond to call" unless @body.respond_to?(:call) + ## It must only be called once. + raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil? - ## - ## If the Body responds to +to_path+, it must return a String - ## identifying the location of a file whose contents are identical - ## to that produced by calling +each+; this may be used by the - ## server as an alternative, possibly more efficient way to - ## transport the response. + ## It must not be called after being closed. + raise LintError, "Response body is already closed" if @closed - if @body.respond_to?(:to_path) - assert("The file identified by body.to_path does not exist") { - ::File.exist? @body.to_path - } + @invoked = :call + + ## It takes a +stream+ argument. + ## + ## The +stream+ argument must implement: + ## <tt>read, write, <<, flush, close, close_read, close_write, closed?</tt> + ## + @body.call(StreamWrapper.new(stream)) end - ## - ## The Body commonly is an Array of Strings, the application - ## instance itself, or a File-like object. - end + class StreamWrapper + extend Forwardable - def close - @closed = true - @body.close if @body.respond_to?(:close) - end + ## The semantics of these IO methods must be a best effort match to + ## those of a normal Ruby IO or Socket object, using standard arguments + ## and raising standard exceptions. Servers are encouraged to simply + ## pass on real IO objects, although it is recognized that this approach + ## is not directly compatible with HTTP/2. + REQUIRED_METHODS = [ + :read, :write, :<<, :flush, :close, + :close_read, :close_write, :closed? + ] + + def_delegators :@stream, *REQUIRED_METHODS + + def initialize(stream) + @stream = stream - # :startdoc: + REQUIRED_METHODS.each do |method_name| + raise LintError, "Stream must respond to #{method_name}" unless stream.respond_to?(method_name) + end + end + end + # :startdoc: + end end end +## ## == Thanks -## Some parts of this specification are adopted from PEP333: Python -## Web Server Gateway Interface -## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank -## everyone involved in that effort. +## Some parts of this specification are adopted from {PEP 333 – Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/] +## I'd like to thank everyone involved in that effort. diff --git a/lib/rack/lobster.rb b/lib/rack/lobster.rb deleted file mode 100644 index b86a625de0c2fcc241b00badd9840344454af499..0000000000000000000000000000000000000000 --- a/lib/rack/lobster.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require 'zlib' - -module Rack - # Paste has a Pony, Rack has a Lobster! - class Lobster - LobsterString = Zlib::Inflate.inflate("eJx9kEEOwyAMBO99xd7MAcytUhPlJyj2 - P6jy9i4k9EQyGAnBarEXeCBqSkntNXsi/ZCvC48zGQoZKikGrFMZvgS5ZHd+aGWVuWwhVF0 - t1drVmiR42HcWNz5w3QanT+2gIvTVCiE1lm1Y0eU4JGmIIbaKwextKn8rvW+p5PIwFl8ZWJ - I8jyiTlhTcYXkekJAzTyYN6E08A+dk8voBkAVTJQ==".delete("\n ").unpack("m*")[0]) - - LambdaLobster = lambda { |env| - if env[QUERY_STRING].include?("flip") - lobster = LobsterString.split("\n"). - map { |line| line.ljust(42).reverse }. - join("\n") - href = "?" - else - lobster = LobsterString - href = "?flip" - end - - 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] - } - - def call(env) - req = Request.new(env) - if req.GET["flip"] == "left" - lobster = LobsterString.split("\n").map do |line| - line.ljust(42).reverse. - gsub('\\', 'TEMP'). - gsub('/', '\\'). - gsub('TEMP', '/'). - gsub('{', '}'). - gsub('(', ')') - end.join("\n") - href = "?flip=right" - elsif req.GET["flip"] == "crash" - raise "Lobster crashed" - else - lobster = LobsterString - href = "?flip=left" - end - - res = Response.new - res.write "<title>Lobstericious!</title>" - res.write "<pre>" - res.write lobster - res.write "</pre>" - res.write "<p><a href='#{href}'>flip!</a></p>" - res.write "<p><a href='?flip=crash'>crash!</a></p>" - res.finish - end - - end -end - -if $0 == __FILE__ - # :nocov: - require_relative '../rack' - Rack::Server.start( - app: Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), Port: 9292 - ) - # :nocov: -end diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb index 4bae3a9034e83b50d3dfbf2fad13722df25fd0a6..342123a0f0880ea806888d4ddf787c1ff40b4419 100644 --- a/lib/rack/lock.rb +++ b/lib/rack/lock.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'thread' +require_relative 'body_proxy' module Rack # Rack::Lock locks every request inside a mutex, so that every request @@ -12,10 +12,8 @@ module Rack def call(env) @mutex.lock - @env = env - @old_rack_multithread = env[RACK_MULTITHREAD] begin - response = @app.call(env.merge!(RACK_MULTITHREAD => false)) + response = @app.call(env) returned = response << BodyProxy.new(response.pop) { unlock } ensure unlock unless returned @@ -26,7 +24,6 @@ module Rack def unlock @mutex.unlock - @env[RACK_MULTITHREAD] = @old_rack_multithread end end end diff --git a/lib/rack/logger.rb b/lib/rack/logger.rb index 6c4bede0cf37eedf036dfd8290f0df43c2b10c13..bdcc069005ab5174c352da14d2a40834290312ff 100644 --- a/lib/rack/logger.rb +++ b/lib/rack/logger.rb @@ -2,6 +2,8 @@ require 'logger' +require_relative 'constants' + module Rack # Sets up rack.logger to write to rack.errors stream class Logger diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb index 41937c9947e63aee85a2e4d8b48860bcce617ee0..ff3145debfcc145219af43935afbae50af178a0d 100644 --- a/lib/rack/media_type.rb +++ b/lib/rack/media_type.rb @@ -15,7 +15,7 @@ module Rack # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 def type(content_type) return nil unless content_type - content_type.split(SPLIT_PATTERN, 2).first.tap &:downcase! + content_type.split(SPLIT_PATTERN, 2).first.tap(&:downcase!) end # The media type parameters provided in CONTENT_TYPE as a Hash, or diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb index b586f5339b6fb13313979fc530d5a6b0cd224fec..6125b1916f8f903fe8e3885426451f33228b7870 100644 --- a/lib/rack/method_override.rb +++ b/lib/rack/method_override.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'request' +require_relative 'utils' + module Rack class MethodOverride HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK] @@ -42,7 +46,7 @@ module Rack end def method_override_param(req) - req.POST[METHOD_OVERRIDE_PARAM_KEY] + req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data? rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params" rescue EOFError diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb index f6c02c1fd6ad871eb09e4bf3783a37c8dc6b91d0..b62e3e8d6fb4dd8830390375ab5d51c7b5beb3bc 100644 --- a/lib/rack/mime.rb +++ b/lib/rack/mime.rb @@ -63,6 +63,7 @@ module Rack ".aif" => "audio/x-aiff", ".aiff" => "audio/x-aiff", ".ami" => "application/vnd.amiga.ami", + ".apng" => "image/apng", ".appcache" => "text/cache-manifest", ".apr" => "application/vnd.lotus-approach", ".asc" => "application/pgp-signature", @@ -77,6 +78,7 @@ module Rack ".atx" => "application/vnd.antix.game-component", ".au" => "audio/basic", ".avi" => "video/x-msvideo", + ".avif" => "image/avif", ".bat" => "application/x-msdownload", ".bcpio" => "application/x-bcpio", ".bdm" => "application/vnd.syncml.dm+wbxml", @@ -197,6 +199,7 @@ module Rack ".fe_launch" => "application/vnd.denovo.fcselayout-link", ".fg5" => "application/vnd.fujitsu.oasysgp", ".fli" => "video/x-fli", + ".flif" => "image/flif", ".flo" => "application/vnd.micrografx.flo", ".flv" => "video/x-flv", ".flw" => "application/vnd.kde.kivio", @@ -237,6 +240,10 @@ module Rack ".h264" => "video/h264", ".hbci" => "application/vnd.hbci", ".hdf" => "application/x-hdf", + ".heic" => "image/heic", + ".heics" => "image/heic-sequence", + ".heif" => "image/heif", + ".heifs" => "image/heif-sequence", ".hh" => "text/x-c", ".hlp" => "application/winhlp", ".hpgl" => "application/vnd.hp-hpgl", @@ -617,6 +624,7 @@ module Rack ".wbs" => "application/vnd.criticaltools.wbs+xml", ".wbxml" => "application/vnd.wap.wbxml", ".webm" => "video/webm", + ".webp" => "image/webp", ".wm" => "video/x-ms-wm", ".wma" => "audio/x-ms-wma", ".wmd" => "application/x-ms-wmd", diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb index 5b2512ca091dec59ee53a65a17108a4662f3d569..5e5c457c728c42850d78c86aaa448af261569207 100644 --- a/lib/rack/mock.rb +++ b/lib/rack/mock.rb @@ -1,273 +1,3 @@ # frozen_string_literal: true -require 'uri' -require 'stringio' -require_relative '../rack' -require 'cgi/cookie' - -module Rack - # Rack::MockRequest helps testing your Rack application without - # actually using HTTP. - # - # After performing a request on a URL with get/post/put/patch/delete, it - # returns a MockResponse with useful helper methods for effective - # testing. - # - # You can pass a hash with additional configuration to the - # get/post/put/patch/delete. - # <tt>:input</tt>:: A String or IO-like to be used as rack.input. - # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors. - # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint. - - class MockRequest - class FatalWarning < RuntimeError - end - - class FatalWarner - def puts(warning) - raise FatalWarning, warning - end - - def write(warning) - raise FatalWarning, warning - end - - def flush - end - - def string - "" - end - end - - DEFAULT_ENV = { - RACK_VERSION => Rack::VERSION, - RACK_INPUT => StringIO.new, - RACK_ERRORS => StringIO.new, - RACK_MULTITHREAD => true, - RACK_MULTIPROCESS => true, - RACK_RUNONCE => false, - }.freeze - - def initialize(app) - @app = app - end - - # Make a GET request and return a MockResponse. See #request. - def get(uri, opts = {}) request(GET, uri, opts) end - # Make a POST request and return a MockResponse. See #request. - def post(uri, opts = {}) request(POST, uri, opts) end - # Make a PUT request and return a MockResponse. See #request. - def put(uri, opts = {}) request(PUT, uri, opts) end - # Make a PATCH request and return a MockResponse. See #request. - def patch(uri, opts = {}) request(PATCH, uri, opts) end - # Make a DELETE request and return a MockResponse. See #request. - def delete(uri, opts = {}) request(DELETE, uri, opts) end - # Make a HEAD request and return a MockResponse. See #request. - def head(uri, opts = {}) request(HEAD, uri, opts) end - # Make an OPTIONS request and return a MockResponse. See #request. - def options(uri, opts = {}) request(OPTIONS, uri, opts) end - - # Make a request using the given request method for the given - # uri to the rack application and return a MockResponse. - # Options given are passed to MockRequest.env_for. - def request(method = GET, uri = "", opts = {}) - env = self.class.env_for(uri, opts.merge(method: method)) - - if opts[:lint] - app = Rack::Lint.new(@app) - else - app = @app - end - - errors = env[RACK_ERRORS] - status, headers, body = app.call(env) - MockResponse.new(status, headers, body, errors) - ensure - body.close if body.respond_to?(:close) - end - - # For historical reasons, we're pinning to RFC 2396. - # URI::Parser = URI::RFC2396_Parser - def self.parse_uri_rfc2396(uri) - @parser ||= URI::Parser.new - @parser.parse(uri) - end - - # Return the Rack environment used for a request to +uri+. - # All options that are strings are added to the returned environment. - # Options: - # :fatal :: Whether to raise an exception if request outputs to rack.errors - # :input :: The rack.input to set - # :method :: The HTTP request method to use - # :params :: The params to use - # :script_name :: The SCRIPT_NAME to set - def self.env_for(uri = "", opts = {}) - uri = parse_uri_rfc2396(uri) - uri.path = "/#{uri.path}" unless uri.path[0] == ?/ - - env = DEFAULT_ENV.dup - - env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b - env[SERVER_NAME] = (uri.host || "example.org").b - env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b - env[QUERY_STRING] = (uri.query.to_s).b - env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b - env[RACK_URL_SCHEME] = (uri.scheme || "http").b - env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b - - env[SCRIPT_NAME] = opts[:script_name] || "" - - if opts[:fatal] - env[RACK_ERRORS] = FatalWarner.new - else - env[RACK_ERRORS] = StringIO.new - end - - if params = opts[:params] - if env[REQUEST_METHOD] == GET - params = Utils.parse_nested_query(params) if params.is_a?(String) - params.update(Utils.parse_nested_query(env[QUERY_STRING])) - env[QUERY_STRING] = Utils.build_nested_query(params) - elsif !opts.has_key?(:input) - opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded" - if params.is_a?(Hash) - if data = Rack::Multipart.build_multipart(params) - opts[:input] = data - opts["CONTENT_LENGTH"] ||= data.length.to_s - opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}" - else - opts[:input] = Utils.build_nested_query(params) - end - else - opts[:input] = params - end - end - end - - empty_str = String.new - opts[:input] ||= empty_str - if String === opts[:input] - rack_input = StringIO.new(opts[:input]) - else - rack_input = opts[:input] - end - - rack_input.set_encoding(Encoding::BINARY) - env[RACK_INPUT] = rack_input - - 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 - } - - env - end - end - - # Rack::MockResponse provides useful helpers for testing your apps. - # Usually, you don't create the MockResponse on your own, but use - # MockRequest. - - class MockResponse < Rack::Response - class << self - alias [] new - end - - # Headers - attr_reader :original_headers, :cookies - - # Errors - attr_accessor :errors - - 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) - body =~ other - end - - def match(other) - body.match other - end - - def body - # FIXME: apparently users of MockResponse expect the return value of - # MockResponse#body to be a string. However, the real response object - # returns the body as a list. - # - # See spec_showstatus.rb: - # - # should "not replace existing messages" do - # ... - # res.body.should == "foo!" - # end - 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 +require_relative 'mock_request' diff --git a/lib/rack/mock_request.rb b/lib/rack/mock_request.rb new file mode 100644 index 0000000000000000000000000000000000000000..b6d7ef4fea53b10e9f9186505940c707c44b2651 --- /dev/null +++ b/lib/rack/mock_request.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require 'uri' +require 'stringio' + +require_relative 'constants' +require_relative 'mock_response' + +module Rack + # Rack::MockRequest helps testing your Rack application without + # actually using HTTP. + # + # After performing a request on a URL with get/post/put/patch/delete, it + # returns a MockResponse with useful helper methods for effective + # testing. + # + # You can pass a hash with additional configuration to the + # get/post/put/patch/delete. + # <tt>:input</tt>:: A String or IO-like to be used as rack.input. + # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors. + # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint. + + class MockRequest + class FatalWarning < RuntimeError + end + + class FatalWarner + def puts(warning) + raise FatalWarning, warning + end + + def write(warning) + raise FatalWarning, warning + end + + def flush + end + + def string + "" + end + end + + DEFAULT_ENV = { + RACK_INPUT => StringIO.new, + RACK_ERRORS => StringIO.new, + }.freeze + + def initialize(app) + @app = app + end + + # Make a GET request and return a MockResponse. See #request. + def get(uri, opts = {}) request(GET, uri, opts) end + # Make a POST request and return a MockResponse. See #request. + def post(uri, opts = {}) request(POST, uri, opts) end + # Make a PUT request and return a MockResponse. See #request. + def put(uri, opts = {}) request(PUT, uri, opts) end + # Make a PATCH request and return a MockResponse. See #request. + def patch(uri, opts = {}) request(PATCH, uri, opts) end + # Make a DELETE request and return a MockResponse. See #request. + def delete(uri, opts = {}) request(DELETE, uri, opts) end + # Make a HEAD request and return a MockResponse. See #request. + def head(uri, opts = {}) request(HEAD, uri, opts) end + # Make an OPTIONS request and return a MockResponse. See #request. + def options(uri, opts = {}) request(OPTIONS, uri, opts) end + + # Make a request using the given request method for the given + # uri to the rack application and return a MockResponse. + # Options given are passed to MockRequest.env_for. + def request(method = GET, uri = "", opts = {}) + env = self.class.env_for(uri, opts.merge(method: method)) + + if opts[:lint] + app = Rack::Lint.new(@app) + else + app = @app + end + + errors = env[RACK_ERRORS] + status, headers, body = app.call(env) + MockResponse.new(status, headers, body, errors) + ensure + body.close if body.respond_to?(:close) + end + + # For historical reasons, we're pinning to RFC 2396. + # URI::Parser = URI::RFC2396_Parser + def self.parse_uri_rfc2396(uri) + @parser ||= URI::Parser.new + @parser.parse(uri) + end + + # Return the Rack environment used for a request to +uri+. + # All options that are strings are added to the returned environment. + # Options: + # :fatal :: Whether to raise an exception if request outputs to rack.errors + # :input :: The rack.input to set + # :http_version :: The SERVER_PROTOCOL to set + # :method :: The HTTP request method to use + # :params :: The params to use + # :script_name :: The SCRIPT_NAME to set + def self.env_for(uri = "", opts = {}) + uri = parse_uri_rfc2396(uri) + uri.path = "/#{uri.path}" unless uri.path[0] == ?/ + + env = DEFAULT_ENV.dup + + env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b + env[SERVER_NAME] = (uri.host || "example.org").b + env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b + env[SERVER_PROTOCOL] = opts[:http_version] || 'HTTP/1.1' + env[QUERY_STRING] = (uri.query.to_s).b + env[PATH_INFO] = (uri.path).b + env[RACK_URL_SCHEME] = (uri.scheme || "http").b + env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b + + env[SCRIPT_NAME] = opts[:script_name] || "" + + if opts[:fatal] + env[RACK_ERRORS] = FatalWarner.new + else + env[RACK_ERRORS] = StringIO.new + end + + if params = opts[:params] + if env[REQUEST_METHOD] == GET + params = Utils.parse_nested_query(params) if params.is_a?(String) + params.update(Utils.parse_nested_query(env[QUERY_STRING])) + env[QUERY_STRING] = Utils.build_nested_query(params) + elsif !opts.has_key?(:input) + opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + if params.is_a?(Hash) + if data = Rack::Multipart.build_multipart(params) + opts[:input] = data + opts["CONTENT_LENGTH"] ||= data.length.to_s + opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}" + else + opts[:input] = Utils.build_nested_query(params) + end + else + opts[:input] = params + end + end + end + + opts[:input] ||= String.new + if String === opts[:input] + rack_input = StringIO.new(opts[:input]) + else + rack_input = opts[:input] + end + + rack_input.set_encoding(Encoding::BINARY) + env[RACK_INPUT] = rack_input + + 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 + } + + env + end + end +end diff --git a/lib/rack/mock_response.rb b/lib/rack/mock_response.rb new file mode 100644 index 0000000000000000000000000000000000000000..3c70bb711a92635da46f77c662dc1eeff77d3fe9 --- /dev/null +++ b/lib/rack/mock_response.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'cgi/cookie' +require 'time' + +require_relative 'response' + +module Rack + # Rack::MockResponse provides useful helpers for testing your apps. + # Usually, you don't create the MockResponse on your own, but use + # MockRequest. + + class MockResponse < Rack::Response + class << self + alias [] new + end + + # Headers + attr_reader :original_headers, :cookies + + # Errors + attr_accessor :errors + + def initialize(status, headers, body, errors = nil) + @original_headers = headers + + if errors + @errors = errors.string if errors.respond_to?(:string) + else + @errors = "" + end + + super(body, status, headers) + + @cookies = parse_cookies_from_header + buffered_body! + end + + def =~(other) + body =~ other + end + + def match(other) + body.match other + end + + def body + return @buffered_body if defined?(@buffered_body) + + # FIXME: apparently users of MockResponse expect the return value of + # MockResponse#body to be a string. However, the real response object + # returns the body as a list. + # + # See spec_showstatus.rb: + # + # should "not replace existing messages" do + # ... + # res.body.should == "foo!" + # end + buffer = @buffered_body = String.new + + @body.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 headers.has_key? 'set-cookie' + set_cookie_header = headers.fetch('set-cookie') + Array(set_cookie_header).each do |header_value| + header_value.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 + 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.drop(1).each do |bit| + if bit.include? '=' + cookie_attribute, attribute_value = bit.split('=', 2) + cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip) + end + if bit.include? 'secure' + cookie_attributes.store('secure', true) + end + end + + if cookie_attributes.key? 'max-age' + cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i) + elsif cookie_attributes.key? 'expires' + cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires'])) + end + + cookie_attributes + end + + end +end diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb index fdae808a83fdc329e086e1b83194916eddf1ef8d..3347662acc3fb00acfaa29c01754251fa99c0e3b 100644 --- a/lib/rack/multipart.rb +++ b/lib/rack/multipart.rb @@ -1,64 +1,44 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' + require_relative 'multipart/parser' +require_relative 'multipart/generator' module Rack # A multipart form data parser, adapted from IOWA. # # Usually, Rack::Request#POST takes care of calling this. module Multipart - autoload :UploadedFile, 'rack/multipart/uploaded_file' - autoload :Generator, 'rack/multipart/generator' - - EOL = "\r\n" MULTIPART_BOUNDARY = "AaB03x" - MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni - TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/ - CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i - VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/ - BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i - MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni - MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni - MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni - # Updated definitions from RFC 2231 - ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]} - ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/ - SECTION = /\*[0-9]+/ - REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/ - REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/ - EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/ - EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/ - EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/ - EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/ - EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/ - EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/ - EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/ - DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/ - RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i class << self def parse_multipart(env, params = Rack::Utils.default_query_parser) - extract_multipart Rack::Request.new(env), params - end + io = env[RACK_INPUT] + + if content_length = env['CONTENT_LENGTH'] + content_length = content_length.to_i + end - def extract_multipart(req, params = Rack::Utils.default_query_parser) - io = req.get_header(RACK_INPUT) - io.rewind - content_length = req.content_length - content_length = content_length.to_i if content_length + content_type = env['CONTENT_TYPE'] - tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY - bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE + tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY + bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE - info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params - req.set_header(RACK_TEMPFILES, info.tmp_files) - info.params + info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params) + env[RACK_TEMPFILES] = info.tmp_files + + return info.params + end + + def extract_multipart(request, params = Rack::Utils.default_query_parser) + parse_multipart(request.env) end def build_multipart(params, first = true) Generator.new(params, first).dump end end - end end diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb index f798a98c5101253082d62041d8ab10a4b32d3b0a..30d7f51dd1983ffce6b70ffda01ae42607321411 100644 --- a/lib/rack/multipart/generator.rb +++ b/lib/rack/multipart/generator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'uploaded_file' + module Rack module Multipart class Generator @@ -74,12 +76,12 @@ module Rack def content_for_tempfile(io, file, name) length = ::File.stat(file.path).size if file.path - filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename + filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\"" <<-EOF --#{MULTIPART_BOUNDARY}\r -Content-Disposition: form-data; name="#{name}"#{filename}\r -Content-Type: #{file.content_type}\r -#{"Content-Length: #{length}\r\n" if length}\r +content-disposition: form-data; name="#{name}"#{filename}\r +content-type: #{file.content_type}\r +#{"content-length: #{length}\r\n" if length}\r #{io.read}\r EOF end @@ -87,7 +89,7 @@ EOF def content_for_other(file, name) <<-EOF --#{MULTIPART_BOUNDARY}\r -Content-Disposition: form-data; name="#{name}"\r +content-disposition: form-data; name="#{name}"\r \r #{file}\r EOF diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb index 0fc185603101c477352e9d077148478a82deac29..2469459d7ffb3f854611c4ebc06c2bf5e9d00890 100644 --- a/lib/rack/multipart/parser.rb +++ b/lib/rack/multipart/parser.rb @@ -2,22 +2,54 @@ require 'strscan' +require_relative '../utils' + module Rack module Multipart class MultipartPartLimitError < Errno::EMFILE; end + class MultipartTotalPartLimitError < StandardError; end - class Parser - (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' + # Use specific error class when parsing multipart request + # that ends early. + class EmptyContentError < ::EOFError; end + + # Base class for multipart exceptions that do not subclass from + # other exception classes for backwards compatibility. + class Error < StandardError; end + + EOL = "\r\n" + MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni + TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/ + CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i + VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/ + BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i + MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni + MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni + MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni + # Updated definitions from RFC 2231 + ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]} + ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/ + SECTION = /\*[0-9]+/ + REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/ + REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/ + EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/ + EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/ + EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/ + EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/ + EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/ + EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/ + EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/ + DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/ + RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i + class Parser BUFSIZE = 1_048_576 TEXT_PLAIN = "text/plain" TEMPFILE_FACTORY = lambda { |filename, content_type| 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 @@ -39,16 +71,12 @@ module Rack if str @cursor += str.bytesize else - # Raise an error for mismatching Content-Length and actual contents + # Raise an error for mismatching content-length and actual contents raise EOFError, "bad content body" end str end - - def rewind - @io.rewind - end end MultipartInfo = Struct.new :params, :tmp_files @@ -67,18 +95,17 @@ module Rack boundary = parse_boundary content_type return EMPTY unless boundary + if boundary.length > 70 + # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary. + # Most clients use no more than 55 characters. + raise Error, "multipart boundary size too large (#{boundary.length} characters)" + end + io = BoundedIO.new(io, content_length) if content_length - outbuf = String.new parser = new(boundary, tmpfile, bufsize, qp) - parser.on_read io.read(bufsize, outbuf) + parser.parse(io) - loop do - break if parser.state == :DONE - parser.on_read io.read(bufsize, outbuf) - end - - io.rewind parser.result end @@ -178,32 +205,46 @@ module Rack def initialize(boundary, tempfile, bufsize, query_parser) @query_parser = query_parser @params = query_parser.make_params - @boundary = "--#{boundary}" @bufsize = bufsize - @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 + @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m + @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish) @head_regex = /(.*?#{EOL})#{EOL}/m end - def on_read(content) - handle_empty_content!(content) - @sbuf.concat content - run_parser + def parse(io) + outbuf = String.new + read_data(io, outbuf) + + loop do + status = + case @state + when :FAST_FORWARD + handle_fast_forward + when :CONSUME_TOKEN + handle_consume_token + when :MIME_HEAD + handle_mime_head + when :MIME_BODY + handle_mime_body + else # when :DONE + return + end + + read_data(io, outbuf) if status == :want_read + end end def result @collector.each do |part| part.get_data do |data| tag_multipart_encoding(part.filename, part.content_type, part.name, data) - @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit) + @query_parser.normalize_params(@params, part.name, data) end end MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body) @@ -211,29 +252,38 @@ module Rack private - def run_parser - loop do - case @state - when :FAST_FORWARD - break if handle_fast_forward == :want_read - when :CONSUME_TOKEN - break if handle_consume_token == :want_read - when :MIME_HEAD - break if handle_mime_head == :want_read - when :MIME_BODY - break if handle_mime_body == :want_read - when :DONE - break - end - end + def dequote(str) # From WEBrick::HTTPUtils + ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup + ret.gsub!(/\\(.)/, "\\1") + ret end + def read_data(io, outbuf) + content = io.read(@bufsize, outbuf) + handle_empty_content!(content) + @sbuf.concat(content) + end + + # This handles the initial parser state. We read until we find the starting + # boundary, then we can transition to the next state. If we find the ending + # boundary, this is an invalid multipart upload, but keep scanning for opening + # boundary in that case. If no boundary found, we need to keep reading data + # and retry. It's highly unlikely the initial read will not consume the + # boundary. The client would have to deliberately craft a response + # with the opening boundary beyond the buffer size for that to happen. def handle_fast_forward - if consume_boundary - @state = :MIME_HEAD - else - raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize - :want_read + while true + case consume_boundary + when :BOUNDARY + # found opening boundary, transition to next state + @state = :MIME_HEAD + return + when :END_BOUNDARY + # invalid multipart upload, but retry for opening boundary + else + # no boundary found, keep reading data + return :want_read + end end end @@ -252,7 +302,7 @@ module Rack head = @sbuf[1] content_type = head[MULTIPART_CONTENT_TYPE, 1] if name = head[MULTIPART_CONTENT_DISPOSITION, 1] - name = Rack::Auth::Digest::Params::dequote(name) + name = dequote(name) else name = head[MULTIPART_CONTENT_ID, 1] end @@ -289,15 +339,16 @@ module Rack end end - def full_boundary; @full_boundary; end - + # Scan until the we find the start or end of the boundary. + # If we find it, return the appropriate symbol for the start or + # end of the boundary. If we don't find the start or end of the + # boundary, clear the buffer and return nil. def consume_boundary - 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 @sbuf.eos? + if read_buffer = @sbuf.scan_until(@body_regex) + read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY + else + @sbuf.terminate + nil end end @@ -307,10 +358,10 @@ module Rack when RFC2183 params = Hash[*head.scan(DISPPARM).flat_map(&:compact)] - if filename = params['filename'] - filename = $1 if filename =~ /^"(.*)"$/ - elsif filename = params['filename*'] + if filename = params['filename*'] encoding, _, filename = filename.split("'", 3) + elsif filename = params['filename'] + filename = $1 if filename =~ /^"(.*)"$/ end when BROKEN filename = $1 @@ -337,6 +388,7 @@ module Rack end CHARSET = "charset" + deprecate_constant :CHARSET def tag_multipart_encoding(filename, content_type, name, body) name = name.to_s @@ -357,7 +409,13 @@ module Rack k.strip! v.strip! v = v[1..-2] if v.start_with?('"') && v.end_with?('"') - encoding = Encoding.find v if k == CHARSET + if k == "charset" + encoding = begin + Encoding.find v + rescue ArgumentError + Encoding::BINARY + end + end end end end @@ -368,7 +426,7 @@ module Rack def handle_empty_content!(content) if content.nil? || content.empty? - raise EOFError + raise EmptyContentError end end end diff --git a/lib/rack/multipart/uploaded_file.rb b/lib/rack/multipart/uploaded_file.rb index 9eaf691277b1e98f0c2439b5ebe52cfa7bc94e21..2782e44c73afcb64d4a8dc7a0e6259740ee53288 100644 --- a/lib/rack/multipart/uploaded_file.rb +++ b/lib/rack/multipart/uploaded_file.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true +require 'tempfile' +require 'fileutils' + module Rack module Multipart class UploadedFile + # The filename, *not* including the path, of the "uploaded" file attr_reader :original_filename diff --git a/lib/rack/null_logger.rb b/lib/rack/null_logger.rb index 3eff73d683fd336dca8f436b9f3cb7a4a6eee5cd..52fc125c3dc4cf9e21e551cf15f0836d0b5c4dd3 100644 --- a/lib/rack/null_logger.rb +++ b/lib/rack/null_logger.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'constants' + module Rack class NullLogger def initialize(app) @@ -22,6 +24,11 @@ module Rack def warn? ; end def error? ; end def fatal? ; end + def debug! ; end + def error! ; end + def fatal! ; end + def info! ; end + def warn! ; end def level ; end def progname ; end def datetime_format ; end @@ -34,6 +41,8 @@ module Rack def sev_threshold=(sev_threshold); end def close ; end def add(severity, message = nil, progname = nil, &block); end + def log(severity, message = nil, progname = nil, &block); end def <<(msg); end + def reopen(logdev = nil); end end end diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb index 1c3923c32ff794175c165b3cf2cfc578638ca567..9e8434cf3a9ccb743f6974a5b57fda4f8cef8a52 100644 --- a/lib/rack/query_parser.rb +++ b/lib/rack/query_parser.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true +require 'uri' + module Rack class QueryParser - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - - DEFAULT_SEP = /[&;] */n + DEFAULT_SEP = /[&] */n COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n } # ParameterTypeError is the error that is raised when incoming structural @@ -20,29 +20,35 @@ module Rack # nested over the specified limit. class ParamsTooDeepError < RangeError; end - def self.make_default(key_space_limit, param_depth_limit) - new Params, key_space_limit, param_depth_limit + def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit) + unless not_deprecated + warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1) + end + + new Params, param_depth_limit end - attr_reader :key_space_limit, :param_depth_limit + attr_reader :param_depth_limit + + def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit) + unless not_deprecated + warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1) + end - def initialize(params_class, key_space_limit, param_depth_limit) @params_class = params_class - @key_space_limit = key_space_limit @param_depth_limit = param_depth_limit end # Stolen from Mongrel, with some small modifications: - # Parses a query string by breaking it up at the '&' - # and ';' characters. You can also use this to parse - # cookies by changing the characters used in the second - # parameter (which defaults to '&;'). - def parse_query(qs, d = nil, &unescaper) + # Parses a query string by breaking it up at the '&'. You can also use this + # to parse cookies by changing the characters used in the second parameter + # (which defaults to '&'). + def parse_query(qs, separator = nil, &unescaper) unescaper ||= method(:unescape) params = make_params - (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p| + (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p| next if p.empty? k, v = p.split('=', 2).map!(&unescaper) @@ -65,14 +71,14 @@ module Rack # query strings with parameters of conflicting types, in this case a # ParameterTypeError is raised. Users are encouraged to return a 400 in this # case. - def parse_nested_query(qs, d = nil) + def parse_nested_query(qs, separator = nil) params = make_params unless qs.nil? || qs.empty? - (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p| + (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p| k, v = p.split('=', 2).map! { |s| unescape(s) } - normalize_params(params, k, v, param_depth_limit) + _normalize_params(params, k, v, 0) end end @@ -83,58 +89,87 @@ module Rack # normalize_params recursively expands parameters into structural types. If # the structural types represented by two different parameter names are in - # conflict, a ParameterTypeError is raised. - def normalize_params(params, name, v, depth) - raise ParamsTooDeepError if depth <= 0 - - name =~ %r(\A[\[\]]*([^\[\]]+)\]*) - k = $1 || '' - after = $' || '' + # conflict, a ParameterTypeError is raised. The depth argument is deprecated + # and should no longer be used, it is kept for backwards compatibility with + # earlier versions of rack. + def normalize_params(params, name, v, _depth=nil) + _normalize_params(params, name, v, 0) + end - if k.empty? - if !v.nil? && name == "[]" - return Array(v) + private def _normalize_params(params, name, v, depth) + raise ParamsTooDeepError if depth >= param_depth_limit + + if !name + # nil name, treat same as empty string (required by tests) + k = after = '' + elsif depth == 0 + # Start of parsing, don't treat [] or [ at start of string specially + if start = name.index('[', 1) + # Start of parameter nesting, use part before brackets as key + k = name[0, start] + after = name[start, name.length] else - return + # Plain parameter with no nesting + k = name + after = '' end + elsif name.start_with?('[]') + # Array nesting + k = '[]' + after = name[2, name.length] + elsif name.start_with?('[') && (start = name.index(']', 1)) + # Hash nesting, use the part inside brackets as the key + k = name[1, start-1] + after = name[start+1, name.length] + else + # Probably malformed input, nested but not starting with [ + # treat full name as key for backwards compatibility. + k = name + after = '' end + return if k.empty? + if after == '' - params[k] = v + if k == '[]' && depth != 0 + return [v] + else + params[k] = v + end elsif after == "[" params[name] = v 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 - elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$) - child_key = $1 + elsif after.start_with?('[]') + # Recognize x[][y] (hash inside array) parameters + unless after[2] == '[' && after.end_with?(']') && (child_key = after[3, after.length-4]) && !child_key.empty? && !child_key.index('[') && !child_key.index(']') + # Handle other nested array parameters + child_key = after[2, after.length] + end params[k] ||= [] raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key) - normalize_params(params[k].last, child_key, v, depth - 1) + _normalize_params(params[k].last, child_key, v, depth + 1) else - params[k] << normalize_params(make_params, child_key, v, depth - 1) + params[k] << _normalize_params(make_params, child_key, v, depth + 1) end else params[k] ||= make_params raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k]) - params[k] = normalize_params(params[k], after, v, depth - 1) + params[k] = _normalize_params(params[k], after, v, depth + 1) end params end def make_params - @params_class.new @key_space_limit - end - - def new_space_limit(key_space_limit) - self.class.new @params_class, key_space_limit, param_depth_limit + @params_class.new end def new_depth_limit(param_depth_limit) - self.class.new @params_class, key_space_limit, param_depth_limit + self.class.new @params_class, param_depth_limit end private @@ -155,13 +190,12 @@ module Rack true end - def unescape(s) - Utils.unescape(s) + def unescape(string, encoding = Encoding::UTF_8) + URI.decode_www_form_component(string, encoding) end class Params - def initialize(limit) - @limit = limit + def initialize @size = 0 @params = {} end @@ -171,8 +205,6 @@ module Rack end def []=(key, value) - @size += key.size if key && !@params.key?(key) - raise ParamsTooDeepError, 'exceeded available parameter key space' if @size > @limit @params[key] = value end diff --git a/lib/rack/recursive.rb b/lib/rack/recursive.rb index 6971cbfd69dd242ba5b0bb8156029057ffd26488..0945d3227814b952ddf4993590569c84f04ef8c6 100644 --- a/lib/rack/recursive.rb +++ b/lib/rack/recursive.rb @@ -2,6 +2,8 @@ require 'uri' +require_relative 'constants' + module Rack # Rack::ForwardRequest gets caught by Rack::Recursive and redirects # the current request to the app at +url+. diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb index 2f17f50b836099a44ba36b398edd82b60e60ba74..a15064acc498b2b02e29d0399b514f0182912e38 100644 --- a/lib/rack/reloader.rb +++ b/lib/rack/reloader.rb @@ -22,8 +22,6 @@ 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 - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - def initialize(app, cooldown = 10, backend = Stat) @app = app @cooldown = cooldown diff --git a/lib/rack/request.rb b/lib/rack/request.rb index fea984590be4f5f1d0ad047871ccb7f9a8df899b..99a33aa1ec48f74b8a7a1e399ea61ad591919210 100644 --- a/lib/rack/request.rb +++ b/lib/rack/request.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' +require_relative 'media_type' + module Rack # Rack::Request provides a convenient interface to a Rack # environment. It is stateless, the environment +env+ passed to the @@ -10,22 +14,54 @@ module Rack # req.params["data"] class Request - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - 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 + # The priority when checking forwarded headers. The default + # is <tt>[:forwarded, :x_forwarded]</tt>, which means, check the + # +Forwarded+ header first, followed by the appropriate + # <tt>X-Forwarded-*</tt> header. You can revert the priority by + # reversing the priority, or remove checking of either + # or both headers by removing elements from the array. + # + # This should be set as appropriate in your environment + # based on what reverse proxies are in use. If you are not + # using reverse proxies, you should probably use an empty + # array. + attr_accessor :forwarded_priority + + # The priority when checking either the <tt>X-Forwarded-Proto</tt> + # or <tt>X-Forwarded-Scheme</tt> header for the forwarded protocol. + # The default is <tt>[:proto, :scheme]</tt>, to try the + # <tt>X-Forwarded-Proto</tt> header before the + # <tt>X-Forwarded-Scheme</tt> header. Rack 2 had behavior + # similar to <tt>[:scheme, :proto]</tt>. You can remove either or + # both of the entries in array to ignore that respective header. + attr_accessor :x_forwarded_proto_priority end + @forwarded_priority = [:forwarded, :x_forwarded] + @x_forwarded_proto_priority = [:proto, :scheme] + + valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/ + + trusted_proxies = Regexp.union( + /\A127#{valid_ipv4_octet}{3}\z/, # localhost IPv4 range 127.x.x.x, per RFC-3330 + /\A::1\z/, # localhost IPv6 ::1 + /\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i, # private IPv6 range fc00 .. fdff + /\A10#{valid_ipv4_octet}{3}\z/, # private IPv4 range 10.x.x.x + /\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/, # private IPv4 range 172.16.0.0 .. 172.31.255.255 + /\A192\.168#{valid_ipv4_octet}{2}\z/, # private IPv4 range 192.168.x.x + /\Alocalhost\z|\Aunix(\z|:)/i, # localhost hostname, and unix domain sockets + ) + + self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) } + + ALLOWED_SCHEMES = %w(https http wss ws).freeze + def initialize(env) + @env = env @params = nil - super(env) end def params @@ -49,6 +85,8 @@ module Rack def initialize(env) @env = env + # This module is included at least in `ActionDispatch::Request` + # The call to `super()` allows additional mixed-in initializers are called super() end @@ -135,6 +173,8 @@ module Rack # The contents of the host/:authority header sent to the proxy. HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST' + HTTP_FORWARDED = 'HTTP_FORWARDED' + # The value of the scheme sent to the proxy. HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME' @@ -144,7 +184,7 @@ module Rack # The port used to connect to the proxy. HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT' - # Another way for specifing https scheme was used. + # Another way for specifying https scheme was used. HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL' def body; get_header(RACK_INPUT) end @@ -159,7 +199,6 @@ module Rack def content_length; get_header('CONTENT_LENGTH') end def logger; get_header(RACK_LOGGER) end def user_agent; get_header('HTTP_USER_AGENT') end - def multithread?; get_header(RACK_MULTITHREAD) end # the referer of the client def referer; get_header('HTTP_REFERER') end @@ -248,9 +287,7 @@ module Rack end def server_port - if port = get_header(SERVER_PORT) - Integer(port) - end + get_header(SERVER_PORT) end def cookies @@ -307,44 +344,67 @@ module Rack def port if authority = self.authority - _, _, port = split_authority(self.authority) - - if port - return port - end + _, _, port = split_authority(authority) end - if forwarded_port = self.forwarded_port - return forwarded_port.first - end - - if scheme = self.scheme - if port = DEFAULT_PORTS[self.scheme] - return port - end - end - - self.server_port + port || forwarded_port&.last || DEFAULT_PORTS[scheme] || server_port end def forwarded_for - if value = get_header(HTTP_X_FORWARDED_FOR) - split_header(value).map do |authority| - split_authority(wrap_ipv6(authority))[1] + forwarded_priority.each do |type| + case type + when :forwarded + if forwarded_for = get_http_forwarded(:for) + return(forwarded_for.map! do |authority| + split_authority(authority)[1] + end) + end + when :x_forwarded + if value = get_header(HTTP_X_FORWARDED_FOR) + return(split_header(value).map do |authority| + split_authority(wrap_ipv6(authority))[1] + end) + end end end + + nil end def forwarded_port - if value = get_header(HTTP_X_FORWARDED_PORT) - split_header(value).map(&:to_i) + forwarded_priority.each do |type| + case type + when :forwarded + if forwarded = get_http_forwarded(:for) + return(forwarded.map do |authority| + split_authority(authority)[2] + end.compact) + end + when :x_forwarded + if value = get_header(HTTP_X_FORWARDED_PORT) + return split_header(value).map(&:to_i) + end + end end + + nil end def forwarded_authority - if value = get_header(HTTP_X_FORWARDED_HOST) - wrap_ipv6(split_header(value).first) + forwarded_priority.each do |type| + case type + when :forwarded + if forwarded = get_http_forwarded(:host) + return forwarded.last + end + when :x_forwarded + if value = get_header(HTTP_X_FORWARDED_HOST) + return wrap_ipv6(split_header(value).last) + end + end end + + nil end def ssl? @@ -356,17 +416,15 @@ module Rack external_addresses = reject_trusted_ip_addresses(remote_addresses) unless external_addresses.empty? - return external_addresses.first + return external_addresses.last end - if forwarded_for = self.forwarded_for - unless forwarded_for.empty? - # The forwarded for addresses are ordered: client, proxy1, proxy2. - # So we reject all the trusted addresses (proxy*) and return the - # last client. Or if we trust everyone, we just return the first - # address. - return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first - end + if (forwarded_for = self.forwarded_for) && !forwarded_for.empty? + # The forwarded for addresses are ordered: client, proxy1, proxy2. + # So we reject all the trusted addresses (proxy*) and return the + # last client. Or if we trust everyone, we just return the first + # address. + return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first end # If all the addresses are trusted, and we aren't forwarded, just return @@ -402,13 +460,13 @@ module Rack end # Determine whether the request body contains form-data by checking - # the request Content-Type for one of the media-types: + # the request content-type for one of the media-types: # "application/x-www-form-urlencoded" or "multipart/form-data". The # list of form-data media types can be modified through the # +FORM_DATA_MEDIA_TYPES+ array. # # A request body is also assumed to contain form-data when no - # Content-Type header is provided and the request_method is POST. + # content-type header is provided and the request_method is POST. def form_data? type = media_type meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD) @@ -427,7 +485,7 @@ module Rack if get_header(RACK_REQUEST_QUERY_STRING) == query_string get_header(RACK_REQUEST_QUERY_HASH) else - query_hash = parse_query(query_string, '&;') + query_hash = parse_query(query_string, '&') set_header(RACK_REQUEST_QUERY_STRING, query_string) set_header(RACK_REQUEST_QUERY_HASH, query_hash) end @@ -438,27 +496,46 @@ module Rack # This method support both application/x-www-form-urlencoded and # multipart/form-data. def POST - if get_header(RACK_INPUT).nil? - raise "Missing rack.input" - elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT) - get_header(RACK_REQUEST_FORM_HASH) - elsif form_data? || parseable_data? - unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart) - form_vars = get_header(RACK_INPUT).read - - # Fix for Safari Ajax postings that always append \0 - # form_vars.sub!(/\0\z/, '') # performance replacement: - form_vars.slice!(-1) if form_vars.end_with?("\0") - - set_header RACK_REQUEST_FORM_VARS, form_vars - set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&') - - get_header(RACK_INPUT).rewind + if error = get_header(RACK_REQUEST_FORM_ERROR) + raise error.class, error.message, cause: error.cause + end + + begin + rack_input = get_header(RACK_INPUT) + + # If the form hash was already memoized: + if form_hash = get_header(RACK_REQUEST_FORM_HASH) + # And it was memoized from the same input: + if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input) + return form_hash + end end - set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT) - get_header RACK_REQUEST_FORM_HASH - else - {} + + # Otherwise, figure out how to parse the input: + if rack_input.nil? + set_header RACK_REQUEST_FORM_INPUT, nil + set_header(RACK_REQUEST_FORM_HASH, {}) + elsif form_data? || parseable_data? + unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart) + form_vars = get_header(RACK_INPUT).read + + # Fix for Safari Ajax postings that always append \0 + # form_vars.sub!(/\0\z/, '') # performance replacement: + form_vars.slice!(-1) if form_vars.end_with?("\0") + + set_header RACK_REQUEST_FORM_VARS, form_vars + set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&') + end + + set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT) + get_header RACK_REQUEST_FORM_HASH + else + set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT) + set_header(RACK_REQUEST_FORM_HASH, {}) + end + rescue => error + set_header(RACK_REQUEST_FORM_ERROR, error) + raise end end @@ -530,9 +607,7 @@ module Rack # shortcut for <tt>request.params[key]</tt> def [](key) - if $VERBOSE - warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead") - end + warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1) params[key.to_s] end @@ -541,9 +616,7 @@ module Rack # # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params. def []=(key, value) - if $VERBOSE - warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead") - end + warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1) params[key.to_s] = value end @@ -582,6 +655,11 @@ module Rack end end + # Get an array of values set in the RFC 7239 `Forwarded` request header. + def get_http_forwarded(token) + Utils.forwarded_values(get_header(HTTP_FORWARDED))&.[](token) + end + def query_parser Utils.default_query_parser end @@ -598,58 +676,94 @@ module Rack value ? value.strip.split(/[,\s]+/) : [] end - AUTHORITY = /^ - # The host: + # ipv6 extracted from resolv stdlib, simplified + # to remove numbered match group creation. + ipv6 = Regexp.union( + /(?:[0-9A-Fa-f]{1,4}:){7} + [0-9A-Fa-f]{1,4}/x, + /(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? :: + (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?/x, + /(?:[0-9A-Fa-f]{1,4}:){6,6} + \d+\.\d+\.\d+\.\d+/x, + /(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? :: + (?:[0-9A-Fa-f]{1,4}:)* + \d+\.\d+\.\d+\.\d+/x, + /[Ff][Ee]80 + (?::[0-9A-Fa-f]{1,4}){7} + %[-0-9A-Za-z._~]+/x, + /[Ff][Ee]80: + (?: + (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? :: + (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? + | + :(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? + )? + :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+/x) + + AUTHORITY = / + \A (?<host> - # An IPv6 address: - (\[(?<ip6>.*)\]) - | - # An IPv4 address: - (?<ip4>[\d\.]+) + # Match IPv6 as a string of hex digits and colons in square brackets + \[(?<address>#{ipv6})\] | - # A hostname: - (?<name>[a-zA-Z0-9\.\-]+) + # Match any other printable string (except square brackets) as a hostname + (?<address>[[[:graph:]&&[^\[\]]]]*?) ) - # The optional port: (:(?<port>\d+))? - $/x + \z + /x private_constant :AUTHORITY def split_authority(authority) - if match = AUTHORITY.match(authority) - if address = match[:ip6] - return match[:host], address, match[:port]&.to_i - else - return match[:host], match[:host], match[:port]&.to_i - end - end - - # Give up! - return authority, authority, nil + return [] if authority.nil? + return [] unless match = AUTHORITY.match(authority) + return match[:host], match[:address], match[:port]&.to_i end def reject_trusted_ip_addresses(ip_addresses) ip_addresses.reject { |ip| trusted_proxy?(ip) } end + FORWARDED_SCHEME_HEADERS = { + proto: HTTP_X_FORWARDED_PROTO, + scheme: HTTP_X_FORWARDED_SCHEME + }.freeze + private_constant :FORWARDED_SCHEME_HEADERS def forwarded_scheme - allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) || - allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))) + forwarded_priority.each do |type| + case type + when :forwarded + if (forwarded_proto = get_http_forwarded(:proto)) && + (scheme = allowed_scheme(forwarded_proto.last)) + return scheme + end + when :x_forwarded + x_forwarded_proto_priority.each do |x_type| + if header = FORWARDED_SCHEME_HEADERS[x_type] + split_header(get_header(header)).reverse_each do |scheme| + if allowed_scheme(scheme) + return scheme + end + end + end + end + end + end + + nil end def allowed_scheme(header) header if ALLOWED_SCHEMES.include?(header) end - def extract_proto_header(header) - if header - if (comma_index = header.index(',')) - header[0, comma_index] - else - header - end - end + def forwarded_priority + Request.forwarded_priority + end + + def x_forwarded_proto_priority + Request.x_forwarded_proto_priority end end @@ -657,3 +771,7 @@ module Rack include Helpers end end + +# :nocov: +require_relative 'multipart' unless defined?(Rack::Multipart) +# :nocov: diff --git a/lib/rack/response.rb b/lib/rack/response.rb index fd6d2f5d5555064f819b5babf4a3f236a1c23b89..f24683bcb54be6831a413140fd6f00cc6af52223 100644 --- a/lib/rack/response.rb +++ b/lib/rack/response.rb @@ -2,6 +2,11 @@ require 'time' +require_relative 'constants' +require_relative 'utils' +require_relative 'media_type' +require_relative 'headers' + module Rack # Rack::Response provides a convenient interface to create a Rack # response. @@ -26,22 +31,45 @@ module Rack attr_accessor :length, :status, :body attr_reader :headers - # @deprecated Use {#headers} instead. - alias header headers + # Deprecated, use headers instead. + def header + warn 'Rack::Response#header is deprecated and will be removed in Rack 3.1', uplevel: 1 + + headers + end - # Initialize the response object with the specified body, status - # and headers. + # Initialize the response object with the specified +body+, +status+ + # and +headers+. + # + # If the +body+ is +nil+, construct an empty response object with internal + # buffering. + # + # If the +body+ responds to +to_str+, assume it's a string-like object and + # construct a buffered response object containing using that string as the + # initial contents of the buffer. # - # @param body [nil, #each, #to_str] the response body. - # @param status [Integer] the integer status as defined by the - # HTTP protocol RFCs. - # @param headers [#each] a list of key-value header pairs which - # conform to the HTTP protocol RFCs. + # Otherwise it is expected +body+ conforms to the normal requirements of a + # Rack response body, typically implementing one of +each+ (enumerable + # body) or +call+ (streaming body). # - # Providing a body which responds to #to_str is legacy behaviour. + # The +status+ defaults to +200+ which is the "OK" HTTP status code. You + # can provide any other valid status code. + # + # The +headers+ must be a +Hash+ of key-value header pairs which conform to + # the Rack specification for response headers. The key must be a +String+ + # instance and the value can be either a +String+ or +Array+ instance. def initialize(body = nil, status = 200, headers = {}) @status = status.to_i - @headers = Utils::HeaderHash[headers] + + unless headers.is_a?(Hash) + warn "Providing non-hash headers to Rack::Response is deprecated and will be removed in Rack 3.1", uplevel: 1 + end + + @headers = Headers.new + # Convert headers input to a plain hash with lowercase keys. + headers.each do |k, v| + @headers[k] = v + end @writer = self.method(:append) @@ -58,7 +86,7 @@ module Rack @length = body.to_str.bytesize else @body = body - @buffered = false + @buffered = nil # undetermined as of yet. @length = 0 end @@ -74,11 +102,16 @@ module Rack CHUNKED == get_header(TRANSFER_ENCODING) end + def no_entity_body? + # The response body is an enumerable body and it is not allowed to have an entity body. + @body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status] + end + # Generate a response array consistent with the requirements of the SPEC. # @return [Array] a 3-tuple suitable of `[status, headers, body]` # which is suitable to be returned from the middleware `#call(env)` method. def finish(&block) - if STATUS_WITH_NO_ENTITY_BODY[status.to_i] + if no_entity_body? delete_header CONTENT_TYPE delete_header CONTENT_LENGTH close @@ -105,7 +138,7 @@ module Rack end end - # Append to body and update Content-Length. + # Append to body and update content-length. # # NOTE: Do not mix #write and direct #body access! # @@ -123,10 +156,22 @@ module Rack @block == nil && @body.empty? end - def has_header?(key); headers.key? key; end - def get_header(key); headers[key]; end - def set_header(key, v); headers[key] = v; end - def delete_header(key); headers.delete key; end + def has_header?(key) + raise ArgumentError unless key.is_a?(String) + @headers.key?(key) + end + def get_header(key) + raise ArgumentError unless key.is_a?(String) + @headers[key] + end + def set_header(key, value) + raise ArgumentError unless key.is_a?(String) + @headers[key] = value + end + def delete_header(key) + raise ArgumentError unless key.is_a?(String) + @headers.delete key + end alias :[] :get_header alias :[]= :set_header @@ -150,31 +195,43 @@ module Rack def forbidden?; status == 403; end def not_found?; status == 404; end def method_not_allowed?; status == 405; end + def not_acceptable?; status == 406; end + def request_timeout?; status == 408; end def precondition_failed?; status == 412; end def unprocessable?; status == 422; end def redirect?; [301, 302, 303, 307, 308].include? status; end def include?(header) - has_header? header + has_header?(header) end # Add a header that may have multiple values. # # Example: - # response.add_header 'Vary', 'Accept-Encoding' - # response.add_header 'Vary', 'Cookie' + # response.add_header 'vary', 'accept-encoding' + # response.add_header 'vary', 'cookie' # - # assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary') + # assert_equal 'accept-encoding,cookie', response.get_header('vary') # # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - def add_header(key, v) - if v.nil? - get_header key - elsif has_header? key - set_header key, "#{get_header key},#{v}" + def add_header(key, value) + raise ArgumentError unless key.is_a?(String) + + if value.nil? + return get_header(key) + end + + value = value.to_s + + if header = get_header(key) + if header.is_a?(Array) + header << value + else + set_header(key, [header, value]) + end else - set_header key, v + set_header(key, value) end end @@ -202,36 +259,39 @@ module Rack end def location - get_header "Location" + get_header "location" end def location=(location) - set_header "Location", location + set_header "location", location end def set_cookie(key, value) - cookie_header = get_header SET_COOKIE - set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value) + add_header SET_COOKIE, Utils.set_cookie_header(key, value) end def delete_cookie(key, value = {}) - set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value) + set_header(SET_COOKIE, + Utils.delete_set_cookie_header!( + get_header(SET_COOKIE), key, value + ) + ) end def set_cookie_header get_header SET_COOKIE end - def set_cookie_header=(v) - set_header SET_COOKIE, v + def set_cookie_header=(value) + set_header SET_COOKIE, value end def cache_control get_header CACHE_CONTROL end - def cache_control=(v) - set_header CACHE_CONTROL, v + def cache_control=(value) + set_header CACHE_CONTROL, value end # Specifies that the content shouldn't be cached. Overrides `cache!` if already called. @@ -254,34 +314,38 @@ module Rack get_header ETAG end - def etag=(v) - set_header ETAG, v + def etag=(value) + set_header ETAG, value end protected def buffered_body! - return if @buffered - - if @body.is_a?(Array) - # The user supplied body was an array: - @body = @body.compact - @body.each do |part| - @length += part.to_s.bytesize + if @buffered.nil? + if @body.is_a?(Array) + # The user supplied body was an array: + @body = @body.compact + @body.each do |part| + @length += part.to_s.bytesize + end + elsif @body.respond_to?(:each) + # Turn the user supplied body into a buffered array: + body = @body + @body = Array.new + + body.each do |part| + @writer.call(part.to_s) + end + + body.close if body.respond_to?(:close) + + @buffered = true + else + @buffered = false end - 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 - - body.close if body.respond_to?(:close) end - @buffered = true + return @buffered end def append(chunk) @@ -309,10 +373,21 @@ module Rack @headers = headers end - def has_header?(key); headers.key? key; end - def get_header(key); headers[key]; end - def set_header(key, v); headers[key] = v; end - def delete_header(key); headers.delete key; end + def has_header?(key) + headers.key?(key) + end + + def get_header(key) + headers[key] + end + + def set_header(key, value) + headers[key] = value + end + + def delete_header(key) + headers.delete(key) + end end end end diff --git a/lib/rack/rewindable_input.rb b/lib/rack/rewindable_input.rb index 91b9d1eb367e8758477fa570a24b215f3696445f..730c6a2851a17cf2f94db5a51a4cb9706e4b3468 100644 --- a/lib/rack/rewindable_input.rb +++ b/lib/rack/rewindable_input.rb @@ -3,17 +3,29 @@ require 'tempfile' +require_relative 'constants' + module Rack # Class which can make any IO object rewindable, including non-rewindable ones. It does # this by buffering the data into a tempfile, which is rewindable. # - # rack.input is required to be rewindable, so if your input stream IO is non-rewindable - # by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class - # to easily make it rewindable. - # # Don't forget to call #close when you're done. This frees up temporary resources that # RewindableInput uses, though it does *not* close the original IO object. class RewindableInput + # Makes rack.input rewindable, for compatibility with applications and middleware + # designed for earlier versions of Rack (where rack.input was required to be + # rewindable). + class Middleware + def initialize(app) + @app = app + end + + def call(env) + env[RACK_INPUT] = RewindableInput.new(env[RACK_INPUT]) + @app.call(env) + end + end + def initialize(io) @io = io @rewindable_io = nil @@ -40,6 +52,11 @@ module Rack @rewindable_io.rewind end + def size + make_rewindable unless @rewindable_io + @rewindable_io.size + end + # Closes this RewindableInput object without closing the originally # wrapped IO object. Cleans up any temporary resources that this RewindableInput # has created. @@ -66,12 +83,14 @@ module Rack # access it because we have the file handle open. @rewindable_io = Tempfile.new('RackRewindableInput') @rewindable_io.chmod(0000) - @rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding) + @rewindable_io.set_encoding(Encoding::BINARY) @rewindable_io.binmode + # :nocov: if filesystem_has_posix_semantics? raise 'Unlink failed. IO closed.' if @rewindable_io.closed? @unlinked = true end + # :nocov: buffer = "".dup while @io.read(1024 * 4, buffer) diff --git a/lib/rack/runtime.rb b/lib/rack/runtime.rb index d9b2d8ed19982d6fb9be36aaadc062c4dd6a3616..a1bfa696e3fae923385777b6bbb953ec0c205395 100644 --- a/lib/rack/runtime.rb +++ b/lib/rack/runtime.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +require_relative 'utils' + module Rack - # Sets an "X-Runtime" response header, indicating the response + # Sets an "x-runtime" response header, indicating the response # time of the request, in seconds # # You can put it right before the application to see the processing @@ -9,18 +11,17 @@ module Rack # too. class Runtime FORMAT_STRING = "%0.6f" # :nodoc: - HEADER_NAME = "X-Runtime" # :nodoc: + HEADER_NAME = "x-runtime" # :nodoc: def initialize(app, name = nil) @app = app @header_name = HEADER_NAME - @header_name += "-#{name}" if name + @header_name += "-#{name.to_s.downcase}" if name end def call(env) start_time = Utils.clock_time - status, headers, body = @app.call(env) - headers = Utils::HeaderHash[headers] + _, headers, _ = response = @app.call(env) request_time = Utils.clock_time - start_time @@ -28,7 +29,7 @@ module Rack headers[@header_name] = FORMAT_STRING % request_time end - [status, headers, body] + response end end end diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb index 3d5e786ff76850438a4f2f9b2a4d9f17cdb34266..9c6e0c42fa8f91d314c6169d2528729b81716fbb 100644 --- a/lib/rack/sendfile.rb +++ b/lib/rack/sendfile.rb @@ -1,32 +1,36 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'utils' +require_relative 'body_proxy' + module Rack # = Sendfile # # The Sendfile middleware intercepts responses whose body is being - # served from a file and replaces it with a server specific X-Sendfile + # served from a file and replaces it with a server specific x-sendfile # header. The web server is then responsible for writing the file contents # to the client. This can dramatically reduce the amount of work required # by the Ruby backend and takes advantage of the web server's optimized file # delivery code. # # 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 + # respond to +to_path+ and the request must include an x-sendfile-type # 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 + # 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 # # === Nginx # - # Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile + # Nginx supports the x-accel-redirect header. This is similar to x-sendfile # but requires parts of the filesystem to be mapped into a private URL # hierarchy. # # The following example shows the Nginx configuration required to create - # a private "/files/" area, enable X-Accel-Redirect, and pass the special - # X-Sendfile-Type and X-Accel-Mapping headers to the backend: + # a private "/files/" area, enable x-accel-redirect, and pass the special + # x-sendfile-type and x-accel-mapping headers to the backend: # # location ~ /files/(.*) { # internal; @@ -40,14 +44,14 @@ module Rack # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # - # proxy_set_header X-Sendfile-Type X-Accel-Redirect; - # proxy_set_header X-Accel-Mapping /var/www/=/files/; + # proxy_set_header x-sendfile-type x-accel-redirect; + # proxy_set_header x-accel-mapping /var/www/=/files/; # # proxy_pass http://127.0.0.1:8080/; # } # - # Note that the X-Sendfile-Type header must be set exactly as shown above. - # The X-Accel-Mapping header should specify the location on the file system, + # Note that the x-sendfile-type header must be set exactly as shown above. + # The x-accel-mapping header should specify the location on the file system, # followed by an equals sign (=), followed name of the private URL pattern # that it maps to. The middleware performs a simple substitution on the # resulting path. @@ -56,8 +60,8 @@ module Rack # # === lighttpd # - # Lighttpd has supported some variation of the X-Sendfile header for some - # time, although only recent version support X-Sendfile in a reverse proxy + # Lighttpd has supported some variation of the x-sendfile header for some + # time, although only recent version support x-sendfile in a reverse proxy # configuration. # # $HTTP["host"] == "example.com" { @@ -71,7 +75,7 @@ module Rack # # proxy-core.allow-x-sendfile = "enable" # proxy-core.rewrite-request = ( - # "X-Sendfile-Type" => (".*" => "X-Sendfile") + # "x-sendfile-type" => (".*" => "x-sendfile") # ) # } # @@ -79,21 +83,21 @@ module Rack # # === Apache # - # X-Sendfile is supported under Apache 2.x using a separate module: + # x-sendfile is supported under Apache 2.x using a separate module: # # https://tn123.org/mod_xsendfile/ # # Once the module is compiled and installed, you can enable it using # XSendFile config directive: # - # RequestHeader Set X-Sendfile-Type X-Sendfile + # RequestHeader Set x-sendfile-type x-sendfile # ProxyPassReverse / http://localhost:8001/ # XSendFile on # # === Mapping parameter # # The third parameter allows for an overriding extension of the - # X-Accel-Mapping header. Mappings should be provided in tuples of internal to + # x-accel-mapping header. Mappings should be provided in tuples of internal to # external. The internal values may contain regular expression syntax, they # will be matched with case indifference. @@ -107,28 +111,29 @@ module Rack end def call(env) - status, headers, body = @app.call(env) + _, headers, body = response = @app.call(env) + if body.respond_to?(:to_path) case type = variation(env) - when 'X-Accel-Redirect' + when /x-accel-redirect/i path = ::File.expand_path(body.to_path) if url = map_accel_path(env, path) headers[CONTENT_LENGTH] = '0' # '?' must be percent-encoded because it is not query string but a part of path - headers[type] = ::Rack::Utils.escape_path(url).gsub('?', '%3F') + headers[type.downcase] = ::Rack::Utils.escape_path(url).gsub('?', '%3F') obody = body - body = Rack::BodyProxy.new([]) do + response[2] = Rack::BodyProxy.new([]) do obody.close if obody.respond_to?(:close) end else - env[RACK_ERRORS].puts "X-Accel-Mapping header missing" + env[RACK_ERRORS].puts "x-accel-mapping header missing" end - when 'X-Sendfile', 'X-Lighttpd-Send-File' + when /x-sendfile|x-lighttpd-send-file/i path = ::File.expand_path(body.to_path) headers[CONTENT_LENGTH] = '0' - headers[type] = path + headers[type.downcase] = path obody = body - body = Rack::BodyProxy.new([]) do + response[2] = Rack::BodyProxy.new([]) do obody.close if obody.respond_to?(:close) end when '', nil @@ -136,7 +141,7 @@ module Rack env[RACK_ERRORS].puts "Unknown x-sendfile variation: '#{type}'.\n" end end - [status, headers, body] + response end private diff --git a/lib/rack/server.rb b/lib/rack/server.rb deleted file mode 100644 index c1f2f5caa321fc9bc4c06d8f1643c81db11445fb..0000000000000000000000000000000000000000 --- a/lib/rack/server.rb +++ /dev/null @@ -1,466 +0,0 @@ -# frozen_string_literal: true - -require 'optparse' -require 'fileutils' - -module Rack - - class Server - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - - class Options - def parse!(args) - options = {} - opt_parser = OptionParser.new("", 24, ' ') do |opts| - opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]" - - opts.separator "" - opts.separator "Ruby options:" - - lineno = 1 - opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line| - eval line, TOPLEVEL_BINDING, "-e", lineno - lineno += 1 - } - - opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") { - options[:debug] = true - } - opts.on("-w", "--warn", "turn warnings on for your script") { - options[:warn] = true - } - opts.on("-q", "--quiet", "turn off logging") { - options[:quiet] = true - } - - opts.on("-I", "--include PATH", - "specify $LOAD_PATH (may be used more than once)") { |path| - (options[:include] ||= []).concat(path.split(":")) - } - - opts.on("-r", "--require LIBRARY", - "require the library, before executing your script") { |library| - (options[:require] ||= []) << library - } - - opts.separator "" - opts.separator "Rack options:" - 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 - } - - opts.on("-o", "--host HOST", "listen on HOST (default: localhost)") { |host| - options[:Host] = host - } - - opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port| - options[:Port] = port - } - - opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name| - name, value = name.split('=', 2) - value = true if value.nil? - options[name.to_sym] = value - } - - opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e| - options[:environment] = e - } - - opts.on("-D", "--daemonize", "run daemonized in the background") { |d| - options[:daemonize] = d ? true : false - } - - opts.on("-P", "--pid FILE", "file to store PID") { |f| - 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:" - - opts.on_tail("-h", "-?", "--help", "Show this message") do - puts opts - puts handler_opts(options) - - exit - end - - opts.on_tail("--version", "Show version") do - puts "Rack #{Rack.version} (Release: #{Rack.release})" - exit - end - end - - begin - opt_parser.parse! args - rescue OptionParser::InvalidOption => e - warn e.message - abort opt_parser.to_s - end - - options[:config] = args.last if args.last && !args.last.empty? - options - end - - def handler_opts(options) - begin - info = [] - server = Rack::Handler.get(options[:server]) || Rack::Handler.default - if server && server.respond_to?(:valid_options) - info << "" - info << "Server-specific options for #{server.name}:" - - has_options = false - server.valid_options.each do |name, description| - 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 - return "" if !has_options - end - info.join("\n") - rescue NameError, LoadError - return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options" - end - end - end - - # Start a new rack server (like running rackup). This will parse ARGV and - # provide standard ARGV rackup options, defaulting to load 'config.ru'. - # - # Providing an options hash will prevent ARGV parsing and will not include - # any default options. - # - # This method can be used to very easily launch a CGI application, for - # example: - # - # Rack::Server.start( - # :app => lambda do |e| - # [200, {'Content-Type' => 'text/html'}, ['hello world']] - # end, - # :server => 'cgi' - # ) - # - # Further options available here are documented on Rack::Server#initialize - def self.start(options = nil) - new(options).start - end - - attr_writer :options - - # Options may include: - # * :app - # 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 - # this selects the middleware that will be wrapped around - # your application. Default options available are: - # - development: CommonLogger, ShowExceptions, and Lint - # - deployment: CommonLogger - # - none: no extra middleware - # note: when the server is a cgi server, CommonLogger is not included. - # * :server - # choose a specific Rack::Handler, e.g. cgi, fcgi, webrick - # * :daemonize - # if true, the server will daemonize itself (fork, detach, etc) - # * :pid - # path to write a pid file after daemonize - # * :Host - # the host address to bind to (used by supporting Rack::Handler) - # * :Port - # the port to bind to (used by supporting Rack::Handler) - # * :AccessLog - # webrick access log options (or supporting Rack::Handler) - # * :debug - # turn on debug output ($DEBUG = true) - # * :warn - # turn on warnings ($-w = true) - # * :include - # 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 = [] - - if options - @use_default_options = false - @options = options - @app = options[:app] if options[:app] - else - argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV - @use_default_options = true - @options = parse_options(argv) - end - end - - def options - merged_options = @use_default_options ? default_options.merge(@options) : @options - merged_options.reject { |k, v| @ignore_options.include?(k) } - end - - def default_options - environment = ENV['RACK_ENV'] || 'development' - default_host = environment == 'development' ? 'localhost' : '0.0.0.0' - - { - environment: environment, - pid: nil, - Port: 9292, - Host: default_host, - AccessLog: [], - config: "config.ru" - } - end - - def app - @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config - end - - class << self - def logging_middleware - lambda { |server| - /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["deployment"] = [ - [Rack::ContentLength], - logging_middleware, - [Rack::TempfileReaper] - ] - m["development"] = [ - [Rack::ContentLength], - logging_middleware, - [Rack::ShowExceptions], - [Rack::Lint], - [Rack::TempfileReaper] - ] - - m - end - - def middleware - default_middleware_by_environment - end - end - - def middleware - self.class.middleware - end - - def start(&block) - if options[:warn] - $-w = true - end - - if includes = options[:include] - $LOAD_PATH.unshift(*includes) - end - - Array(options[:require]).each do |library| - require library - end - - if options[:debug] - $DEBUG = true - require 'pp' - p options[:server] - pp wrapped_app - pp app - end - - check_pid! if options[:pid] - - # Touch the wrapped app, so that the config.ru is loaded before - # daemonization (i.e. before chdir, etc). - handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do - wrapped_app - end - - daemonize_app if options[:daemonize] - - write_pid if options[:pid] - - trap(:INT) do - if server.respond_to?(:shutdown) - server.shutdown - else - exit - end - end - - server.run(wrapped_app, **options, &block) - end - - def server - @_server ||= Rack::Handler.get(options[:server]) - - unless @_server - @_server = Rack::Handler.default - - # We already speak FastCGI - @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI' - end - - @_server - end - - private - def build_app_and_options_from_config - if !::File.exist? options[:config] - abort "configuration #{options[:config]} not found" - end - - app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) - @options.merge!(options) { |key, old, new| old } - 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 - - def parse_options(args) - # Don't evaluate CGI ISINDEX parameters. - # http://www.meb.uni-bonn.de/docs/cgi/cl.html - args.clear if ENV.include?(REQUEST_METHOD) - - @options = opt_parser.parse!(args) - @options[:config] = ::File.expand_path(options[:config]) - ENV["RACK_ENV"] = options[:environment] - @options - end - - def opt_parser - Options.new - end - - def build_app(app) - middleware[options[:environment]].reverse_each do |middleware| - middleware = middleware.call(self) if middleware.respond_to?(:call) - next unless middleware - klass, *args = middleware - app = klass.new(app, *args) - end - app - end - - def wrapped_app - @wrapped_app ||= build_app app - end - - def daemonize_app - # Cannot be covered as it forks - # :nocov: - Process.daemon - # :nocov: - end - - def write_pid - ::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") } - at_exit { ::FileUtils.rm_f(options[:pid]) } - rescue Errno::EEXIST - check_pid! - retry - end - - def check_pid! - case pidfile_process_status - when :running, :not_owned - $stderr.puts "A server is already running. Check #{options[:pid]}." - exit(1) - when :dead - ::File.delete(options[:pid]) - end - end - - def pidfile_process_status - return :exited unless ::File.exist?(options[:pid]) - - pid = ::File.read(options[:pid]).to_i - return :dead if pid == 0 - - Process.kill(0, pid) - :running - rescue Errno::ESRCH - :dead - rescue Errno::EPERM - :not_owned - end - - end - -end diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb deleted file mode 100644 index 638bd3b3b08eea45c44de5aaca0fbdba4b47f12a..0000000000000000000000000000000000000000 --- a/lib/rack/session/abstract/id.rb +++ /dev/null @@ -1,523 +0,0 @@ -# frozen_string_literal: true - -# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net -# bugrep: Andreas Zehnder - -require_relative '../../../rack' -require 'time' -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 - include Enumerable - attr_writer :id - - Unspecified = Object.new - - def self.find(req) - req.get_header RACK_SESSION - end - - def self.set(req, session) - req.set_header RACK_SESSION, session - end - - def self.set_options(req, options) - req.set_header RACK_SESSION_OPTIONS, options.dup - end - - def initialize(store, req) - @store = store - @req = req - @loaded = false - end - - def id - return @id if @loaded or instance_variable_defined?(:@id) - @id = @store.send(:extract_session_id, @req) - end - - def options - @req.session_options - end - - def each(&block) - load_for_read! - @data.each(&block) - end - - def [](key) - load_for_read! - @data[key.to_s] - end - - def dig(key, *keys) - load_for_read! - @data.dig(key.to_s, *keys) - end - - def fetch(key, default = Unspecified, &block) - load_for_read! - if default == Unspecified - @data.fetch(key.to_s, &block) - else - @data.fetch(key.to_s, default, &block) - end - end - - def has_key?(key) - load_for_read! - @data.has_key?(key.to_s) - end - alias :key? :has_key? - alias :include? :has_key? - - def []=(key, value) - load_for_write! - @data[key.to_s] = value - end - alias :store :[]= - - def clear - load_for_write! - @data.clear - end - - def destroy - clear - @id = @store.send(:delete_session, @req, id, options) - end - - def to_hash - load_for_read! - @data.dup - end - - def update(hash) - load_for_write! - @data.update(stringify_keys(hash)) - end - alias :merge! :update - - def replace(hash) - load_for_write! - @data.replace(stringify_keys(hash)) - end - - def delete(key) - load_for_write! - @data.delete(key.to_s) - end - - def inspect - if loaded? - @data.inspect - else - "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>" - end - end - - def exists? - return @exists if instance_variable_defined?(:@exists) - @data = {} - @exists = @store.send(:session_exists?, @req) - end - - def loaded? - @loaded - end - - def empty? - load_for_read! - @data.empty? - end - - def keys - load_for_read! - @data.keys - end - - def values - load_for_read! - @data.values - end - - private - - def load_for_read! - load! if !loaded? && exists? - end - - def load_for_write! - load! unless loaded? - end - - def load! - @id, session = @store.send(:load_session, @req) - @data = stringify_keys(session) - @loaded = true - end - - def stringify_keys(other) - # Use transform_keys after dropping Ruby 2.4 support - hash = {} - other.to_hash.each do |key, value| - hash[key.to_s] = value - end - hash - end - end - - # ID sets up a basic framework for implementing an id based sessioning - # service. Cookies sent to the client for maintaining sessions will only - # contain an id reference. Only #find_session, #write_session and - # #delete_session are required to be overwritten. - # - # All parameters are optional. - # * :key determines the name of the cookie, by default it is - # 'rack.session' - # * :path, :domain, :expire_after, :secure, and :httponly set the related - # cookie options as by Rack::Response#set_cookie - # * :skip will not a set a cookie in the response nor update the session state - # * :defer will not set a cookie in the response but still update the session - # state if it is used with a backend - # * :renew (implementation dependent) will prompt the generation of a new - # session id, and migration of data to be referenced at the new id. If - # :defer is set, it will be overridden and the cookie will be set. - # * :sidbits sets the number of bits in length that a generated session - # id will be. - # - # These options can be set on a per request basis, at the location of - # <tt>env['rack.session.options']</tt>. Additionally the id of the - # session can be found within the options hash at the key :id. It is - # highly not recommended to change its value. - # - # Is Rack::Utils::Context compatible. - # - # Not included by default; you must require 'rack/session/abstract/id' - # to use. - - 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 - }.freeze - - attr_reader :key, :default_options, :sid_secure - - def initialize(app, options = {}) - @app = app - @default_options = self.class::DEFAULT_OPTIONS.merge(options) - @key = @default_options.delete(:key) - @cookie_only = @default_options.delete(:cookie_only) - @same_site = @default_options.delete(:same_site) - initialize_sid - end - - def call(env) - context(env) - end - - def context(env, app = @app) - req = make_request env - prepare_session(req) - status, headers, body = app.call(req.env) - res = Rack::Response::Raw.new status, headers - commit_session(req, res) - [status, headers, body] - end - - private - - def make_request(env) - Rack::Request.new env - end - - def initialize_sid - @sidbits = @default_options[:sidbits] - @sid_secure = @default_options[:secure_random] - @sid_length = @sidbits / 4 - end - - # Generate a new session id using Ruby #rand. The size of the - # session id is controlled by the :sidbits option. - # Monkey patch this to use custom methods for session id generation. - - def generate_sid(secure = @sid_secure) - if secure - secure.hex(@sid_length) - else - "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1) - end - rescue NotImplementedError - generate_sid(false) - end - - # Sets the lazy session at 'rack.session' and places options and session - # metadata into 'rack.session.options'. - - def prepare_session(req) - session_was = req.get_header RACK_SESSION - session = session_class.new(self, req) - req.set_header RACK_SESSION, session - req.set_header RACK_SESSION_OPTIONS, @default_options.dup - session.merge! session_was if session_was - end - - # Extracts the session id from provided cookies and passes it and the - # environment to #find_session. - - def load_session(req) - sid = current_session_id(req) - sid, session = find_session(req, sid) - [sid, session || {}] - end - - # Extract session id from request object. - - def extract_session_id(request) - sid = request.cookies[@key] - sid ||= request.params[@key] unless @cookie_only - sid - end - - # Returns the current session id from the SessionHash. - - def current_session_id(req) - req.get_header(RACK_SESSION).id - end - - # Check if the session exists or not. - - def session_exists?(req) - value = current_session_id(req) - value && !value.empty? - end - - # Session should be committed if it was loaded, any of specific options like :renew, :drop - # or :expire_after was given and the security permissions match. Skips if skip is given. - - def commit_session?(req, session, options) - if options[:skip] - false - else - has_session = loaded_session?(session) || forced_session_update?(session, options) - has_session && security_matches?(req, options) - end - end - - def loaded_session?(session) - !session.is_a?(session_class) || session.loaded? - end - - def forced_session_update?(session, options) - force_options?(options) && session && !session.empty? - end - - def force_options?(options) - options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any? - end - - def security_matches?(request, options) - return true unless options[:secure] - request.ssl? - end - - # Acquires the session from the environment and the session id from - # the session options and passes them to #write_session. If successful - # and the :defer option is not true, a cookie will be added to the - # response with the session's id. - - def commit_session(req, res) - session = req.get_header RACK_SESSION - options = session.options - - if options[:drop] || options[:renew] - session_id = delete_session(req, session.id || generate_sid, options) - return unless session_id - end - - return unless commit_session?(req, session, options) - - session.send(:load!) unless loaded_session?(session) - session_id ||= session.id - 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.") - elsif options[:defer] and not options[:renew] - req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE - else - cookie = Hash.new - 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] - - if @same_site.respond_to? :call - cookie[:same_site] = @same_site.call(req, res) - else - cookie[:same_site] = @same_site - end - set_cookie(req, res, cookie.merge!(options)) - end - 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. - - def set_cookie(request, res, cookie) - if request.cookies[@key] != cookie[:value] || cookie[:expires] - res.set_cookie_header = - Utils.add_cookie_to_header(res.set_cookie_header, @key, cookie) - end - end - - # Allow subclasses to prepare_session for different Session classes - - def session_class - SessionHash - end - - # All thread safety and session retrieval procedures should occur here. - # Should return [session_id, session]. - # If nil is provided as the session id, generation of a new valid id - # should occur within. - - def find_session(env, sid) - raise '#find_session not implemented.' - end - - # All thread safety and session storage procedures should occur here. - # Must return the session id if the session was saved successfully, or - # false if the session could not be saved. - - def write_session(req, sid, session, options) - raise '#write_session not implemented.' - end - - # All thread safety and session destroy procedures should occur here. - # Should return a new session id or nil if options[:drop] - - def delete_session(req, sid, options) - raise '#delete_session not implemented' - 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 } - unless k.instance_variable_defined?(:"@_rack_warned") - warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE - k.instance_variable_set(:"@_rack_warned", true) - end - super - end - - # All thread safety and session retrieval procedures should occur here. - # Should return [session_id, session]. - # If nil is provided as the session id, generation of a new valid id - # should occur within. - - def find_session(req, sid) - get_session req.env, sid - end - - # All thread safety and session storage procedures should occur here. - # Must return the session id if the session was saved successfully, or - # false if the session could not be saved. - - def write_session(req, sid, session, options) - set_session req.env, sid, session, options - end - - # All thread safety and session destroy procedures should occur here. - # Should return a new session id or nil if options[:drop] - - def delete_session(req, sid, options) - destroy_session req.env, sid, options - end - end - end - end -end diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb deleted file mode 100644 index bb541396f7b0ef9f8b57dee2446a28646a1b9c07..0000000000000000000000000000000000000000 --- a/lib/rack/session/cookie.rb +++ /dev/null @@ -1,203 +0,0 @@ -# frozen_string_literal: true - -require 'openssl' -require 'zlib' -require_relative 'abstract/id' -require 'json' -require 'base64' - -module Rack - - module Session - - # Rack::Session::Cookie provides simple cookie based session management. - # By default, the session is a Ruby Hash stored as base64 encoded marshalled - # data set to :key (default: rack.session). The object that encodes the - # session data is configurable and must respond to +encode+ and +decode+. - # Both methods must take a string and return a string. - # - # When the secret key is set, cookie data is checked for data integrity. - # The old secret key is also accepted and allows graceful secret rotation. - # - # Example: - # - # use Rack::Session::Cookie, :key => 'rack.session', - # :domain => 'foo.com', - # :path => '/', - # :expire_after => 2592000, - # :secret => 'change_me', - # :old_secret => 'also_change_me' - # - # All parameters are optional. - # - # Example of a cookie with no encoding: - # - # Rack::Session::Cookie.new(application, { - # :coder => Rack::Session::Cookie::Identity.new - # }) - # - # Example of a cookie with custom encoding: - # - # Rack::Session::Cookie.new(application, { - # :coder => Class.new { - # def encode(str); str.reverse; end - # def decode(str); str.reverse; end - # }.new - # }) - # - - class Cookie < Abstract::PersistedSecure - # Encode session cookies as Base64 - class Base64 - def encode(str) - ::Base64.strict_encode64(str) - end - - def decode(str) - ::Base64.decode64(str) - end - - # Encode session cookies as Marshaled Base64 data - class Marshal < Base64 - def encode(str) - super(::Marshal.dump(str)) - end - - def decode(str) - return unless str - ::Marshal.load(super(str)) rescue nil - end - end - - # N.B. Unlike other encoding methods, the contained objects must be a - # valid JSON composite type, either a Hash or an Array. - class JSON < Base64 - def encode(obj) - super(::JSON.dump(obj)) - end - - def decode(str) - return unless str - ::JSON.parse(super(str)) rescue nil - end - end - - class ZipJSON < Base64 - def encode(obj) - super(Zlib::Deflate.deflate(::JSON.dump(obj))) - end - - def decode(str) - return unless str - ::JSON.parse(Zlib::Inflate.inflate(super(str))) - rescue - nil - end - end - end - - # Use no encoding for session cookies - class Identity - def encode(str); str; end - def decode(str); str; end - end - - attr_reader :coder - - def initialize(app, options = {}) - @secrets = options.values_at(:secret, :old_secret).compact - @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1) - - warn <<-MSG unless secure?(options) - SECURITY WARNING: No secret option provided to Rack::Session::Cookie. - This poses a security threat. It is strongly recommended that you - provide a secret to prevent exploits that may be possible from crafted - cookies. This will not be supported in future versions of Rack, and - future versions will even invalidate your existing user cookies. - - Called from: #{caller[0]}. - MSG - @coder = options[:coder] ||= Base64::Marshal.new - super(app, options.merge!(cookie_only: true)) - end - - private - - def find_session(req, sid) - data = unpacked_cookie_data(req) - data = persistent_session_id!(data) - [data["session_id"], data] - end - - def extract_session_id(request) - unpacked_cookie_data(request)["session_id"] - end - - def unpacked_cookie_data(request) - request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k| - session_data = request.cookies[@key] - - if @secrets.size > 0 && session_data - session_data, _, digest = session_data.rpartition('--') - session_data = nil unless digest_match?(session_data, digest) - end - - request.set_header(k, coder.decode(session_data) || {}) - end - end - - 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) - - if @secrets.first - session_data << "--#{generate_hmac(session_data, @secrets.first)}" - end - - if session_data.size > (4096 - @key.size) - req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.") - nil - else - SessionId.new(session_id, session_data) - end - end - - def delete_session(req, session_id, options) - # Nothing to do here, data is in the client - generate_sid unless options[:drop] - end - - def digest_match?(data, digest) - return unless data && digest - @secrets.any? do |secret| - Rack::Utils.secure_compare(digest, generate_hmac(data, secret)) - end - end - - def generate_hmac(data, secret) - OpenSSL::HMAC.hexdigest(@hmac.new, secret, data) - end - - def secure?(options) - @secrets.size >= 1 || - (options[:coder] && options[:let_coder_handle_secure_encoding]) - end - - end - end -end diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb deleted file mode 100644 index 6a601174075b257a061b754b4b12748efbda32d5..0000000000000000000000000000000000000000 --- a/lib/rack/session/memcache.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require 'rack/session/dalli' - -module Rack - module Session - 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 deleted file mode 100644 index 4885605f5d294ea867164758c206c00cccce592c..0000000000000000000000000000000000000000 --- a/lib/rack/session/pool.rb +++ /dev/null @@ -1,85 +0,0 @@ -# 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 -# sergio, threadiness and bugreps - -require_relative 'abstract/id' -require 'thread' - -module Rack - module Session - # Rack::Session::Pool provides simple cookie based session management. - # Session data is stored in a hash held by @pool. - # In the context of a multithreaded environment, sessions being - # committed to the pool is done in a merging manner. - # - # The :drop option is available in rack.session.options if you wish to - # explicitly remove the session from the session cache. - # - # Example: - # myapp = MyRackApp.new - # sessioned = Rack::Session::Pool.new(myapp, - # :domain => 'foo.com', - # :expire_after => 2592000 - # ) - # Rack::Handler::WEBrick.run sessioned - - class Pool < Abstract::PersistedSecure - attr_reader :mutex, :pool - DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge drop: false - - def initialize(app, options = {}) - super - @pool = Hash.new - @mutex = Mutex.new - end - - def generate_sid - loop do - sid = super - break sid unless @pool.key? sid.private_id - end - end - - def find_session(req, sid) - with_lock(req) do - unless sid and session = get_session_with_fallback(sid) - sid, session = generate_sid, {} - @pool.store sid.private_id, session - end - [sid, session] - end - end - - def write_session(req, session_id, new_session, options) - with_lock(req) do - @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.public_id) - @pool.delete(session_id.private_id) - generate_sid unless options[:drop] - end - end - - def with_lock(req) - @mutex.lock if req.multithread? - yield - 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 07e60388069f7926eb98c91e310217b5acaa0fb9..ca090a5048a27b759590a37e123a987cbbf87a29 100644 --- a/lib/rack/show_exceptions.rb +++ b/lib/rack/show_exceptions.rb @@ -3,6 +3,10 @@ require 'ostruct' require 'erb' +require_relative 'constants' +require_relative 'utils' +require_relative 'request' + module Rack # Rack::ShowExceptions catches all exceptions raised from the app it # wraps. It shows a useful backtrace with the sourcefile and @@ -55,7 +59,12 @@ module Rack private :accepts_html? def dump_exception(exception) - string = "#{exception.class}: #{exception.message}\n".dup + if exception.respond_to?(:detailed_message) + message = exception.detailed_message(highlight: false) + else + message = exception.message + end + string = "#{exception.class}: #{message}\n".dup string << exception.backtrace.map { |l| "\t#{l}" }.join("\n") string end @@ -159,7 +168,7 @@ module Rack div.commands { margin-left: 40px; } div.commands a { color:black; text-decoration:none; } #summary { background: #ffc; } - #summary h2 { font-weight: normal; color: #666; } + #summary h2 { font-family: monospace; font-weight: normal; color: #666; white-space: pre-wrap; } #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; } #summary ul#quicklinks li { float: left; padding: 0 1em; } #summary ul#quicklinks>li+li { border-left: 1px #666 solid; } @@ -227,7 +236,11 @@ module Rack <div id="summary"> <h1><%=h exception.class %> at <%=h path %></h1> + <% if exception.respond_to?(:detailed_message) %> + <h2><%=h exception.detailed_message(highlight: false) %></h2> + <% else %> <h2><%=h exception.message %></h2> + <% end %> <table><tr> <th>Ruby</th> <td> diff --git a/lib/rack/show_status.rb b/lib/rack/show_status.rb index a99bdaf33aa7e5ec388444f50e90ba32aaf0f71a..b6f75a016e9682dca1a3fdfe4d8a06b9e075fbd5 100644 --- a/lib/rack/show_status.rb +++ b/lib/rack/show_status.rb @@ -2,6 +2,11 @@ require 'erb' +require_relative 'constants' +require_relative 'utils' +require_relative 'request' +require_relative 'body_proxy' + module Rack # Rack::ShowStatus catches all empty responses and replaces them # with a site explaining the error. @@ -17,8 +22,7 @@ module Rack end def call(env) - status, headers, body = @app.call(env) - headers = Utils::HeaderHash[headers] + status, headers, body = response = @app.call(env) empty = headers[CONTENT_LENGTH].to_i <= 0 # client or server error, or explicit message @@ -33,12 +37,18 @@ module Rack # Yes, it is dumb, but I don't like Ruby yelling at me. detail = detail = env[RACK_SHOWSTATUS_DETAIL] || message - body = @template.result(binding) - size = body.bytesize - [status, headers.merge(CONTENT_TYPE => "text/html", CONTENT_LENGTH => size.to_s), [body]] - else - [status, headers, body] + html = @template.result(binding) + size = html.bytesize + + response[2] = Rack::BodyProxy.new([html]) do + body.close if body.respond_to?(:close) + end + + headers[CONTENT_TYPE] = "text/html" + headers[CONTENT_LENGTH] = size.to_s end + + response end def h(obj) # :nodoc: diff --git a/lib/rack/static.rb b/lib/rack/static.rb index 8cb58b2fd7342fe530ff7e4508cf132fb2abdf00..5c9b6760ffce346dac17d04ff9695dfcbeea67f4 100644 --- a/lib/rack/static.rb +++ b/lib/rack/static.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'files' +require_relative 'mime' + module Rack # The Rack::Static middleware intercepts requests for static files @@ -78,16 +82,14 @@ module Rack # :header_rules => [ # # Cache all static files in public caches (e.g. Rack::Cache) # # as well as in the browser - # [:all, {'Cache-Control' => 'public, max-age=31536000'}], + # [:all, {'cache-control' => 'public, max-age=31536000'}], # # # Provide web fonts with cross-origin access-control-headers # # Firefox requires this when serving assets using a Content Delivery Network - # [:fonts, {'Access-Control-Allow-Origin' => '*'}] + # [:fonts, {'access-control-allow-origin' => '*'}] # ] # class Static - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - def initialize(app, options = {}) @app = app @urls = options[:urls] || ["/favicon.ico"] @@ -137,10 +139,8 @@ module Rack elsif response[0] == 304 # Do nothing, leave headers as is else - if mime_type = Mime.mime_type(::File.extname(path), 'text/plain') - response[1][CONTENT_TYPE] = mime_type - end - response[1]['Content-Encoding'] = 'gzip' + response[1][CONTENT_TYPE] = Mime.mime_type(::File.extname(path), 'text/plain') + response[1]['content-encoding'] = 'gzip' end end diff --git a/lib/rack/tempfile_reaper.rb b/lib/rack/tempfile_reaper.rb index 9b04fefc2441402ea1862a1780bc7611ffd27fa7..0b94cc73a157b52de72fb9706079c3c2947d972b 100644 --- a/lib/rack/tempfile_reaper.rb +++ b/lib/rack/tempfile_reaper.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require_relative 'constants' +require_relative 'body_proxy' + module Rack # Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart) @@ -12,11 +15,19 @@ module Rack def call(env) env[RACK_TEMPFILES] ||= [] - status, headers, body = @app.call(env) - body_proxy = BodyProxy.new(body) do - env[RACK_TEMPFILES].each(&:close!) unless env[RACK_TEMPFILES].nil? + + begin + _, _, body = response = @app.call(env) + rescue Exception + env[RACK_TEMPFILES]&.each(&:close!) + raise end - [status, headers, body_proxy] + + response[2] = BodyProxy.new(body) do + env[RACK_TEMPFILES]&.each(&:close!) + end + + response end end end diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb index 8462f92067d4c3397378cff6766d21b2ac5a9123..99c4d82365a298bfe709b8dab0f8edb2e7756ddc 100644 --- a/lib/rack/urlmap.rb +++ b/lib/rack/urlmap.rb @@ -2,6 +2,8 @@ require 'set' +require_relative 'constants' + 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 @@ -74,7 +76,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 c8e61ea1806c615bfdad47f0f7c14916d6b60694..99d696dbf47c707c2d69acf54240f4bd51d5a00e 100644 --- a/lib/rack/utils.rb +++ b/lib/rack/utils.rb @@ -8,29 +8,29 @@ require 'tempfile' require 'time' require_relative 'query_parser' +require_relative 'mime' +require_relative 'headers' +require_relative 'constants' module Rack # Rack::Utils contains a grab-bag of useful methods for writing web # applications adopted from all kinds of Ruby libraries. module Utils - (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4' - ParameterTypeError = QueryParser::ParameterTypeError InvalidParameterError = QueryParser::InvalidParameterError + ParamsTooDeepError = QueryParser::ParamsTooDeepError DEFAULT_SEP = QueryParser::DEFAULT_SEP COMMON_SEP = QueryParser::COMMON_SEP KeySpaceConstrainedParams = QueryParser::Params - RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ] - RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] - class << self attr_accessor :default_query_parser end - # The default number of bytes to allow parameter keys to take up. - # This helps prevent a rogue client from flooding a Request. - self.default_query_parser = QueryParser.make_default(65536, 100) + # The default amount of nesting to allowed by hash parameters. + # This helps prevent a rogue client from triggering a possible stack overflow + # when parsing parameters. + self.default_query_parser = QueryParser.make_default(32) module_function @@ -86,11 +86,12 @@ module Rack end def self.key_space_limit - default_query_parser.key_space_limit + warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1) + 65536 end def self.key_space_limit=(v) - self.default_query_parser = self.default_query_parser.new_space_limit(v) + warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1) end if defined?(Process::CLOCK_MONOTONIC) @@ -131,13 +132,13 @@ module Rack }.join("&") when Hash value.map { |k, v| - build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) + build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k) }.delete_if(&:empty?).join('&') when nil - prefix + escape(prefix) else raise ArgumentError, "value must be a Hash" if prefix.nil? - "#{prefix}=#{escape(value)}" + "#{escape(prefix)}=#{escape(value)}" end end @@ -152,6 +153,19 @@ module Rack end end + def forwarded_values(forwarded_header) + return nil unless forwarded_header + forwarded_header = forwarded_header.to_s.gsub("\n", ";") + + forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values| + field.split(/\s*,\s*/).each do |pair| + return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i + (values[$1.downcase.to_sym] ||= []) << $2 + end + end + end + module_function :forwarded_values + # Return best accept value to use, based on the algorithm # in RFC 2616 Section 14. If there are multiple best # matches (same specificity and quality), the value returned @@ -166,7 +180,7 @@ module Rack end.compact.sort_by do |match, quality| (match.split('/', 2).count('*') * -10) + quality end.last - matches && matches.first + matches&.first end ESCAPE_HTML = { @@ -217,17 +231,20 @@ module Rack (encoding_candidates & available_encodings)[0] end - def parse_cookies(env) - parse_cookies_header env[HTTP_COOKIE] - end + # :call-seq: + # parse_cookies_header(value) -> hash + # + # Parse cookies from the provided header +value+ according to RFC6265. The + # syntax for cookie headers only supports semicolons. Returns a map of + # cookie +key+ to cookie +value+. + # + # parse_cookies_header('myname=myvalue; max-age=0') + # # => {"myname"=>"myvalue", "max-age"=>"0"} + # + def parse_cookies_header(value) + return {} unless value - def parse_cookies_header(header) - # According to RFC 6265: - # The syntax for cookie headers only supports semicolons - # User Agent -> Server == - # Cookie: SID=31d4d96e407aad42; lang=en-US - return {} unless header - header.split(/[;] */n).each_with_object({}) do |cookie, cookies| + value.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) @@ -235,14 +252,66 @@ module Rack end def add_cookie_to_header(header, key, value) + warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1) + + case header + when nil, '' + return set_cookie_header(key, value) + when String + [header, set_cookie_header(key, value)] + when Array + header + [set_cookie_header(key, value)] + else + raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}" + end + end + + # :call-seq: + # parse_cookies(env) -> hash + # + # Parse cookies from the provided request environment using + # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+. + # + # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'}) + # # => {'myname' => 'myvalue'} + # + def parse_cookies(env) + parse_cookies_header env[HTTP_COOKIE] + end + + # :call-seq: + # set_cookie_header(key, value) -> encoded string + # + # Generate an encoded string using the provided +key+ and +value+ suitable + # for the +set-cookie+ header according to RFC6265. The +value+ may be an + # instance of either +String+ or +Hash+. + # + # If the cookie +value+ is an instance of +Hash+, it considers the following + # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance + # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more + # details about the interpretation of these fields, consult + # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2). + # + # An extra cookie attribute +escape_key+ can be provided to control whether + # or not the cookie key is URL encoded. If explicitly set to +false+, the + # cookie key name will not be url encoded (escaped). The default is +true+. + # + # set_cookie_header("myname", "myvalue") + # # => "myname=myvalue" + # + # set_cookie_header("myname", {value: "myvalue", max_age: 10}) + # # => "myname=myvalue; max-age=10" + # + def set_cookie_header(key, value) case value when Hash + key = escape(key) unless value[:escape_key] == false 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] expires = "; expires=#{value[:expires].httpdate}" if value[:expires] secure = "; secure" if value[:secure] - httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only]) + httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only]) same_site = case value[:same_site] when false, nil @@ -257,100 +326,109 @@ module Rack raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}" end value = value[:value] + else + key = escape(key) end + value = [value] unless Array === value - cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \ + return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}" + end - case header - when nil, '' - cookie - when String - [header, cookie].join("\n") - when Array - (header + [cookie]).join("\n") + # :call-seq: + # set_cookie_header!(headers, key, value) -> header value + # + # Append a cookie in the specified headers with the given cookie +key+ and + # +value+ using set_cookie_header. + # + # If the headers already contains a +set-cookie+ key, it will be converted + # to an +Array+ if not already, and appended to. + def set_cookie_header!(headers, key, value) + if header = headers[SET_COOKIE] + if header.is_a?(Array) + header << set_cookie_header(key, value) + else + headers[SET_COOKIE] = [header, set_cookie_header(key, value)] + end else - raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}" + headers[SET_COOKIE] = set_cookie_header(key, value) end end - def set_cookie_header!(header, key, value) - header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value) - nil + # :call-seq: + # delete_set_cookie_header(key, value = {}) -> encoded string + # + # Generate an encoded string based on the given +key+ and +value+ using + # set_cookie_header for the purpose of causing the specified cookie to be + # deleted. The +value+ may be an instance of +Hash+ and can include + # attributes as outlined by set_cookie_header. The encoded cookie will have + # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty + # +value+. When used with the +set-cookie+ header, it will cause the client + # to *remove* any matching cookie. + # + # delete_set_cookie_header("myname") + # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + # + def delete_set_cookie_header(key, value = {}) + set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: '')) end def make_delete_cookie_header(header, key, value) - case header - when nil, '' - cookies = [] - when String - cookies = header.split("\n") - when Array - cookies = header - end - - key = escape(key) - domain = value[:domain] - path = value[:path] - regexp = if domain - if path - /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/ - else - /\A#{key}=.*domain=#{domain}(?:;|$)/ - end - elsif path - /\A#{key}=.*path=#{path}(?:;|$)/ - else - /\A#{key}=/ - end - - cookies.reject! { |cookie| regexp.match? cookie } + warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1) - cookies.join("\n") + delete_set_cookie_header!(header, key, value) end - def delete_cookie_header!(header, key, value = {}) - header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value) - nil + def delete_cookie_header!(headers, key, value = {}) + headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value) + + return nil end - # Adds a cookie that will *remove* a cookie from the client. Hence the - # strange method name. def add_remove_cookie_to_header(header, key, value = {}) - new_header = make_delete_cookie_header(header, key, value) + warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1) - add_cookie_to_header(new_header, key, - { value: '', path: nil, domain: nil, - max_age: '0', - expires: Time.at(0) }.merge(value)) + delete_set_cookie_header!(header, key, value) + end + # :call-seq: + # delete_set_cookie_header!(header, key, value = {}) -> header value + # + # Set an expired cookie in the specified headers with the given cookie + # +key+ and +value+ using delete_set_cookie_header. This causes + # the client to immediately delete the specified cookie. + # + # delete_set_cookie_header!(nil, "mycookie") + # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + # + # If the header is non-nil, it will be modified in place. + # + # header = [] + # delete_set_cookie_header!(header, "mycookie") + # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"] + # header + # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"] + # + def delete_set_cookie_header!(header, key, value = {}) + if header + header = Array(header) + header << delete_set_cookie_header(key, value) + else + header = delete_set_cookie_header(key, value) + end + + return header end def rfc2822(time) time.rfc2822 end - # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead - # of '% %b %Y'. - # It assumes that the time is in GMT to comply to the RFC 2109. - # - # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough - # that I'm certain someone implemented only that option. - # Do not use %a and %b from Time.strptime, it would use localized names for - # weekday and month. - # - def rfc2109(time) - wday = RFC2822_DAY_NAME[time.wday] - mon = RFC2822_MONTH_NAME[time.mon - 1] - time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT") - end - # Parses the "Range:" header, if present, into an array of Range objects. # Returns nil if the header is missing or syntactically invalid. # Returns an empty array if none of the ranges are satisfiable. def byte_ranges(env, size) - warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE get_byte_ranges env['HTTP_RANGE'], size end @@ -383,20 +461,30 @@ module Rack ranges end - # Constant time string comparison. - # - # NOTE: the values compared should be of fixed length, such as strings - # that have already been processed by HMAC. This should not be used - # on variable length plaintext strings because it could leak length info - # via timing attacks. - def secure_compare(a, b) - return false unless a.bytesize == b.bytesize + # :nocov: + if defined?(OpenSSL.fixed_length_secure_compare) + # Constant time string comparison. + # + # NOTE: the values compared should be of fixed length, such as strings + # that have already been processed by HMAC. This should not be used + # on variable length plaintext strings because it could leak length info + # via timing attacks. + def secure_compare(a, b) + return false unless a.bytesize == b.bytesize + + OpenSSL.fixed_length_secure_compare(a, b) + end + # :nocov: + else + def secure_compare(a, b) + return false unless a.bytesize == b.bytesize - l = a.unpack("C*") + l = a.unpack("C*") - r, i = 0, -1 - b.each_byte { |v| r |= v ^ l[i += 1] } - r == 0 + r, i = 0, -1 + b.each_byte { |v| r |= v ^ l[i += 1] } + r == 0 + end end # Context allows the use of a compatible middleware at different points @@ -425,94 +513,32 @@ module Rack end end - # A case-insensitive Hash that preserves the original case of a + # A wrapper around Headers # header when set. # # @api private class HeaderHash < Hash # :nodoc: def self.[](headers) - if headers.is_a?(HeaderHash) && !headers.frozen? + warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1 + if headers.is_a?(Headers) && !headers.frozen? return headers - else - return self.new(headers) end - end - def initialize(hash = {}) - super() - @names = {} - hash.each { |k, v| self[k] = v } + new_headers = Headers.new + headers.each{|k,v| new_headers[k] = v} + new_headers end - # on dup/clone, we need to duplicate @names hash - def initialize_copy(other) - super - @names = other.names.dup + def self.new(hash = {}) + warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1 + headers = Headers.new + hash.each{|k,v| headers[k] = v} + headers end - # on clear, we need to clear @names hash - def clear - super - @names.clear + def self.allocate + raise TypeError, "cannot allocate HeaderHash" end - - def each - super do |k, v| - yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v) - end - end - - def to_hash - hash = {} - each { |k, v| hash[k] = v } - hash - end - - def [](k) - super(k) || super(@names[k.downcase]) - end - - def []=(k, v) - canonical = k.downcase.freeze - delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary - @names[canonical] = k - super k, v - end - - def delete(k) - canonical = k.downcase - result = super @names.delete(canonical) - result - end - - def include?(k) - super || @names.include?(k.downcase) - end - - alias_method :has_key?, :include? - alias_method :member?, :include? - alias_method :key?, :include? - - def merge!(other) - other.each { |k, v| self[k] = v } - self - end - - def merge(other) - hash = dup - hash.merge! other - end - - def replace(other) - clear - other.each { |k, v| self[k] = v } - self - end - - protected - def names - @names - end end # Every standard HTTP code mapped to the appropriate message. diff --git a/lib/rack/version.rb b/lib/rack/version.rb index d451de434c390e2dca840332535bc6aa5dd9e4b6..e634f23aecee4cef06bcd755e33f28cb1c1c163c 100644 --- a/lib/rack/version.rb +++ b/lib/rack/version.rb @@ -13,14 +13,19 @@ module Rack # The Rack protocol version number implemented. - VERSION = [1, 3] + VERSION = [1, 3].freeze + deprecate_constant :VERSION - # Return the Rack protocol version as a dotted string. + VERSION_STRING = "1.3".freeze + deprecate_constant :VERSION_STRING + + # The Rack protocol version number implemented. def self.version - VERSION.join(".") + warn "Rack.version is deprecated and will be removed in Rack 3.1!", uplevel: 1 + VERSION end - RELEASE = "2.2.6.4" + RELEASE = "3.0.8" # Return the Rack release as a dotted string. def self.release diff --git a/rack.gemspec b/rack.gemspec index 246ed7c639ba65a21e1ad70620a7e12ab2f49d2d..743804ebf23066ae59cea395338b8d02d7270cf9 100644 --- a/rack.gemspec +++ b/rack.gemspec @@ -17,30 +17,26 @@ Gem::Specification.new do |s| middleware) into a single method call. EOF - s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*}'] + - %w(MIT-LICENSE rack.gemspec Rakefile README.rdoc SPEC.rdoc) - - s.bindir = 'bin' - s.executables << 'rackup' - s.require_path = 'lib' - s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.md', 'CONTRIBUTING.md'] + s.files = Dir['lib/**/*'] + %w(MIT-LICENSE README.md SPEC.rdoc) + s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'CONTRIBUTING.md'] s.author = 'Leah Neukirchen' s.email = 'leah@vuxu.org' s.homepage = 'https://github.com/rack/rack' - s.required_ruby_version = '>= 2.3.0' + s.required_ruby_version = '>= 2.4.0' s.metadata = { "bug_tracker_uri" => "https://github.com/rack/rack/issues", - "changelog_uri" => "https://github.com/rack/rack/blob/master/CHANGELOG.md", + "changelog_uri" => "https://github.com/rack/rack/blob/main/CHANGELOG.md", "documentation_uri" => "https://rubydoc.info/github/rack/rack", "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 'bundler' s.add_development_dependency 'rake' end diff --git a/test/builder/an_underscore_app.rb b/test/builder/an_underscore_app.rb index f58a2be509f0e422202dfd35fcbb0e120a730aa1..4f6e87e50349a03914e95ef75b9d7ae16672fa7d 100644 --- a/test/builder/an_underscore_app.rb +++ b/test/builder/an_underscore_app.rb @@ -2,6 +2,6 @@ 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/bom.ru b/test/builder/bom.ru index 5740f9a13826a4865da00ec40dcefd548d617caf..6f491f0f7fa3aaec4dbb5db1bbcb690ec38d9f71 100644 --- a/test/builder/bom.ru +++ b/test/builder/bom.ru @@ -1 +1 @@ -run -> (env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] } +run -> (env) { [200, { 'content-type' => 'text/plain' }, ['OK']] } diff --git a/test/builder/comment.ru b/test/builder/comment.ru index 894ba5d017927d96c19dcbeb82aa70711e82cce0..7d03db8bfaea2d922e37617aca9c72cfdca8d2e2 100644 --- a/test/builder/comment.ru +++ b/test/builder/comment.ru @@ -3,4 +3,4 @@ =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 dd8d45a9255e0c59bcdb45065cec4e2a6ceb282c..f1dcf5660b619f1003786a2d9f464538769b5160 100644 --- a/test/builder/end.ru +++ b/test/builder/end.ru @@ -1,6 +1,6 @@ # frozen_string_literal: true -run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } +run lambda { |env| [200, { 'content-type' => 'text/plain' }, ['OK']] } __END__ Should not be evaluated Neither should diff --git a/test/builder/frozen.ru b/test/builder/frozen.ru index 5bad750f4f4250fc5209dfe922525f225df8e03d..71ccc54d7f0e7527d5b020fa3d48e2f769d906b3 100644 --- a/test/builder/frozen.ru +++ b/test/builder/frozen.ru @@ -3,5 +3,5 @@ run lambda { |env| body = 'frozen' raise "Not frozen!" unless body.frozen? - [200, { 'Content-Type' => 'text/plain' }, [body]] + [200, { 'content-type' => 'text/plain' }, [body]] } diff --git a/test/builder/line.ru b/test/builder/line.ru index 9ad88986087ce02ab6776dc9dc49d9e5d5323bbe..03a8e017c5b1ff8d50157cee0b3d47081c491cf8 100644 --- a/test/builder/line.ru +++ b/test/builder/line.ru @@ -1,3 +1,3 @@ # frozen_string_literal: true -run lambda{ |env| [200, { 'Content-Type' => 'text/plain' }, [__LINE__.to_s]] } +run lambda{ |env| [200, { 'content-type' => 'text/plain' }, [__LINE__.to_s]] } diff --git a/test/builder/options.ru b/test/builder/options.ru index dca48fd9190c5dd55d895322d72858f6771446e7..5b3b42b6b7175ac5cd5643a78ceb82f189d916da 100644 --- a/test/builder/options.ru +++ b/test/builder/options.ru @@ -1,4 +1,4 @@ # frozen_string_literal: true #\ -d -p 2929 --env test -run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } +run lambda { |env| [200, { 'content-type' => 'text/plain' }, ['OK']] } diff --git a/test/cgi/rackup_stub.rb b/test/cgi/rackup_stub.rb old mode 100755 new mode 100644 diff --git a/test/cgi/sample_rackup.ru b/test/cgi/sample_rackup.ru old mode 100755 new mode 100644 index c8e94c9f1527d859870b095ec22ab7974d3fe42c..ec154da3a078eda35e7be2c6920c92e125a1be5c --- a/test/cgi/sample_rackup.ru +++ b/test/cgi/sample_rackup.ru @@ -1,5 +1,5 @@ # frozen_string_literal: true -require '../testrequest' +require '../test_request' run Rack::Lint.new(TestRequest.new) diff --git a/test/cgi/test b/test/cgi/test old mode 100755 new mode 100644 index a1de2fbe39a84cb7800adc392ffcfbacbe2d0658..546ad906194fbc9e43921e4b690f4b2db7bc54dd --- a/test/cgi/test +++ b/test/cgi/test @@ -1,9 +1,5 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -$: << File.join(File.dirname(__FILE__), "..", "..", "lib") - -require 'rack' -require '../testrequest' - -Rack::Handler::CGI.run(Rack::Lint.new(TestRequest.new)) +***** DO NOT MODIFY THIS FILE! ***** +If you modify this file, tests will break!!! +The quick brown fox jumps over the ruby dog. +The quick brown fox jumps over the lazy dog. +***** DO NOT MODIFY THIS FILE! ***** diff --git a/test/cgi/test.ru b/test/cgi/test.ru old mode 100755 new mode 100644 index 1263778df2d458483d78060efe49e4f9fe151bd3..d13f288fbda950c1d8f26a2b1fea96efa6af423d --- a/test/cgi/test.ru +++ b/test/cgi/test.ru @@ -1,5 +1,5 @@ #!../../bin/rackup # frozen_string_literal: true -require '../testrequest' +require '../test_request' run Rack::Lint.new(TestRequest.new) diff --git a/test/helper.rb b/test/helper.rb index 55799c8c65b4b92f868480450b2a3a462258a9c3..a2f569c39a1f83ab75ebd055204164d479397369 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,21 +1,41 @@ # frozen_string_literal: true if ENV.delete('COVERAGE') - require 'coverage' require 'simplecov' - def SimpleCov.rack_coverage(**opts) - start do - add_filter "/test/" - add_filter "/lib/rack/handler" - add_group('Missing'){|src| src.covered_percent < 100} - add_group('Covered'){|src| src.covered_percent == 100} - end + SimpleCov.start do + enable_coverage :branch + add_filter "/test/" + add_filter "/lib/rack/handler" + add_group('Missing'){|src| src.covered_percent < 100} + add_group('Covered'){|src| src.covered_percent == 100} + end +end + +if ENV['SEPARATE'] + def self.separate_testing + yield + end +else + $:.unshift(File.expand_path('../lib', __dir__)) + require_relative '../lib/rack' + + def self.separate_testing end - SimpleCov.rack_coverage end -$:.unshift(File.expand_path('../lib', __dir__)) -require_relative '../lib/rack' require 'minitest/global_expectations/autorun' require 'stringio' + +class Minitest::Spec + def self.deprecated(*args, &block) + it(*args) do + begin + verbose, $VERBOSE = $VERBOSE, nil + instance_exec(&block) + ensure + $VERBOSE = verbose + end + end + end +end diff --git a/test/load/rack-test-a.rb b/test/load/rack-test-a.rb deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/test/load/rack-test-b.rb b/test/load/rack-test-b.rb deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/test/multipart/bad_robots b/test/multipart/bad_robots index 7e5bd418f6602ae7a5c205270d4a5be5e98cea3b..c4b8258f024d1d7ceff0bae2264bbc6b72f32644 100644 --- a/test/multipart/bad_robots +++ b/test/multipart/bad_robots @@ -1,5 +1,5 @@ --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="bbbbbbbbbbbbbbb" +content-disposition: form-data; name="bbbbbbbbbbbbbbb" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @@ -208,52 +208,52 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaa --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="ccccccc" +content-disposition: form-data; name="ccccccc" ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="file.name" +content-disposition: form-data; name="file.name" INPUTMSG.gz --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="file.content_type" +content-disposition: form-data; name="file.content_type" application/octet-stream --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="file.path" +content-disposition: form-data; name="file.path" /var/tmp/uploads/4/0001728414 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="file.md5" +content-disposition: form-data; name="file.md5" aa73198feb4b4c1c3186f5e7466cbbcc --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="file.size" +content-disposition: form-data; name="file.size" 13212 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="size" +content-disposition: form-data; name="size" 80892 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="mail_server_id" +content-disposition: form-data; name="mail_server_id" <1111111111.22222222.3333333333333.JavaMail.app@ffff-aaaa.dddd> --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="addresses" +content-disposition: form-data; name="addresses" {"campsy_programmer@pinkedum.com":{"domain":"pinkedum.com","name":"Campsy Programmer","type":["env_sender"],"mailbox":"campsy_programmer"},"tex@rapidcity.com":{"domain":"rapidcity.com","name":"Big Tex","type":["env_recipients","to"],"mailbox":"tex"},"group-digests@linkedin.com":{"domain":"linkedin.com","name":"Group Members","type":["from"],"mailbox":"group-digests"}} --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="received_on" +content-disposition: form-data; name="received_on" 2009-11-15T14:21:11Z --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="id" +content-disposition: form-data; name="id" dbfd9804d26d11deab24e3037639bf77 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon -Content-Disposition: form-data; name="ip_address" +content-disposition: form-data; name="ip_address" 127.0.0.1 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon-- diff --git a/test/multipart/binary b/test/multipart/binary index a3bd67c4973fae2a6aaff0f69b50f2bde312d298..ee2e7de2dea352ab65ca72d14e6831c494cb0afa 100644 Binary files a/test/multipart/binary and b/test/multipart/binary differ diff --git a/test/multipart/content_type_and_no_disposition b/test/multipart/content_type_and_no_disposition index 8a07dacdff7762767b53b4b45a0c223d61014007..7b8fcb8c109b4357a6e6af92a55abd8f864e89be 100644 --- a/test/multipart/content_type_and_no_disposition +++ b/test/multipart/content_type_and_no_disposition @@ -1,5 +1,5 @@ --AaB03x -Content-Type: text/plain; charset=US-ASCII +content-type: text/plain; charset=US-ASCII contents --AaB03x-- diff --git a/test/multipart/content_type_and_no_filename b/test/multipart/content_type_and_no_filename index bd4c89b0defbd30fb540f63be7bb72c382df5e36..05ce5041b6cae8c5d901fe32512630e453ed0e56 100644 --- a/test/multipart/content_type_and_no_filename +++ b/test/multipart/content_type_and_no_filename @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="text" -Content-Type: text/plain; charset=US-ASCII +content-disposition: form-data; name="text" +content-type: text/plain; charset=US-ASCII contents --AaB03x-- diff --git a/test/multipart/content_type_and_unknown_charset b/test/multipart/content_type_and_unknown_charset new file mode 100644 index 0000000000000000000000000000000000000000..cf9c14c7744b1fa0d8aa9567b44dceeeb98381be --- /dev/null +++ b/test/multipart/content_type_and_unknown_charset @@ -0,0 +1,6 @@ +--AaB03x +content-disposition: form-data; name="text" +content-type: text/plain; charset=foo; bar=baz + +contents +--AaB03x-- diff --git a/test/multipart/empty b/test/multipart/empty index f0f79835c96f3f28742b544cd20675da3581c0c7..d0f22e578c2c00ebe261145c6fc2ad5da682b5b6 100644 --- a/test/multipart/empty +++ b/test/multipart/empty @@ -1,10 +1,10 @@ --AaB03x -Content-Disposition: form-data; name="submit-name" +content-disposition: form-data; name="submit-name" Larry --AaB03x -Content-Disposition: form-data; name="files"; filename="file1.txt" -Content-Type: text/plain +content-disposition: form-data; name="files"; filename="file1.txt" +content-type: text/plain --AaB03x-- diff --git a/test/multipart/end_boundary_first b/test/multipart/end_boundary_first new file mode 100644 index 0000000000000000000000000000000000000000..282c7ff777248705a355f6560cac9a8679a65d85 --- /dev/null +++ b/test/multipart/end_boundary_first @@ -0,0 +1,8 @@ +--AaB03x-- + +--AaB03x +Content-Disposition: form-data; name="files"; filename="foo" +Content-Type: application/octet-stream + +contents +--AaB03x-- diff --git a/test/multipart/fail_16384_nofile b/test/multipart/fail_16384_nofile index bdcd3320f3f9266756f26c225b832b91a3783877..b10ac5bafe128dee35458c124eead7e7b9883168 100644 --- a/test/multipart/fail_16384_nofile +++ b/test/multipart/fail_16384_nofile @@ -1,813 +1,813 @@ ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="_method" +content-disposition: form-data; name="_method" put ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="authenticity_token" +content-disposition: form-data; name="authenticity_token" XCUgSyYsZ+iHQunq/yCSKFzjeVmsXV/WcphHQ0J+05I= ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[SESE]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[SESE]" BooBar ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[BBBBBBBBB]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[BBBBBBBBB]" 18 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[CCCCCCCCCCCCCCCCCCC]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[CCCCCCCCCCCCCCCCCCC]" 0 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[STARTFOO]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[STARTFOO]" 2009-11-04 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ENDFOO]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ENDFOO]" 2009-12-01 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]" 0 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[EEEEEEEEEE]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[EEEEEEEEEE]" 10000 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[FFFFFFFFF]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[FFFFFFFFF]" boskoizcool ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]" 0 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[YYYYYYYYYYYYYYY]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[YYYYYYYYYYYYYYY]" 5.00 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ZZZZZZZZZZZZZ]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ZZZZZZZZZZZZZ]" mille ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[XXXXXXXXXXXXXXXXXXXXX]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[XXXXXXXXXXXXXXXXXXXXX]" 0 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][9]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][9]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][10]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][10]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][11]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][11]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][12]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][12]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][13]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][13]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][14]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][14]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][15]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][15]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][16]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][16]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][17]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][17]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][18]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][18]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][19]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][19]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][20]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][20]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][21]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][21]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][22]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][22]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][23]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][23]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][0]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][0]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][1]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][2]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][2]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][3]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][3]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][4]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][4]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][5]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][5]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][6]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][6]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][7]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][7]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][8]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][8]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][ZEZE]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][ZEZE]" PLAPLAPLAINCINCINC ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][123412341234e]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][123412341234e]" SITE ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][12345678901]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][12345678901]" 56 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_type]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_type]" none ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][has_hashashas_has]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][has_hashashas_has]" 0 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_freee]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_freee]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_frefre]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_frefre]" forever ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][self_block]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][self_block]" 0 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][COUCOUN]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][COUCOUN]" ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][REGREG]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][REGREG]" ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][c1c1]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][c1c1]" ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA_TARTARTAR_wizard_rule" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA_TARTARTAR_wizard_rule" ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_rule]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_rule]" ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[selection_selection]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[selection_selection]" R ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][selection_selection]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][selection_selection]" 1 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][ba_unit_id]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][ba_unit_id]" 1015 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][selection_selection]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][selection_selection]" 2 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][ba_unit_id]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][ba_unit_id]" 1017 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo -Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[tile_name]" +content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[tile_name]" ------WebKitFormBoundaryWsY0GnpbI5U7ztzo-- diff --git a/test/multipart/filename_and_modification_param b/test/multipart/filename_and_modification_param index 20893f4240376bba7427d0c49923746443221206..e9af861846c74954d59103ef548ab3bc61570494 100644 --- a/test/multipart/filename_and_modification_param +++ b/test/multipart/filename_and_modification_param @@ -1,6 +1,6 @@ --AaB03x -Content-Type: image/jpeg -Content-Disposition: attachment; name="files"; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"; +content-type: image/jpeg +content-disposition: attachment; name="files"; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"; Content-Description: a complete map of the human genome contents diff --git a/test/multipart/filename_and_no_name b/test/multipart/filename_and_no_name index 00d58153d9471c1f16d5e54de5e517e69b617c82..88953d3fe648c976463d0773792513824ad04e51 100644 --- a/test/multipart/filename_and_no_name +++ b/test/multipart/filename_and_no_name @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; filename="file1.txt" -Content-Type: text/plain +content-disposition: form-data; filename="file1.txt" +content-type: text/plain contents --AaB03x-- diff --git a/test/multipart/filename_multi b/test/multipart/filename_multi new file mode 100644 index 0000000000000000000000000000000000000000..9ab5e1ef5f933f8dcab00b20eee8eed3cfd7db19 --- /dev/null +++ b/test/multipart/filename_multi @@ -0,0 +1,6 @@ +--AaB03x +Content-Disposition: form-data; name="files"; filename="foo"; filename*=utf-8''bar +Content-Type: application/octet-stream + +contents +--AaB03x-- diff --git a/test/multipart/filename_with_encoded_words b/test/multipart/filename_with_encoded_words index 0c89b02a25eb31c6dff9df42581b7f12bfb80897..a2747dfd65f8704619c2e09ac22b4b4788e49d71 100644 --- a/test/multipart/filename_with_encoded_words +++ b/test/multipart/filename_with_encoded_words @@ -1,6 +1,6 @@ --AaB03x -Content-Type: image/jpeg -Content-Disposition: attachment; name="files"; filename*=utf-8''%D1%84%D0%B0%D0%B9%D0%BB +content-type: image/jpeg +content-disposition: attachment; name="files"; filename*=utf-8''%D1%84%D0%B0%D0%B9%D0%BB Content-Description: a complete map of the human genome contents diff --git a/test/multipart/filename_with_escaped_quotes b/test/multipart/filename_with_escaped_quotes index 0a332df70f2a6919c530ed84fbd93dbe79f3a66e..f096db5feac6985cb6b09481cc7c476a07efe539 100644 --- a/test/multipart/filename_with_escaped_quotes +++ b/test/multipart/filename_with_escaped_quotes @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="escape \"quotes" -Content-Type: application/octet-stream +content-disposition: form-data; name="files"; filename="escape \"quotes" +content-type: application/octet-stream contents --AaB03x-- diff --git a/test/multipart/filename_with_escaped_quotes_and_modification_param b/test/multipart/filename_with_escaped_quotes_and_modification_param index 929f6ad3f960bd1b51a1a7bc43beddcdb1d6fcda..a1b1ed0c006e6819b2c2e5dfc7631f2a2853d9fb 100644 --- a/test/multipart/filename_with_escaped_quotes_and_modification_param +++ b/test/multipart/filename_with_escaped_quotes_and_modification_param @@ -1,6 +1,6 @@ --AaB03x -Content-Type: image/jpeg -Content-Disposition: attachment; name="files"; filename="\"human\" genome.jpeg"; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"; +content-type: image/jpeg +content-disposition: attachment; name="files"; filename="\"human\" genome.jpeg"; modification-date="Wed, 12 Feb 1997 16:29:51 -0500"; Content-Description: a complete map of the human genome contents diff --git a/test/multipart/filename_with_null_byte b/test/multipart/filename_with_null_byte index 961d44c48985dc759743f01ac1ba97f1b94d0a99..26e28674dbc99da29da9836f62cf0c23d8af1862 100644 --- a/test/multipart/filename_with_null_byte +++ b/test/multipart/filename_with_null_byte @@ -1,6 +1,6 @@ --AaB03x -Content-Type: image/jpeg -Content-Disposition: attachment; name="files"; filename="flowers.exe%00.jpg" +content-type: image/jpeg +content-disposition: attachment; name="files"; filename="flowers.exe%00.jpg" Content-Description: a complete map of the human genome contents diff --git a/test/multipart/filename_with_percent_escaped_quotes b/test/multipart/filename_with_percent_escaped_quotes index 7db06413737c6351d96750d4f56d2fbe6e38e423..af2394b1bb92dfc05d2e8508aa136eb51642788b 100644 --- a/test/multipart/filename_with_percent_escaped_quotes +++ b/test/multipart/filename_with_percent_escaped_quotes @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="escape %22quotes" -Content-Type: application/octet-stream +content-disposition: form-data; name="files"; filename="escape %22quotes" +content-type: application/octet-stream contents --AaB03x-- diff --git a/test/multipart/filename_with_plus b/test/multipart/filename_with_plus index aa75022b937827fd16a0c77bc1e230e2b436b8e2..e169a11ee6d47b576506b1b644dafbf40d15d6ce 100644 --- a/test/multipart/filename_with_plus +++ b/test/multipart/filename_with_plus @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="foo+bar" -Content-Type: application/octet-stream +content-disposition: form-data; name="files"; filename="foo+bar" +content-type: application/octet-stream contents --AaB03x-- diff --git a/test/multipart/filename_with_single_quote b/test/multipart/filename_with_single_quote index f7220abeef9ab2d3c085770d72ccf2a0dae0e905..8412701b498bb691b704e4d0f11f74efaa9b867e 100644 --- a/test/multipart/filename_with_single_quote +++ b/test/multipart/filename_with_single_quote @@ -1,6 +1,6 @@ --AaB03x -Content-Type: image/jpeg -Content-Disposition: attachment; name="files"; filename="bob's flowers.jpg" +content-type: image/jpeg +content-disposition: attachment; name="files"; filename="bob's flowers.jpg" Content-Description: a complete map of the human genome contents diff --git a/test/multipart/filename_with_unescaped_percentages b/test/multipart/filename_with_unescaped_percentages index f63dd22804fbdf7adca9eddb371f7ae7aa703533..a5ba7aeaf1c001bf9ece013540e0d2c817ce6e1e 100644 --- a/test/multipart/filename_with_unescaped_percentages +++ b/test/multipart/filename_with_unescaped_percentages @@ -1,6 +1,6 @@ ------WebKitFormBoundary2NHc7OhsgU68l3Al -Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg" -Content-Type: image/jpeg +content-disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg" +content-type: image/jpeg contents ------WebKitFormBoundary2NHc7OhsgU68l3Al-- diff --git a/test/multipart/filename_with_unescaped_percentages2 b/test/multipart/filename_with_unescaped_percentages2 index 83eac3652180efdcbb25351d49f148bdc4530d07..57023461a3382d42143eb2f73ac3ee6bc72a1e2b 100644 --- a/test/multipart/filename_with_unescaped_percentages2 +++ b/test/multipart/filename_with_unescaped_percentages2 @@ -1,6 +1,6 @@ ------WebKitFormBoundary2NHc7OhsgU68l3Al -Content-Disposition: form-data; name="document[attachment]"; filename="100%a" -Content-Type: image/jpeg +content-disposition: form-data; name="document[attachment]"; filename="100%a" +content-type: image/jpeg contents ------WebKitFormBoundary2NHc7OhsgU68l3Al-- diff --git a/test/multipart/filename_with_unescaped_percentages3 b/test/multipart/filename_with_unescaped_percentages3 index 4dba3c8856d98747c0443d53c3cb0252975cd2fa..f8d9114e18f986df0b80c5ea741e166bcbb38344 100644 --- a/test/multipart/filename_with_unescaped_percentages3 +++ b/test/multipart/filename_with_unescaped_percentages3 @@ -1,6 +1,6 @@ ------WebKitFormBoundary2NHc7OhsgU68l3Al -Content-Disposition: form-data; name="document[attachment]"; filename="100%" -Content-Type: image/jpeg +content-disposition: form-data; name="document[attachment]"; filename="100%" +content-type: image/jpeg contents ------WebKitFormBoundary2NHc7OhsgU68l3Al-- diff --git a/test/multipart/filename_with_unescaped_quotes b/test/multipart/filename_with_unescaped_quotes index 9a291e8e08a2d6218fef84e41e7a30004f8f74ab..6cd7c0da1b847f0d340fef296eb5ca1c299ea18e 100644 --- a/test/multipart/filename_with_unescaped_quotes +++ b/test/multipart/filename_with_unescaped_quotes @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="escape "quotes" -Content-Type: application/octet-stream +content-disposition: form-data; name="files"; filename="escape "quotes" +content-type: application/octet-stream contents --AaB03x-- diff --git a/test/multipart/ie b/test/multipart/ie index eae06ab5b4da7578f720781651093d26afe3aad5..ac8151b12f3ef5ad4bf5750b4f0cfefbadebe2e3 100644 --- a/test/multipart/ie +++ b/test/multipart/ie @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="C:\Documents and Settings\Administrator\Desktop\file1.txt" -Content-Type: text/plain +content-disposition: form-data; name="files"; filename="C:\Documents and Settings\Administrator\Desktop\file1.txt" +content-type: text/plain contents --AaB03x-- diff --git a/test/multipart/invalid_character b/test/multipart/invalid_character index 82467181a77cf871ba054cffc30cd3556206fbfd..324e6400621f5d7b85093a93a334ee9441eff060 100644 --- a/test/multipart/invalid_character +++ b/test/multipart/invalid_character @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="invalidÃ.txt" -Content-Type: text/plain +content-disposition: form-data; name="files"; filename="invalidÃ.txt" +content-type: text/plain contents --AaB03x-- diff --git a/test/multipart/mixed_files b/test/multipart/mixed_files index 624d8045bd5736b393262f684bdf6de4cd24a359..253ca74cbe02576852834bc03ee30f09c93d323b 100644 --- a/test/multipart/mixed_files +++ b/test/multipart/mixed_files @@ -1,20 +1,20 @@ --AaB03x -Content-Disposition: form-data; name="foo" +content-disposition: form-data; name="foo" bar --AaB03x -Content-Disposition: form-data; name="files" -Content-Type: multipart/mixed, boundary=BbC04y +content-disposition: form-data; name="files" +content-type: multipart/mixed, boundary=BbC04y --BbC04y -Content-Disposition: attachment; filename="file.txt" -Content-Type: text/plain +content-disposition: attachment; filename="file.txt" +content-type: text/plain contents --BbC04y -Content-Disposition: attachment; filename="flowers.jpg" -Content-Type: image/jpeg -Content-Transfer-Encoding: binary +content-disposition: attachment; filename="flowers.jpg" +content-type: image/jpeg +content-transfer-encoding: binary contents --BbC04y-- diff --git a/test/multipart/nested b/test/multipart/nested index 51978824452a2fc351c4d92d444d80879b2a76ca..054ad3d00279af18601b64f8696b4110fc22cda8 100644 --- a/test/multipart/nested +++ b/test/multipart/nested @@ -1,10 +1,10 @@ --AaB03x -Content-Disposition: form-data; name="foo[submit-name]" +content-disposition: form-data; name="foo[submit-name]" Larry --AaB03x -Content-Disposition: form-data; name="foo[files]"; filename="file1.txt" -Content-Type: text/plain +content-disposition: form-data; name="foo[files]"; filename="file1.txt" +content-type: text/plain contents --AaB03x-- diff --git a/test/multipart/none b/test/multipart/none index d66f4730f13c485ba8d86b546735b0fdebaa1c5f..121f85fc91be52541bd689d2f4854718ae0c9fd6 100644 --- a/test/multipart/none +++ b/test/multipart/none @@ -1,9 +1,9 @@ --AaB03x -Content-Disposition: form-data; name="submit-name" +content-disposition: form-data; name="submit-name" Larry --AaB03x -Content-Disposition: form-data; name="files"; filename="" +content-disposition: form-data; name="files"; filename="" --AaB03x-- diff --git a/test/multipart/preceding_boundary b/test/multipart/preceding_boundary new file mode 100644 index 0000000000000000000000000000000000000000..b65e647b67f615181ea4894218313d821f783f56 --- /dev/null +++ b/test/multipart/preceding_boundary @@ -0,0 +1,6 @@ +A--AaB03x +Content-Disposition: form-data; name="files"; filename="foo" +Content-Type: application/octet-stream + +contents +--AaB03x-- diff --git a/test/multipart/quoted b/test/multipart/quoted index cf4e9b64897096cda23d2aa3e1198b7db9e952d1..8593bc20a2ecfd80e2ff4530cd93c45354c88796 100644 --- a/test/multipart/quoted +++ b/test/multipart/quoted @@ -1,15 +1,15 @@ --AaB:03x -Content-Disposition: form-data; name="submit-name" +content-disposition: form-data; name="submit-name" Larry --AaB:03x -Content-Disposition: form-data; name="submit-name-with-content" -Content-Type: text/plain +content-disposition: form-data; name="submit-name-with-content" +content-type: text/plain Berry --AaB:03x -Content-Disposition: form-data; name="files"; filename="file1.txt" -Content-Type: text/plain +content-disposition: form-data; name="files"; filename="file1.txt" +content-type: text/plain contents --AaB:03x-- diff --git a/test/multipart/robust_field_separation b/test/multipart/robust_field_separation index 34956b150c3263f6329bd620255375c77e23d895..9fca06350bc3c3594ae1ebd3e828bda7a1616e39 100644 --- a/test/multipart/robust_field_separation +++ b/test/multipart/robust_field_separation @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data;name="text" -Content-Type: text/plain +content-disposition: form-data;name="text" +content-type: text/plain contents --AaB03x-- diff --git a/test/multipart/semicolon b/test/multipart/semicolon index 00fd68ab85084bdf44c177fd34fb9cac1c101e44..d3702a2ba1e01b32ebec90136a03582251cf6a4c 100644 --- a/test/multipart/semicolon +++ b/test/multipart/semicolon @@ -1,6 +1,6 @@ --AaB03x -Content-Disposition: form-data; name="files"; filename="fi;le1.txt" -Content-Type: text/plain +content-disposition: form-data; name="files"; filename="fi;le1.txt" +content-type: text/plain contents --AaB03x-- \ No newline at end of file diff --git a/test/multipart/space case.txt b/test/multipart/space case.txt new file mode 100644 index 0000000000000000000000000000000000000000..0839b2e9412b314cb8bb9a20f587aa13752ae310 --- /dev/null +++ b/test/multipart/space case.txt @@ -0,0 +1 @@ +contents \ No newline at end of file diff --git a/test/multipart/text b/test/multipart/text index 01376d02fe5c4fabe5f4dcceaebaa236fd05df75..ac340228cf65cc74ec7520a43d13c363bac811f4 100644 --- a/test/multipart/text +++ b/test/multipart/text @@ -1,15 +1,15 @@ --AaB03x -Content-Disposition: form-data; name="submit-name" +content-disposition: form-data; name="submit-name" Larry --AaB03x -Content-Disposition: form-data; name="submit-name-with-content" -Content-Type: text/plain +content-disposition: form-data; name="submit-name-with-content" +content-type: text/plain Berry --AaB03x -Content-Disposition: form-data; name="files"; filename="file1.txt" -Content-Type: text/plain +content-disposition: form-data; name="files"; filename="file1.txt" +content-type: text/plain contents --AaB03x-- \ No newline at end of file diff --git a/test/multipart/three_files_three_fields b/test/multipart/three_files_three_fields index 40d88b56c5df2db7480a9d3552dade4864ecadfa..8917424eaad8f939ef71ac14aa4a6744c0600454 100644 --- a/test/multipart/three_files_three_fields +++ b/test/multipart/three_files_three_fields @@ -12,20 +12,20 @@ content-disposition: form-data; name="from" others --AaB03x content-disposition: form-data; name="fileupload1"; filename="file1.jpg" -Content-Type: image/jpeg -Content-Transfer-Encoding: base64 +content-type: image/jpeg +content-transfer-encoding: base64 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg --AaB03x content-disposition: form-data; name="fileupload2"; filename="file2.jpg" -Content-Type: image/jpeg -Content-Transfer-Encoding: base64 +content-type: image/jpeg +content-transfer-encoding: base64 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg --AaB03x content-disposition: form-data; name="fileupload3"; filename="file3.jpg" -Content-Type: image/jpeg -Content-Transfer-Encoding: base64 +content-type: image/jpeg +content-transfer-encoding: base64 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg --AaB03x-- diff --git a/test/multipart/webkit b/test/multipart/webkit index 1375af317bda6ae0c402162f2277d602a69930e1..044d4db375ef4b1740720432247ca4703dee0ea2 100644 --- a/test/multipart/webkit +++ b/test/multipart/webkit @@ -1,32 +1,32 @@ ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="_method" +content-disposition: form-data; name="_method" put ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="profile[blog]" +content-disposition: form-data; name="profile[blog]" ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="profile[public_email]" +content-disposition: form-data; name="profile[public_email]" ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="profile[interests]" +content-disposition: form-data; name="profile[interests]" ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="profile[bio]" +content-disposition: form-data; name="profile[bio]" hello "quote" ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="media"; filename="" +content-disposition: form-data; name="media"; filename="" Content-Type: application/octet-stream ------WebKitFormBoundaryWLHCs9qmcJJoyjKR -Content-Disposition: form-data; name="commit" +content-disposition: form-data; name="commit" Save ------WebKitFormBoundaryWLHCs9qmcJJoyjKR-- diff --git a/test/rackup/config.ru b/test/rackup/config.ru index fa9b6ecab50cbd3b7a1eabafa520d07710823738..267ffb506ab43ad4c6be9aa1a2f658b98526f86d 100644 --- a/test/rackup/config.ru +++ b/test/rackup/config.ru @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "#{File.dirname(__FILE__)}/../testrequest" +require_relative "../test_request" $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 deleted file mode 100644 index 21b6051676cb8ffe2cf2a3bb1e226accb30ae634..0000000000000000000000000000000000000000 --- a/test/registering_handler/rack/handler/registering_myself.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module Rack - module Handler - class RegisteringMyself - end - - register :registering_myself, RegisteringMyself - end -end diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb index 7d39b195260deeb11828f447c6c4e2d7a6a8d7d7..ee7000495e6af27e690903d07235a0a38902a2e2 100644 --- a/test/spec_auth_basic.rb +++ b/test/spec_auth_basic.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/auth/basic' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/lint' +end + describe Rack::Auth::Basic do def realm 'WallysWorld' @@ -9,7 +15,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 @@ -34,8 +40,8 @@ describe Rack::Auth::Basic do def assert_basic_auth_challenge(response) response.must_be :client_error? response.status.must_equal 401 - response.must_include 'WWW-Authenticate' - response.headers['WWW-Authenticate'].must_match(/Basic realm="#{Regexp.escape(realm)}"/) + response.must_include 'www-authenticate' + response.headers['www-authenticate'].must_match(/Basic realm="#{Regexp.escape(realm)}"/) response.body.must_be :empty? end @@ -62,7 +68,7 @@ describe Rack::Auth::Basic do request 'HTTP_AUTHORIZATION' => 'Digest params' do |response| response.must_be :client_error? response.status.must_equal 400 - response.wont_include 'WWW-Authenticate' + response.wont_include 'www-authenticate' end end @@ -70,7 +76,7 @@ describe Rack::Auth::Basic do request 'HTTP_AUTHORIZATION' => '' do |response| response.must_be :client_error? response.status.must_equal 400 - response.wont_include 'WWW-Authenticate' + response.wont_include 'www-authenticate' end end @@ -86,7 +92,7 @@ describe Rack::Auth::Basic do request 'HTTP_AUTHORIZATION' => auth do |response| response.must_be :client_error? response.status.must_equal 400 - response.wont_include 'WWW-Authenticate' + response.wont_include 'www-authenticate' end end diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb index 6e32152f401c377e1d9c07136e5327039b1a463b..3a8981c5d96b18faa025213575e06f1ddd9f6b0d 100644 --- a/test/spec_auth_digest.rb +++ b/test/spec_auth_digest.rb @@ -2,6 +2,16 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/auth/digest/md5' + require_relative '../lib/rack/auth/digest/nonce' + require_relative '../lib/rack/auth/digest/params' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/urlmap' + require_relative '../lib/rack/method_override' +end + describe Rack::Auth::Digest::MD5 do def realm 'WallysWorld' @@ -10,7 +20,7 @@ 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 @@ -81,7 +91,7 @@ describe Rack::Auth::Digest::MD5 do sleep wait end - challenge = response['WWW-Authenticate'].split(' ', 2).last + challenge = response['www-authenticate'].split(' ', 2).last params = Rack::Auth::Digest::Params.parse(challenge) @@ -102,15 +112,15 @@ describe Rack::Auth::Digest::MD5 do def assert_digest_auth_challenge(response) response.must_be :client_error? response.status.must_equal 401 - response.must_include 'WWW-Authenticate' - response.headers['WWW-Authenticate'].must_match(/^Digest /) + response.must_include 'www-authenticate' + response.headers['www-authenticate'].must_match(/^Digest /) response.body.must_be :empty? end def assert_bad_request(response) response.must_be :client_error? response.status.must_equal 400 - response.wont_include 'WWW-Authenticate' + response.wont_include 'www-authenticate' end it 'challenge when no credentials are specified' do @@ -160,7 +170,7 @@ describe Rack::Auth::Digest::MD5 do 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/) + response.headers['www-authenticate'].wont_match(/\bstale=true\b/) end ensure Rack::Auth::Digest::Nonce.time_limit = nil @@ -173,7 +183,7 @@ describe Rack::Auth::Digest::MD5 do 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/) + response.headers['www-authenticate'].must_match(/\bstale=true\b/) end ensure Rack::Auth::Digest::Nonce.time_limit = nil @@ -263,6 +273,7 @@ describe Rack::Auth::Digest::MD5 do req.respond_to?(:nonce).must_equal true req.respond_to?(:a).must_equal true req.a.must_equal 'b' + proc{req.missing}.must_raise NoMethodError lambda { req.a(2) }.must_raise ArgumentError end @@ -270,4 +281,8 @@ describe Rack::Auth::Digest::MD5 do Rack::Auth::Digest::Nonce.new.fresh?.must_equal true Rack::Auth::Digest::Nonce.new.stale?.must_equal false end + + it 'Params.new can be called without a block' do + Rack::Auth::Digest::Params.new.must_be_instance_of(Rack::Auth::Digest::Params) + end end diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb index 1199f2f18e42db06a53b943174b05417cf28c24b..91fcbd74e856ede0d1fc9cd5bf0ef9166058d6e8 100644 --- a/test/spec_body_proxy.rb +++ b/test/spec_body_proxy.rb @@ -2,6 +2,10 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/body_proxy' +end + describe Rack::BodyProxy do it 'call each on the wrapped body' do called = false diff --git a/test/spec_builder.rb b/test/spec_builder.rb index c0f59c1828cb14193b3ecfa3e60b72ca94eeaa8e..2cc7732c7fa6e2418021917a8e3964120b31568a 100644 --- a/test/spec_builder.rb +++ b/test/spec_builder.rb @@ -2,6 +2,15 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/builder' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/content_length' + require_relative '../lib/rack/show_exceptions' + require_relative '../lib/rack/auth/basic' +end + class NothingMiddleware def initialize(app, **) @app = app @@ -25,13 +34,20 @@ describe Rack::Builder do Rack::Lint.new Rack::Builder.new(&block).to_app end + it "supports run with block" do + app = builder_to_app do + run {|env| [200, { "content-type" => "text/plain" }, ["OK"]]} + end + Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK' + end + 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' @@ -42,13 +58,13 @@ describe Rack::Builder do app = builder_to_app do map '/sub' do use Rack::ContentLength - run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['sub']] } + run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['sub']] } end use Rack::ContentLength - 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("/").headers['Content-Length'].must_equal '4' - Rack::MockRequest.new(app).get("/sub").headers['Content-Length'].must_equal '3' + Rack::MockRequest.new(app).get("/").headers['content-length'].must_equal '4' + Rack::MockRequest.new(app).get("/sub").headers['content-length'].must_equal '3' end it "doesn't dupe env even when mapping" do @@ -57,7 +73,7 @@ describe Rack::Builder do map '/' do |outer_env| run lambda { |inner_env| inner_env['new_key'] = 'new_value' - [200, { "Content-Type" => "text/plain" }, ['root']] + [200, { "content-type" => "text/plain" }, ['root']] } end end @@ -68,7 +84,7 @@ describe Rack::Builder do 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]] } + run lambda { |env| [200, { "content-type" => "text/plain" }, [object_id.to_s]] } end end @@ -107,7 +123,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("/") @@ -135,9 +151,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' @@ -174,7 +190,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 @@ -229,12 +245,11 @@ describe Rack::Builder do File.join(File.dirname(__FILE__), 'builder', name) end - it "parses commented options" do - app, options = Rack::Builder.parse_file config_file('options.ru') - options[:debug].must_equal true - options[:environment].must_equal 'test' - options[:Port].must_equal '2929' - Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK' + it "raises if parses commented options" do + proc do + Rack::Builder.parse_file config_file('options.ru') + end.must_raise(RuntimeError). + message.must_include('Parsing options from the first comment line is no longer supported') end it "removes __END__ before evaluating app" do @@ -243,9 +258,8 @@ describe Rack::Builder do end it "supports multi-line comments" do - proc, env = Rack::Builder.parse_file(config_file('comment.ru')) - proc.must_be_kind_of Proc - env.must_equal({}) + app = Rack::Builder.parse_file(config_file('comment.ru')) + app.must_be_kind_of(Proc) end it 'requires an_underscore_app not ending in .ru' do @@ -263,11 +277,13 @@ describe Rack::Builder do it "strips leading unicode byte order mark when present" do enc = Encoding.default_external begin + verbose, $VERBOSE = $VERBOSE, nil Encoding.default_external = 'UTF-8' app, _ = Rack::Builder.parse_file config_file('bom.ru') Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK' ensure Encoding.default_external = enc + $VERBOSE = verbose end end @@ -283,7 +299,7 @@ describe Rack::Builder do describe 'new_from_string' do it "builds a rack app from string" do - app, = Rack::Builder.new_from_string "run lambda{|env| [200, {'Content-Type' => 'text/plane'}, ['OK']] }" + app, = Rack::Builder.new_from_string "run lambda{|env| [200, {'content-type' => 'text/plane'}, ['OK']] }" 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 8f1fd131ce915c935a0b3060e162fa86cb1bbafd..50dd2805b33c00108015c97a663b92342bd20ed8 100644 --- a/test/spec_cascade.rb +++ b/test/spec_cascade.rb @@ -2,6 +2,14 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/cascade' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/urlmap' + require_relative '../lib/rack/files' +end + describe Rack::Cascade do def cascade(*args) Rack::Lint.new Rack::Cascade.new(*args) @@ -13,7 +21,7 @@ describe Rack::Cascade do app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" }) app3 = Rack::URLMap.new("/foo" => lambda { |env| - [200, { "Content-Type" => "text/plain" }, [""]]}) + [200, { "content-type" => "text/plain" }, [""]]}) it "dispatch onward on 404 and 405 by default" do cascade = cascade([app1, app2, app3]) @@ -47,17 +55,17 @@ describe Rack::Cascade do res = app.call('/') s, h, body = res s.must_equal 404 - h['Content-Type'].must_equal 'text/plain' + h['content-type'].must_equal 'text/plain' body.must_be_empty res[0] = 200 - h['Content-Type'] = 'text/html' + h['content-type'] = 'text/html' body << "a" res = app.call('/') s, h, body = res s.must_equal 404 - h['Content-Type'].must_equal 'text/plain' + h['content-type'].must_equal 'text/plain' body.must_be_empty end diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb index ceb7bdfb2e1df2e03b4a2fb77e25ccb3474347c0..4ba4eefbc5aa81770d3e1471c4600ca2dd3d1427 100644 --- a/test/spec_chunked.rb +++ b/test/spec_chunked.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/chunked' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Chunked do def chunked(app) proc do |env| @@ -24,33 +30,49 @@ describe Rack::Chunked do end def trailers - { "Expires" => "tomorrow" } + { "expires" => "tomorrow" } end end it 'yields trailer headers after the response' do app = lambda { |env| - [200, { "Content-Type" => "text/plain", "Trailer" => "Expires" }, TrailerBody.new] + [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" + 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!']] } + it 'chunk responses with no content-length' do + 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' + 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\n\r\n" end + it 'avoid empty chunks' do + 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' + response.body.must_equal "5\r\nHello\r\n6\r\nWorld!\r\n0\r\n\r\n" + end + + it 'handles unclosable bodies' do + app = lambda { |env| [200, { "content-type" => "text/plain" }, ['Hello', '', 'World!']] } + response = Rack::MockResponse.new(*Rack::Chunked.new(app).call(@env)) + response.headers.wont_include 'content-length' + response.headers['transfer-encoding'].must_equal 'chunked' + response.body.must_equal "5\r\nHello\r\n6\r\nWorld!\r\n0\r\n\r\n" + 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' + response.headers.wont_include 'content-length' + response.headers['transfer-encoding'].must_equal 'chunked' response.body.must_equal "0\r\n\r\n" end @@ -59,68 +81,65 @@ describe Rack::Chunked do closed = false def obj.each; yield 's' end obj.define_singleton_method(:close) { closed = true } - app = lambda { |env| [200, { "Content-Type" => "text/plain" }, obj] } + app = lambda { |env| [200, { "content-type" => "text/plain" }, obj] } response = Rack::MockRequest.new(Rack::Chunked.new(app)).get('/', @env) - response.headers.wont_include 'Content-Length' - response.headers['Transfer-Encoding'].must_equal 'chunked' + response.headers.wont_include 'content-length' + response.headers['transfer-encoding'].must_equal 'chunked' response.body.must_equal "1\r\ns\r\n0\r\n\r\n" closed.must_equal true end 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.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".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 + 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 - headers.wont_include 'Transfer-Encoding' - headers.must_include 'Content-Length' + headers.wont_include 'transfer-encoding' + headers.must_include 'content-length' body.join.must_equal 'Hello World!' end it 'not modify response when client is HTTP/1.0' do - app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] } + app = lambda { |env| [200, { "content-type" => "text/plain" }, ['Hello', ' ', 'World!']] } @env['SERVER_PROTOCOL'] = 'HTTP/1.0' status, headers, body = chunked(app).call(@env) status.must_equal 200 - headers.wont_include 'Transfer-Encoding' + headers.wont_include 'transfer-encoding' body.join.must_equal 'Hello World!' 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 - headers.wont_include 'Transfer-Encoding' + headers.wont_include 'transfer-encoding' body.join.must_equal 'Hello World!' end - @env.delete('SERVER_PROTOCOL') # unicorn will do this on pre-HTTP/1.0 requests - check.call - @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 + 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 - headers['Transfer-Encoding'].must_equal 'identity' + headers['transfer-encoding'].must_equal 'identity' body.join.must_equal 'Hello World!' end @@ -129,7 +148,7 @@ describe Rack::Chunked do app = lambda { |env| [status_code, {}, []] } status, headers, _ = chunked(app).call(@env) status.must_equal status_code - headers.wont_include 'Transfer-Encoding' + headers.wont_include 'transfer-encoding' end end end diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb index 4ddb5f03d374214e78467587e65b100f6f0d93f2..595debd47a1d0f9e41c0a47a3670ce25d580dafa 100644 --- a/test/spec_common_logger.rb +++ b/test/spec_common_logger.rb @@ -3,21 +3,27 @@ require_relative 'helper' require 'logger' +separate_testing do + require_relative '../lib/rack/common_logger' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::CommonLogger do obj = 'foobar' length = obj.size 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" }, []]} app_without_lint = lambda { |env| [200, @@ -28,14 +34,14 @@ describe Rack::CommonLogger do res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/") res.errors.wont_be :empty? - res.errors.must_match(/"GET \/ " 200 #{length} /) + res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /) end it "log to anything with +write+" do log = StringIO.new Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/") - log.string.must_match(/"GET \/ " 200 #{length} /) + log.string.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /) end it "work with standard library logger" do @@ -43,21 +49,35 @@ describe Rack::CommonLogger do log = Logger.new(logdev) Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/") - logdev.string.must_match(/"GET \/ " 200 #{length} /) + logdev.string.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /) end it "log - content length if header is missing" do res = Rack::MockRequest.new(Rack::CommonLogger.new(app_without_length)).get("/") res.errors.wont_be :empty? - res.errors.must_match(/"GET \/ " 200 - /) + res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 - /) end it "log - content length if header is zero" do res = Rack::MockRequest.new(Rack::CommonLogger.new(app_with_zero_length)).get("/") res.errors.wont_be :empty? - res.errors.must_match(/"GET \/ " 200 - /) + res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 - /) + end + + it "log - records host from X-Forwarded-For header" do + res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/", 'HTTP_X_FORWARDED_FOR' => '203.0.113.0') + + res.errors.wont_be :empty? + res.errors.must_match(/203\.0\.113\.0 - /) + end + + it "log - records host from RFC 7239 forwarded for header" do + res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/", 'HTTP_FORWARDED' => 'for=203.0.113.0') + + res.errors.wont_be :empty? + res.errors.must_match(/203\.0\.113\.0 - /) end def with_mock_time(t = 0) @@ -75,10 +95,10 @@ describe Rack::CommonLogger do it "log in common log format" do log = StringIO.new with_mock_time do - Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/") + Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/", 'QUERY_STRING' => 'foo=bar') end - md = /- - - \[([^\]]+)\] "(\w+) \/ " (\d{3}) \d+ ([\d\.]+)/.match(log.string) + md = /- - - \[([^\]]+)\] "(\w+) \/\?foo=bar HTTP\/1\.1" (\d{3}) \d+ ([\d\.]+)/.match(log.string) md.wont_equal nil time, method, status, duration = *md.captures time.must_equal Time.at(0).strftime("%d/%b/%Y:%H:%M:%S %z") @@ -90,9 +110,9 @@ describe Rack::CommonLogger do it "escapes non printable characters except newline" do logdev = StringIO.new log = Logger.new(logdev) - Rack::MockRequest.new(Rack::CommonLogger.new(app_without_lint, log)).request("GET\b", "/hello") + Rack::MockRequest.new(Rack::CommonLogger.new(app_without_lint, log)).request("GET\x1f", "/hello") - logdev.string.must_match(/GET\\x8 \/hello/) + logdev.string.must_match(/GET\\x1f \/hello HTTP\/1\.1/) end it "log path with PATH_INFO" do @@ -100,7 +120,7 @@ describe Rack::CommonLogger do log = Logger.new(logdev) Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/hello") - logdev.string.must_match(/"GET \/hello " 200 #{length} /) + logdev.string.must_match(/"GET \/hello HTTP\/1\.1" 200 #{length} /) end it "log path with SCRIPT_NAME" do @@ -108,7 +128,15 @@ describe Rack::CommonLogger do log = Logger.new(logdev) Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/path", script_name: "/script") - logdev.string.must_match(/"GET \/script\/path " 200 #{length} /) + logdev.string.must_match(/"GET \/script\/path HTTP\/1\.1" 200 #{length} /) + end + + it "log path with SERVER_PROTOCOL" do + logdev = StringIO.new + log = Logger.new(logdev) + Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/path", http_version: "HTTP/1.0") + + logdev.string.must_match(/"GET \/path HTTP\/1\.0" 200 #{length} /) end def length diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb index 5d517be4dad6276d94bbf8ee5d38f7339d2e1c01..d028b3dec60bfe521743d10e747d86928c1df535 100644 --- a/test/spec_conditional_get.rb +++ b/test/spec_conditional_get.rb @@ -3,15 +3,21 @@ require_relative 'helper' require 'time' +separate_testing do + require_relative '../lib/rack/conditional_get' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::ConditionalGet do def conditional_get(app) Rack::Lint.new Rack::ConditionalGet.new(app) end - it "set a 304 status and truncate body when If-Modified-Since hits" 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) @@ -20,20 +26,36 @@ describe Rack::ConditionalGet do response.body.must_be :empty? end - it "set a 304 status and truncate body when If-Modified-Since hits and is higher than current time" 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']] }) + + response = Rack::MockRequest.new(app). + get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate) + + response.status.must_equal 304 + response.body.must_be :empty? + end + + it "closes bodies" do + body = Object.new + def body.each; yield 'TEST' end + closed = false + body.define_singleton_method(:close){closed = true} app = conditional_get(lambda { |env| - [200, { 'Last-Modified' => (Time.now - 3600).httpdate }, ['TEST']] }) + [200, { 'last-modified' => (Time.now - 3600).httpdate }, body] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate) response.status.must_equal 304 response.body.must_be :empty? + closed.must_equal true end - it "set a 304 status and truncate body when If-None-Match hits" 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') @@ -42,9 +64,9 @@ describe Rack::ConditionalGet do response.body.must_be :empty? end - it "set a 304 status and truncate body when If-None-Match hits but If-Modified-Since is after Last-Modified" do + it "set a 304 status and truncate body when if-none-match hits but if-modified-since is after last-modified" do app = conditional_get(lambda { |env| - [200, { 'Last-Modified' => (Time.now + 3600).httpdate, 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] }) + [200, { 'last-modified' => (Time.now + 3600).httpdate, 'etag' => '1234', 'content-type' => 'text/plain' }, ['TEST']] }) response = Rack::MockRequest.new(app). get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate, 'HTTP_IF_NONE_MATCH' => '1234') @@ -53,10 +75,21 @@ describe Rack::ConditionalGet do response.body.must_be :empty? end - it "not set a 304 status if If-Modified-Since hits but Etag does not" do + it "not set a 304 status if last-modified is too short" do + app = conditional_get(lambda { |env| + [200, { 'last-modified' => '1234', 'content-type' => 'text/plain' }, ['TEST']] }) + + response = Rack::MockRequest.new(app). + get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate) + + response.status.must_equal 200 + response.body.must_equal 'TEST' + end + + 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') @@ -65,10 +98,10 @@ describe Rack::ConditionalGet do response.body.must_equal 'TEST' end - it "set a 304 status and truncate body when both If-None-Match and If-Modified-Since hits" 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') @@ -79,7 +112,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') @@ -90,7 +123,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') @@ -102,7 +135,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 304ef8bf715a67026821b03f717927b090b5511c..ce5f777776342aed616135bfe52f1042bcbc9474 100644 --- a/test/spec_config.rb +++ b/test/spec_config.rb @@ -2,6 +2,13 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/config' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/builder' + require_relative '../lib/rack/mock_request' +end + describe Rack::Config do it "accept a block that modifies the environment" do app = Rack::Builder.new do @@ -10,7 +17,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 07a4c56e72ce7f5bd81160d9f7851fe494f826e6..cffe8cb45957b613994dd53b0c9f81df23c4d580 100644 --- a/test/spec_content_length.rb +++ b/test/spec_content_length.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/content_length' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::ContentLength do def content_length(app) Rack::Lint.new Rack::ContentLength.new(app) @@ -11,60 +17,58 @@ describe Rack::ContentLength do Rack::MockRequest.env_for end - it "set Content-Length on Array bodies if none is set" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } + it "set content-length on Array bodies if none is set" do + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] } response = content_length(app).call(request) - response[1]['Content-Length'].must_equal '13' + response[1]['content-length'].must_equal '13' end - it "set Content-Length on variable length bodies" do + it "not set content-length on variable length bodies" 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_equal '12' + 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!"] } + it "not change content-length if it is already set" do + 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' + response[1]['content-length'].must_equal '1' end - it "not set Content-Length on 304 responses" do + it "not set content-length on 304 responses" do app = lambda { |env| [304, {}, []] } response = content_length(app).call(request) - response[1]['Content-Length'].must_be_nil + response[1]['content-length'].must_be_nil end - it "not set Content-Length when Transfer-Encoding is chunked" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked' }, []] } + it "not set content-length when transfer-encoding is chunked" do + app = lambda { |env| [200, { 'content-type' => 'text/plain', 'transfer-encoding' => 'chunked' }, []] } response = content_length(app).call(request) - response[1]['Content-Length'].must_be_nil + response[1]['content-length'].must_be_nil end # Using "Connection: close" for this is fairly contended. It might be useful # to have some other way to signal this. # - # should "not force a Content-Length when Connection:close" do + # should "not force a content-length when Connection:close" do # app = lambda { |env| [200, {'Connection' => 'close'}, []] } # response = content_length(app).call({}) - # response[1]['Content-Length'].must_be_nil + # response[1]['content-length'].must_be_nil # end it "close bodies that need to be closed" do body = Struct.new(:body) do attr_reader :closed - def each; body.join; end + def each; body.each {|b| yield b}; close; end def close; @closed = true; end - def to_ary; end + def to_ary; enum_for.to_a; end end.new(%w[one two three]) - app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] } - response = content_length(app).call(request) - body.closed.must_be_nil - response[2].close + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] } + content_length(app).call(request) body.closed.must_equal true end @@ -73,13 +77,13 @@ describe Rack::ContentLength do def each yield body.shift until body.empty? end - def to_ary; end + def to_ary; enum_for.to_a; 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 + response[1]['content-length'].must_equal expected.join.size.to_s response[2].to_enum.to_a.must_equal expected end end diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb index 4cfc32231f84e15f2af996eaa541fcb6c4a8cdca..fff687080f536312c1e4d7ddbc8c814127615367 100644 --- a/test/spec_content_type.rb +++ b/test/spec_content_type.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/content_type' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::ContentType do def content_type(app, *args) Rack::Lint.new Rack::ContentType.new(app, *args) @@ -11,45 +17,30 @@ describe Rack::ContentType do Rack::MockRequest.env_for end - it "set Content-Type to default text/html if none is set" do + it "set content-type to default text/html if none is set" do app = lambda { |env| [200, {}, "Hello, World!"] } headers = content_type(app).call(request)[1] - headers['Content-Type'].must_equal 'text/html' + headers['content-type'].must_equal 'text/html' end - it "set Content-Type to chosen default if none is set" do + it "set content-type to chosen default if none is set" do app = lambda { |env| [200, {}, "Hello, World!"] } headers = content_type(app, 'application/octet-stream').call(request)[1] - headers['Content-Type'].must_equal 'application/octet-stream' - end - - it "not change Content-Type if it is already set" do - app = lambda { |env| [200, { 'Content-Type' => 'foo/bar' }, "Hello, World!"] } - headers = content_type(app).call(request)[1] - headers['Content-Type'].must_equal 'foo/bar' + headers['content-type'].must_equal 'application/octet-stream' end - it "detect Content-Type case insensitive" do - app = lambda { |env| [200, { 'CONTENT-Type' => 'foo/bar' }, "Hello, World!"] } + it "not change content-type if it is already set" do + 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['content-type'].must_equal '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 - - ['100', '204', '304'].each do |code| - it "not set Content-Type on #{code} responses if status is a string" do + 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 + response[1]['content-type'].must_be_nil end end end diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb index ed9cffeca63f362c9e9c51054bdded6a0b7912cd..9d6e81f50f30798c6d31f2478c0ee1b1a5aabc77 100644 --- a/test/spec_deflater.rb +++ b/test/spec_deflater.rb @@ -4,13 +4,19 @@ require_relative 'helper' require 'time' # for Time#httpdate require 'zlib' +separate_testing do + require_relative '../lib/rack/deflater' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Deflater do def build_response(status, body, accept_encoding, options = {}) body = [body] if body.respond_to? :to_str app = lambda do |env| res = [status, options['response_headers'] || {}, body] - res[1]['Content-Type'] = 'text/plain' unless res[0] == 304 + res[1]['content-type'] = 'text/plain' unless res[0] == 304 res end @@ -25,7 +31,7 @@ describe Rack::Deflater do # # [expected_status] expected response status, e.g. 200, 304 # [expected_body] expected response body - # [accept_encoing] what Accept-Encoding header to send and expect, e.g. + # [accept_encoding] what Accept-Encoding header to send and expect, e.g. # 'deflate' - accepts and expects deflate encoding in response # { 'gzip' => nil } - accepts gzip but expects no encoding in response # [options] hash of request options, i.e. @@ -68,7 +74,7 @@ describe Rack::Deflater do io = StringIO.new(body_text) gz = Zlib::GzipReader.new(io) mtime = gz.mtime.to_i - if last_mod = headers['Last-Modified'] + if last_mod = headers['last-modified'] Time.httpdate(last_mod).to_i.must_equal mtime else mtime.must_be(:<=, Time.now.to_i) @@ -86,6 +92,7 @@ describe Rack::Deflater do # yield full response verification yield(status, headers, body) if block_given? + body.close if body.respond_to?(:close) end # automatic gzip detection (streamable) @@ -103,22 +110,34 @@ describe Rack::Deflater do 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' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) end end + it 'should not update vary response header if it includes * or accept-encoding' do + verify(200, 'foobar', deflate_or_gzip, 'response_headers' => { 'vary' => 'Accept-Encoding' } ) do |status, headers, body| + headers['vary'].must_equal 'Accept-Encoding' + end + verify(200, 'foobar', deflate_or_gzip, 'response_headers' => { 'vary' => '*' } ) do |status, headers, body| + headers['vary'].must_equal '*' + end + verify(200, 'foobar', deflate_or_gzip, 'response_headers' => { 'vary' => 'Do-Not-Accept-Encoding' } ) do |status, headers, body| + headers['vary'].must_equal 'Do-Not-Accept-Encoding,Accept-Encoding' + 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' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) end end @@ -129,9 +148,9 @@ describe Rack::Deflater do verify(200, app_body, deflate_or_gzip, { 'skip_body_verify' => true }) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'gzip', - 'Vary' => 'Accept-Encoding', - 'Content-Type' => 'text/plain' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) buf = [] @@ -149,9 +168,9 @@ describe Rack::Deflater do opts = { 'skip_body_verify' => true } verify(200, app_body, 'gzip', opts) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'gzip', - 'Vary' => 'Accept-Encoding', - 'Content-Type' => 'text/plain' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) buf = [] @@ -173,9 +192,9 @@ describe Rack::Deflater do it 'be able to deflate String bodies' do verify(200, 'Hello world!', deflate_or_gzip) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'gzip', - 'Vary' => 'Accept-Encoding', - 'Content-Type' => 'text/plain' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) end end @@ -186,9 +205,19 @@ describe Rack::Deflater do verify(200, 'foobar', 'gzip', { 'app_body' => app_body }) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'gzip', - 'Vary' => 'Accept-Encoding', - 'Content-Type' => 'text/plain' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' + }) + end + end + + it 'be able to gzip files' do + verify(200, File.binread(__FILE__), 'gzip', { 'app_body' => File.open(__FILE__)}) do |status, headers, body| + headers.must_equal({ + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) end end @@ -199,9 +228,9 @@ describe Rack::Deflater do verify(200, app_body, 'gzip', { 'skip_body_verify' => true }) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'gzip', - 'Vary' => 'Accept-Encoding', - 'Content-Type' => 'text/plain' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) buf = [] @@ -216,8 +245,8 @@ describe Rack::Deflater do it 'be able to fallback to no deflation' do verify(200, 'Hello world!', 'superzip') do |status, headers, body| headers.must_equal({ - 'Vary' => 'Accept-Encoding', - 'Content-Type' => 'text/plain' + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) end end @@ -247,67 +276,86 @@ describe Rack::Deflater do } } + app_body3 = [app_body] + closed = false + app_body3.define_singleton_method(:close){closed = true} + options3 = { + 'app_status' => 200, + 'app_body' => app_body3, + 'request_headers' => { + 'PATH_INFO' => '/' + } + } + verify(406, not_found_body1, 'identity;q=0', options1) do |status, headers, body| headers.must_equal({ - 'Content-Type' => 'text/plain', - 'Content-Length' => not_found_body1.length.to_s + 'content-type' => 'text/plain', + 'content-length' => not_found_body1.length.to_s }) end verify(406, not_found_body2, 'identity;q=0', options2) do |status, headers, body| headers.must_equal({ - 'Content-Type' => 'text/plain', - 'Content-Length' => not_found_body2.length.to_s + 'content-type' => 'text/plain', + 'content-length' => not_found_body2.length.to_s }) end + + verify(406, not_found_body1, 'identity;q=0', options3) do |status, headers, body| + headers.must_equal({ + 'content-type' => 'text/plain', + 'content-length' => not_found_body1.length.to_s + }) + end + closed.must_equal true end - it 'handle gzip response with Last-Modified header' do + it 'handle gzip response with last-modified header' do last_modified = Time.now.httpdate options = { 'response_headers' => { - 'Content-Type' => 'text/plain', - 'Last-Modified' => last_modified + 'content-type' => 'text/plain', + 'last-modified' => last_modified } } verify(200, 'Hello World!', 'gzip', options) do |status, headers, body| headers.must_equal({ - 'Content-Encoding' => 'gzip', - 'Vary' => 'Accept-Encoding', - 'Last-Modified' => last_modified, - 'Content-Type' => 'text/plain' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'last-modified' => last_modified, + 'content-type' => 'text/plain' }) end end - it 'do nothing when no-transform Cache-Control directive present' do + it 'do nothing when no-transform cache-control directive present' do options = { 'response_headers' => { - 'Content-Type' => 'text/plain', - 'Cache-Control' => 'no-transform' + 'content-type' => 'text/plain', + 'cache-control' => 'no-transform' } } verify(200, 'Hello World!', { 'gzip' => nil }, options) do |status, headers, body| - headers.wont_include 'Content-Encoding' + headers.wont_include 'content-encoding' end end - it 'do nothing when Content-Encoding already present' do + it 'do nothing when content-encoding already present' do options = { 'response_headers' => { - 'Content-Type' => 'text/plain', - 'Content-Encoding' => 'gzip' + 'content-type' => 'text/plain', + 'content-encoding' => 'gzip' } } verify(200, 'Hello World!', { 'gzip' => nil }, options) end - it 'deflate when Content-Encoding is identity' do + it 'deflate when content-encoding is identity' do options = { 'response_headers' => { - 'Content-Type' => 'text/plain', - 'Content-Encoding' => 'identity' + 'content-type' => 'text/plain', + 'content-encoding' => 'identity' } } verify(200, 'Hello World!', deflate_or_gzip, options) @@ -316,7 +364,7 @@ describe Rack::Deflater do it "deflate if content-type matches :include" do options = { 'response_headers' => { - 'Content-Type' => 'text/plain' + 'content-type' => 'text/plain' }, 'deflater_options' => { include: %w(text/plain) @@ -328,7 +376,7 @@ describe Rack::Deflater do it "deflate if content-type is included it :include" do options = { 'response_headers' => { - 'Content-Type' => 'text/plain; charset=us-ascii' + 'content-type' => 'text/plain; charset=us-ascii' }, 'deflater_options' => { include: %w(text/plain) @@ -349,7 +397,7 @@ describe Rack::Deflater do it "not deflate if content-type do not match :include" do options = { 'response_headers' => { - 'Content-Type' => 'text/plain' + 'content-type' => 'text/plain' }, 'deflater_options' => { include: %w(text/json) @@ -361,7 +409,7 @@ describe Rack::Deflater do it "not deflate if content-length is 0" do options = { 'response_headers' => { - 'Content-Length' => '0' + 'content-length' => '0' }, } verify(200, '', { 'gzip' => nil }, options) @@ -385,16 +433,16 @@ describe Rack::Deflater do verify(200, 'Hello World!', { 'gzip' => nil }, options) end - it "check for Content-Length via :if" do + it "check for content-length via :if" do response = 'Hello World!' response_len = response.length options = { 'response_headers' => { - 'Content-Length' => response_len.to_s + 'content-length' => response_len.to_s }, 'deflater_options' => { if: lambda { |env, status, headers, body| - headers['Content-Length'].to_i >= response_len + headers['content-length'].to_i >= response_len } } } @@ -417,9 +465,9 @@ describe Rack::Deflater do } 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' + 'content-encoding' => 'gzip', + 'vary' => 'Accept-Encoding', + 'content-type' => 'text/plain' }) buf = ''.dup @@ -435,4 +483,43 @@ describe Rack::Deflater do raw_bytes.must_be(:<, expect.bytesize) end end + + it 'will honor sync: false to avoid unnecessary flushing when deflating files' do + content = File.binread(__FILE__) + options = { + 'deflater_options' => { sync: false }, + 'app_body' => File.open(__FILE__), + 'skip_body_verify' => true, + } + verify(200, content, 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 + buf.must_equal content + raw_bytes.must_be(:<, content.bytesize) + end + end + + it 'does not close the response body prematurely' do + app_body = Class.new do + attr_reader :closed; + def each; yield('foo'); yield('bar'); end; + def close; @closed = true; end; + end.new + + verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body| + assert_nil app_body.closed + end + end end diff --git a/test/spec_directory.rb b/test/spec_directory.rb index 0e4d501fbcecbfa1ef22757e9c8fdc8a83f98396..fcd1f16cad06143bf7d458ab7a450d45ee207acd 100644 --- a/test/spec_directory.rb +++ b/test/spec_directory.rb @@ -4,9 +4,17 @@ require_relative 'helper' require 'tempfile' require 'fileutils' +separate_testing do + require_relative '../lib/rack/directory' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/utils' + require_relative '../lib/rack/builder' +end + 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 @@ -32,12 +40,34 @@ describe Rack::Directory do end end + it "serve root directory index" do + res = Rack::MockRequest.new(Rack::Lint.new(app)). + get("/") + + res.must_be :ok? + assert_includes(res.body, '<html><head>') + assert_includes(res.body, "href='cgi") + end + it "serve directory indices" do res = Rack::MockRequest.new(Rack::Lint.new(app)). get("/cgi/") res.must_be :ok? - assert_match(res, /<html><head>/) + assert_includes(res.body, '<html><head>') + assert_includes(res.body, "rackup_stub.rb") + end + + it "return 404 for pipes" do + begin + File.mkfifo('test/cgi/fifo') + res = Rack::MockRequest.new(Rack::Lint.new(app)). + get("/cgi/fifo") + + res.status.must_equal 404 + ensure + File.delete('test/cgi/fifo') + end end it "serve directory indices with bad symlinks" do @@ -119,6 +149,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") diff --git a/test/spec_etag.rb b/test/spec_etag.rb index 77c2dfc32002d7fdf49189d715ee609f7dec6481..e1670dfa577c60ca23c6f59d39e720ed3f24ef07 100644 --- a/test/spec_etag.rb +++ b/test/spec_etag.rb @@ -3,6 +3,12 @@ require_relative 'helper' require 'time' +separate_testing do + require_relative '../lib/rack/etag' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::ETag do def etag(app, *args) Rack::Lint.new Rack::ETag.new(app, *args) @@ -13,93 +19,91 @@ describe Rack::ETag do end def sendfile_body - res = ['Hello World'] - def res.to_path ; "/tmp/hello.txt" ; end - res + File.new(File::NULL) end - it "set ETag if none is set if status is 200" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } + it "set etag if none is set if status is 200" do + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app).call(request) - response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" + 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!"]] } + it "set etag if none is set if status is 201" do + app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app).call(request) - response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" + 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!"]] } + 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!"]] } response = etag(app).call(request) - response[1]['Cache-Control'].must_equal 'max-age=0, private, must-revalidate' + 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!"]] } + it "set cache-control to chosen one if none is set" do + app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] } response = etag(app, nil, 'public').call(request) - response[1]['Cache-Control'].must_equal 'public' + 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' }, []] } + it "set a given cache-control even if digest could not be calculated" do + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, []] } response = etag(app, 'no-cache').call(request) - response[1]['Cache-Control'].must_equal 'no-cache' + response[1]['cache-control'].must_equal 'no-cache' end - it "does not set a cache-control if it is already set" do - app = lambda { |env| [201, { 'Content-Type' => 'text/plain', 'cache-control' => 'public' }, ["Hello, World!"]] } + it "not set cache-control if it is already set" do + 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 it is already set" do - 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' + it "not set cache-control if directive isn't present" do + 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 set Cache-Control if directive isn't present" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] } - response = etag(app, nil, nil).call(request) - response[1]['Cache-Control'].must_be_nil + it "not change etag if it is already set" do + 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 change ETag if it is already set" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'ETag' => '"abc"' }, ["Hello, World!"]] } + it "not set etag if body is empty" do + app = lambda { |env| [200, { 'content-type' => 'text/plain', 'last-modified' => Time.now.httpdate }, []] } response = etag(app).call(request) - response[1]['ETag'].must_equal "\"abc\"" + response[1]['etag'].must_be_nil end - it "not set ETag if body is empty" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate }, []] } + it "set handle empty body parts" do + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello", "", ", World!"]] } response = etag(app).call(request) - response[1]['ETag'].must_be_nil + response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" 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!"]] } + 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!"]] } response = etag(app).call(request) - response[1]['ETag'].must_be_nil + 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] } + it "not set etag if a sendfile_body is given" do + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, sendfile_body] } response = etag(app).call(request) - response[1]['ETag'].must_be_nil + 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.']] } + it "not set etag if a status is not 200 or 201" do + app = lambda { |env| [401, { 'content-type' => 'text/plain' }, ['Access denied.']] } response = etag(app).call(request) - response[1]['ETag'].must_be_nil + response[1]['etag'].must_be_nil end - it "set ETag even if no-cache is given" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Cache-Control' => 'no-cache, must-revalidate' }, ['Hello, World!']] } + it "set etag even if no-cache is given" do + 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_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" + response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\"" end it "close the original body" do diff --git a/test/spec_events.rb b/test/spec_events.rb index e2077984d0f9c76861e174e42e83bd8d0c45bc23..e40d72be26fad02aa10c4ad6c43e7bd7db54c871 100644 --- a/test/spec_events.rb +++ b/test/spec_events.rb @@ -2,6 +2,10 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/events' +end + module Rack class TestEvents < Minitest::Test class EventMiddleware @@ -58,7 +62,7 @@ module Rack app = lambda { |env| events << [app, :call]; ret } se = EventMiddleware.new events e = Events.new app, [se] - triple = e.call({}) + e.call({}) assert_equal [[se, :on_start], [app, :call], [se, :on_commit], @@ -118,7 +122,6 @@ module Rack def test_finish_is_called_if_there_is_an_exception events = [] - ret = [200, {}, []] app = lambda { |env| raise } se = EventMiddleware.new events e = Events.new app, [se] diff --git a/test/spec_files.rb b/test/spec_files.rb index 898b0d9095b19d4c31812f3b087ad15befef901d..5b5a09d8004613349b55d75f34860822a4f713f8 100644 --- a/test/spec_files.rb +++ b/test/spec_files.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/files' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Files do DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT @@ -19,16 +25,7 @@ describe Rack::Files do ) file_path = File.expand_path("cgi/test", __dir__) - status, headers, body = app.serving(request, file_path) - assert_equal 200, status - end - - it 'raises if you attempt to define response_body in subclass' do - c = Class.new(Rack::Files) - - lambda do - c.send(:define_method, :response_body){} - end.must_raise RuntimeError + assert_equal 200, app.serving(request, file_path)[0] end it 'serves files with + in the file name' do @@ -58,13 +55,13 @@ describe Rack::Files do res.status.must_equal 404 end - it "set Last-Modified header" do + it "set last-modified header" do res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test") path = File.join(DOCROOT, "/cgi/test") res.must_be :ok? - res["Last-Modified"].must_equal File.mtime(path).httpdate + res["last-modified"].must_equal File.mtime(path).httpdate end it "return 304 if file isn't modified since last serve" do @@ -190,9 +187,23 @@ describe Rack::Files do 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/208" - res.body.must_equal "frozen_strin" + res["content-length"].must_equal "12" + res["content-range"].must_equal "bytes 22-33/209" + res.body.must_equal "IS FILE! ***" + end + + it "handle case where file is truncated during request" do + env = Rack::MockRequest.env_for("/cgi/test") + env["HTTP_RANGE"] = "bytes=0-3300" + files = Class.new(Rack::Files) do + def filesize(_); 10000 end + end.new(DOCROOT) + + res = Rack::MockResponse.new(*files.call(env)) + + res.status.must_equal 206 + res["content-length"].must_equal "209" + res["content-range"].must_equal "bytes 0-3300/10000" end it "return correct multiple byte ranges in body" do @@ -201,20 +212,20 @@ describe Rack::Files do res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 206 - res["Content-Length"].must_equal "191" - res["Content-Type"].must_equal "multipart/byteranges; boundary=AaB03x" + res["content-length"].must_equal "191" + res["content-type"].must_equal "multipart/byteranges; boundary=AaB03x" expected_body = <<-EOF \r --AaB03x\r -Content-Type: text/plain\r -Content-Range: bytes 22-33/208\r +content-type: text/plain\r +content-range: bytes 22-33/209\r \r -frozen_strin\r +IS FILE! ***\r --AaB03x\r -Content-Type: text/plain\r -Content-Range: bytes 60-80/208\r +content-type: text/plain\r +content-range: bytes 60-80/209\r \r -e.join(File.dirname(_\r +, tests will break!!!\r --AaB03x--\r EOF @@ -227,17 +238,17 @@ e.join(File.dirname(_\r res = Rack::MockResponse.new(*files(DOCROOT).call(env)) res.status.must_equal 416 - res["Content-Range"].must_equal "bytes */208" + res["content-range"].must_equal "bytes */209" end it "support custom http headers" do env = Rack::MockRequest.env_for("/cgi/test") - status, heads, _ = files(DOCROOT, 'Cache-Control' => 'public, max-age=38', - 'Access-Control-Allow-Origin' => '*').call(env) + status, heads, _ = files(DOCROOT, 'cache-control' => 'public, max-age=38', + 'access-control-allow-origin' => '*').call(env) status.must_equal 200 - heads['Cache-Control'].must_equal 'public, max-age=38' - heads['Access-Control-Allow-Origin'].must_equal '*' + heads['cache-control'].must_equal 'public, max-age=38' + heads['access-control-allow-origin'].must_equal '*' end it "support not add custom http headers if none are supplied" do @@ -245,8 +256,8 @@ e.join(File.dirname(_\r status, heads, _ = files(DOCROOT).call(env) status.must_equal 200 - heads['Cache-Control'].must_be_nil - heads['Access-Control-Allow-Origin'].must_be_nil + heads['cache-control'].must_be_nil + heads['access-control-allow-origin'].must_be_nil end it "only support GET, HEAD, and OPTIONS requests" do @@ -257,7 +268,7 @@ e.join(File.dirname(_\r res = req.send(method, "/cgi/test") res.must_be :client_error? res.must_be :method_not_allowed? - res.headers['Allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS) + res.headers['allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS) end allowed = %w[get head options] @@ -271,36 +282,36 @@ e.join(File.dirname(_\r req = Rack::MockRequest.new(files(DOCROOT)) res = req.options('/cgi/test') res.must_be :successful? - res.headers['Allow'].wont_equal nil - res.headers['Allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS) + res.headers['allow'].wont_equal nil + res.headers['allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS) end - it "set Content-Length correctly for HEAD requests" do + it "set content-length correctly for HEAD requests" do req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT))) res = req.head "/cgi/test" res.must_be :successful? - res['Content-Length'].must_equal "208" + res['content-length'].must_equal "209" end it "default to a mime type of text/plain" do 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" + 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::Files.new(DOCROOT, nil, 'application/octet-stream'))) res = req.get "/cgi/test" res.must_be :successful? - res['Content-Type'].must_equal "application/octet-stream" + res['content-type'].must_equal "application/octet-stream" end - it "not set Content-Type if the mime type is not set" do + it "not set content-type if the mime type is not set" do 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 + res['content-type'].must_be_nil end it "return error when file not found for head request" do diff --git a/test/spec_handler.rb b/test/spec_handler.rb deleted file mode 100644 index d6d9cccec38b39848c3e0a87c1994b4c53da1e57..0000000000000000000000000000000000000000 --- a/test/spec_handler.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' - -class Rack::Handler::Lobster; end -class RockLobster; end - -describe Rack::Handler do - it "has registered default handlers" do - Rack::Handler.get('cgi').must_equal Rack::Handler::CGI - Rack::Handler.get('webrick').must_equal Rack::Handler::WEBrick - - begin - Rack::Handler.get('fastcgi').must_equal Rack::Handler::FastCGI - rescue LoadError - end - end - - it "raise LoadError if handler doesn't exist" do - lambda { - Rack::Handler.get('boom') - }.must_raise(LoadError) - - lambda { - Rack::Handler.get('Object') - }.must_raise(LoadError) - end - - it "get unregistered, but already required, handler by name" do - Rack::Handler.get('Lobster').must_equal Rack::Handler::Lobster - end - - it "register custom handler" do - Rack::Handler.register('rock_lobster', 'RockLobster') - Rack::Handler.get('rock_lobster').must_equal RockLobster - end - - it "not need registration for properly coded handlers even if not already required" do - begin - $LOAD_PATH.push File.expand_path('../unregistered_handler', __FILE__) - Rack::Handler.get('Unregistered').must_equal Rack::Handler::Unregistered - lambda { Rack::Handler.get('UnRegistered') }.must_raise LoadError - Rack::Handler.get('UnregisteredLongOne').must_equal Rack::Handler::UnregisteredLongOne - ensure - $LOAD_PATH.delete File.expand_path('../unregistered_handler', __FILE__) - end - end - - it "allow autoloaded handlers to be registered properly while being loaded" do - path = File.expand_path('../registering_handler', __FILE__) - begin - $LOAD_PATH.push path - Rack::Handler.get('registering_myself').must_equal Rack::Handler::RegisteringMyself - ensure - $LOAD_PATH.delete path - end - end -end diff --git a/test/spec_head.rb b/test/spec_head.rb index d2dedd28167051033c69c207bacfe6d382458ff7..4ab90c2efd486937fe1f400094e749bb466eb51c 100644 --- a/test/spec_head.rb +++ b/test/spec_head.rb @@ -2,12 +2,18 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/head' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + 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) @@ -20,7 +26,7 @@ describe Rack::Head do resp, _ = test_response("REQUEST_METHOD" => type) resp[0].must_equal 200 - resp[1].must_equal "Content-type" => "test/plain", "Content-length" => "3" + resp[1].must_equal "content-type" => "test/plain", "content-length" => "3" resp[2].to_enum.to_a.must_equal ["foo"] end end @@ -29,14 +35,14 @@ describe Rack::Head do resp, _ = test_response("REQUEST_METHOD" => "HEAD") resp[0].must_equal 200 - resp[1].must_equal "Content-type" => "test/plain", "Content-length" => "3" + resp[1].must_equal "content-type" => "test/plain", "content-length" => "3" resp[2].to_enum.to_a.must_equal [] end it "close the body when it is removed" do resp, body = test_response("REQUEST_METHOD" => "HEAD") resp[0].must_equal 200 - resp[1].must_equal "Content-type" => "test/plain", "Content-length" => "3" + resp[1].must_equal "content-type" => "test/plain", "content-length" => "3" resp[2].to_enum.to_a.must_equal [] body.wont_be :closed? resp[2].close diff --git a/test/spec_headers.rb b/test/spec_headers.rb new file mode 100644 index 0000000000000000000000000000000000000000..f22680b77b9500c6ff348c41d433ce91e6691a0e --- /dev/null +++ b/test/spec_headers.rb @@ -0,0 +1,520 @@ +# frozen_string_literal: true + +require_relative 'helper' + +separate_testing do + require_relative '../lib/rack/headers' +end + +class RackHeadersTest < Minitest::Spec + before do + @h = Rack::Headers.new + @fh = Rack::Headers['AB'=>'1', 'cd'=>'2', '3'=>'4'] + end + + def test_public_interface + headers_methods = Rack::Headers.public_instance_methods.sort + hash_methods = Hash.public_instance_methods.sort + assert_empty(headers_methods - hash_methods) + assert_empty(hash_methods - headers_methods) + end + + def test_class_aref + assert_equal Hash[], Rack::Headers[] + assert_equal Hash['a'=>'2'], Rack::Headers['A'=>'2'] + assert_equal Hash['a'=>'2', 'b'=>'4'], Rack::Headers['A'=>'2', 'B'=>'4'] + assert_equal Hash['a','2','b','4'], Rack::Headers['A','2','B','4'] + assert_raises(ArgumentError){Rack::Headers['A']} + assert_raises(ArgumentError){Rack::Headers['A',2,'B']} + end + + def test_default_values + h, ch = Hash.new, Rack::Headers.new + assert_equal h, ch + h, ch = Hash.new('1'), Rack::Headers.new('1') + assert_equal h, ch + assert_equal h['3'], ch['3'] + h['a'], ch['A'] = ['2', '2'] + assert_equal h['a'], ch['a'] + h, ch = Hash.new{|h,k| k*2}, Rack::Headers.new{|h,k| k*2} + assert_equal h['3'], ch['3'] + h['c'], ch['C'] = ['2', '2'] + assert_equal h['c'], ch['c'] + assert_raises(ArgumentError){Rack::Headers.new('1'){|hash,k| key}} + + assert_nil @fh.default + assert_nil @fh.default_proc + assert_nil @fh['55'] + assert_equal '3', Rack::Headers.new('3').default + assert_nil Rack::Headers.new('3').default_proc + assert_equal '3', Rack::Headers.new('3')['1'] + + @fh.default = '4' + assert_equal '4', @fh.default + assert_nil @fh.default_proc + assert_equal '4', @fh['55'] + + h = Rack::Headers.new('5') + assert_equal '5', h.default + assert_nil h.default_proc + assert_equal '5', h['55'] + + h = Rack::Headers.new{|hash, key| '1234'} + assert_nil h.default + refute_equal nil, h.default_proc + assert_equal '1234', h['55'] + + h = Rack::Headers.new{|hash, key| hash[key] = '1234'; nil} + assert_nil h.default + refute_equal nil, h.default_proc + assert_nil h['Ac'] + assert_equal '1234', h['aC'] + end + + def test_store_and_retrieve + assert_nil @h['a'] + @h['A'] = '2' + assert_equal '2', @h['a'] + assert_equal '2', @h['A'] + @h['a'] = '3' + assert_equal '3', @h['a'] + assert_equal '3', @h['A'] + @h['AB'] = '5' + assert_equal '5', @h['ab'] + assert_equal '5', @h['AB'] + assert_equal '5', @h['aB'] + assert_equal '5', @h['Ab'] + @h.store('C', '8') + assert_equal '8', @h['c'] + assert_equal '8', @h['C'] + end + + def test_clear + assert_equal 3, @fh.length + @fh.clear + assert_equal Hash[], @fh + assert_equal 0, @fh.length + end + + def test_delete + assert_equal 3, @fh.length + assert_equal '1', @fh.delete('aB') + assert_equal 2, @fh.length + assert_nil @fh.delete('Ab') + assert_equal 2, @fh.length + end + + def test_delete_if_and_reject + assert_equal 3, @fh.length + hash = @fh.reject{|key, value| key == 'ab' || key == 'cd'} + assert_equal 1, hash.length + assert_equal Hash['3'=>'4'], hash + assert_equal 3, @fh.length + hash = @fh.delete_if{|key, value| key == 'ab' || key == 'cd'} + assert_equal 1, hash.length + assert_equal Hash['3'=>'4'], hash + assert_equal 1, @fh.length + assert_equal Hash['3'=>'4'], @fh + assert_nil @fh.reject!{|key, value| key == 'ab' || key == 'cd'} + hash = @fh.reject!{|key, value| key == '3'} + assert_equal 0, hash.length + assert_equal Hash[], hash + assert_equal 0, @fh.length + assert_equal Hash[], @fh + end + + def test_dup_and_clone + def @h.foo; 1; end + h2 = @h.dup + h3 = @h.clone + h2['A'] = '2' + h3['B'] = '3' + assert_equal Rack::Headers[], @h + assert_raises NoMethodError do h2.foo end + assert_equal 1, h3.foo + assert_equal '2', h2['a'] + assert_equal '3', h3['b'] + end + + def test_each + i = 0 + @h.each{i+=1} + assert_equal 0, i + items = [['ab','1'], ['cd','2'], ['3','4']] + @fh.each do |k,v| + assert items.include?([k,v]) + items -= [[k,v]] + end + assert_equal [], items + end + + def test_each_key + i = 0 + @h.each{i+=1} + assert_equal 0, i + keys = ['ab', 'cd', '3'] + @fh.each_key do |k| + assert keys.include?(k) + assert k.frozen? + keys -= [k] + end + assert_equal [], keys + end + + def test_each_value + i = 0 + @h.each{i+=1} + assert_equal 0, i + values = ['1', '2', '4'] + @fh.each_value do |v| + assert values.include?(v) + values -= [v] + end + assert_equal [], values + end + + def test_empty + assert @h.empty? + assert !@fh.empty? + end + + def test_fetch + assert_raises(ArgumentError){@h.fetch(1,2,3)} + assert_raises(ArgumentError){@h.fetch(1,2,3){4}} + assert_raises(IndexError){@h.fetch(1)} + @h.default = '33' + assert_raises(IndexError){@h.fetch(1)} + @h['1'] = '8' + assert_equal '8', @h.fetch('1') + assert_equal '3', @h.fetch(2, '3') + assert_equal '222', @h.fetch('2'){|k| k*3} + assert_equal '1', @fh.fetch('Ab') + assert_equal '2', @fh.fetch('cD', '3') + assert_equal '4', @fh.fetch("3", 3) + assert_equal '4', @fh.fetch("3"){|k| k*3} + assert_raises(IndexError){Rack::Headers.new{34}.fetch(1)} + end + + def test_has_key + %i'include? has_key? key? member?'.each do |meth| + assert !@h.send(meth,1) + assert @fh.send(meth,'Ab') + assert @fh.send(meth,'cD') + assert @fh.send(meth,'3') + assert @fh.send(meth,'ab') + assert @fh.send(meth,'CD') + assert @fh.send(meth,'3') + assert !@fh.send(meth,1) + end + end + + def test_has_value + %i'value? has_value?'.each do |meth| + assert !@h.send(meth,'1') + assert @fh.send(meth,'1') + assert @fh.send(meth,'2') + assert @fh.send(meth,'4') + assert !@fh.send(meth,'3') + end + end + + def test_inspect + %i'inspect to_s'.each do |meth| + assert_equal '{}', @h.send(meth) + assert_equal '{"ab"=>"1", "cd"=>"2", "3"=>"4"}', @fh.send(meth) + end + end + + def test_invert + assert_kind_of(Rack::Headers, @h.invert) + assert_equal({}, @h.invert) + assert_equal({"1"=>"ab", "2"=>"cd", "4"=>"3"}, @fh.invert) + assert_equal({'cd'=>'ab'}, Rack::Headers['AB'=>'CD'].invert) + assert_equal({'cd'=>'xy'}, Rack::Headers['AB'=>'Cd', 'xY'=>'cD'].invert) + end + + def test_keys + assert_equal [], @h.keys + assert_equal %w'ab cd 3', @fh.keys + end + + def test_length + %i'length size'.each do |meth| + assert_equal 0, @h.send(meth) + assert_equal 3, @fh.send(meth) + end + end + + def test_merge_and_update + assert_equal @h, @h.merge({}) + assert_equal @fh, @fh.merge({}) + assert_equal Rack::Headers['ab'=>'55'], @h.merge({'ab'=>'55'}) + assert_equal Rack::Headers[], @h + assert_equal Rack::Headers['ab'=>'55'], @h.update({'ab'=>'55'}) + assert_equal Rack::Headers['ab'=>'55'], @h + assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh.merge({'ab'=>'55'}) + assert_equal Rack::Headers['ab'=>'1', 'cd'=>'2', '3'=>'4'], @fh + assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'55'}) + assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh + assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh.merge({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join} + assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh + assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh.update({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join} + assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh + assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join} + assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh + end + + def test_replace + h = @h.dup + fh = @fh.dup + h1 = fh.replace(@h) + assert_equal @h, h1 + assert_same fh, h1 + + h2 = h.replace(@fh) + assert_equal @fh, h2 + assert_same h, h2 + + assert_equal @h, fh.replace({}) + assert_equal @fh, h.replace('AB'=>'1', 'cd'=>'2', '3'=>'4') + end + + def test_select + assert_equal({}, @h.select{true}) + assert_equal({}, @h.select{false}) + assert_equal({'3' => '4', "ab" => '1', 'cd' => '2'}, @fh.select{true}) + assert_equal({}, @fh.select{false}) + assert_equal({'cd' => '2'}, @fh.select{|k,v| k.start_with?('c')}) + assert_equal({'3' => '4'}, @fh.select{|k,v| v == '4'}) + end + + def test_shift + assert_nil @h.shift + array = @fh.to_a + i = 3 + while true + assert i >= 0 + kv = @fh.shift + if kv.nil? + assert_equal [], array + break + else + i -= 1 + assert array.include?(kv) + array -= [kv] + end + end + assert_equal [], array + assert_equal 0, i + end + + def test_sort + assert_equal [], @h.sort + assert_equal [], @h.sort{|a,b| a.to_s<=>b.to_s} + assert_equal [['ab', '1'], ['cd', '4'], ['ef', '2']], Rack::Headers['CD','4','AB','1','EF','2'].sort + assert_equal [['3', '4'], ['ab', '1'], ['cd', '2']], @fh.sort{|(ak,av),(bk,bv)| ak.to_s<=>bk.to_s} + end + + def test_to_a + assert_equal [], @h.to_a + assert_equal [['ab', '1'], ['cd', '2'], ['3', '4']], @fh.to_a + end + + def test_to_hash + assert_equal Hash[], @h.to_hash + assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_hash + end + + def test_values + assert_equal [], @h.values + assert_equal ['f', 'c'], Rack::Headers['aB','f','1','c'].values + end + + def test_values_at + assert_equal [], @h.values_at() + assert_equal [nil], @h.values_at(1) + assert_equal [nil, nil], @h.values_at(1, 1) + assert_equal [], @fh.values_at() + assert_equal ['1'], @fh.values_at('AB') + assert_equal ['2', '1'], @fh.values_at('CD', 'Ab') + assert_equal ['2', nil, '1'], @fh.values_at('CD', 32, 'aB') + assert_equal ['4', '2', nil, '1'], @fh.values_at('3', 'CD', 32, 'ab') + end + + def test_assoc + assert_nil @h.assoc(1) + assert_equal ['ab', '1'], @fh.assoc('Ab') + assert_equal ['cd', '2'], @fh.assoc('CD') + assert_nil @fh.assoc('4') + assert_equal ['3', '4'], @fh.assoc('3') + end + + def test_default_proc= + @h.default_proc = proc{|h, k| k * 2} + assert_equal 'aa', @h['A'] + @h['Ab'] = '2' + assert_equal '2', @h['aB'] + end + + def test_flatten + assert_equal [], @h.flatten + assert_equal ['ab', '1', 'cd', '2', '3', '4'], @fh.flatten + @fh['X'] = '56' + assert_equal ['ab', '1', 'cd', '2', '3', '4', 'x', '56'], @fh.flatten + assert_equal ['ab', '1', 'cd', '2', '3', '4', 'x', '56'], @fh.flatten(2) + end + + def test_keep_if + assert_equal @h, @h.keep_if{|k, v| true} + assert_equal @fh, @fh.keep_if{|k, v| true} + assert_equal @h, @fh.dup.keep_if{|k, v| false} + assert_equal Rack::Headers["AB"=>'1'], @fh.keep_if{|k, v| k == "ab"} + end + + def test_key + assert_nil @h.key('1') + assert_nil @fh.key(1) + assert_equal 'ab', @fh.key('1') + assert_equal 'cd', @fh.key('2') + assert_nil @fh.key('3') + assert_equal '3', @fh.key('4') + end + + def test_rassoc + assert_nil @h.rassoc('1') + assert_equal ['ab', '1'], @fh.rassoc('1') + assert_equal ['cd', '2'], @fh.rassoc('2') + assert_nil @fh.rassoc('3') + assert_equal ['3', '4'], @fh.rassoc('4') + end + + def test_select! + assert_nil @h.select!{|k, v| true} + assert_nil @fh.select!{|k, v| true} + assert_equal @h, @fh.dup.select!{|k, v| false} + assert_equal Rack::Headers["AB"=>'1'], @fh.select!{|k, v| k == "ab"} + end + + def test_compare_by_identity + assert_raises(TypeError){@fh.compare_by_identity} + end + + def test_compare_by_identity? + assert_equal(false, @fh.compare_by_identity?) + end + + def test_to_h + assert_equal Hash[], @h.to_h + assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_h + end + + def test_dig + assert_equal('1', @fh.dig('AB')) + assert_equal('2', @fh.dig('Cd')) + assert_equal('4', @fh.dig('3')) + assert_nil(@fh.dig('4')) + + assert_raises(TypeError){@fh.dig('AB', 1)} + assert_raises(TypeError){@fh.dig('cd', 2)} + assert_raises(TypeError){@fh.dig('3', 3)} + assert_nil(@fh.dig('4', 5)) + end + + def test_fetch_values + assert_equal(['1'], @fh.fetch_values('AB')) + assert_equal(['1', '2', '4'], @fh.fetch_values('AB', 'Cd', '3')) + assert_raises(KeyError){@fh.fetch_values('AB', 'cD', '4')} + end + + def test_to_proc + pr = @fh.to_proc + assert_equal('1', pr['AB']) + assert_equal('2', pr['cD']) + assert_equal('4', pr['3']) + assert_nil(pr['4']) + end + + def test_compact + assert_equal(false, @fh.compact.equal?(@fh)) + assert_equal(@fh, @fh.compact) + assert_equal(Rack::Headers['Ab'=>1], Rack::Headers['aB'=>1, 'cd'=>nil].compact) + end + + def test_compact! + fh = @fh.dup + assert_nil(@fh.compact!) + assert_equal(fh, @fh) + + h = Rack::Headers['Ab'=>1, 'cd'=>nil] + assert_equal(Rack::Headers['aB'=>1], h.compact!) + assert_equal(Rack::Headers['AB'=>1], h) + end + + def test_transform_values + fh = @fh.transform_values{|v| v.to_s*2} + assert_equal('1', @fh['aB']) + assert_equal(Rack::Headers['AB'=>'11', 'cD'=>'22', '3'=>'44'], fh) + assert_equal('11', fh['Ab']) + end + + def test_transform_values! + @fh.transform_values!{|v| v.to_s*2} + assert_equal('11', @fh['AB']) + assert_equal(Rack::Headers['Ab'=>'11', 'CD'=>'22', '3'=>'44'], @fh) + assert_equal('11', @fh['aB']) + end + + if RUBY_VERSION >= '2.5' + def test_slice + assert_equal(Rack::Headers['Ab'=>'1', 'cD'=>'2', '3'=>'4'], @fh.slice('aB', 'Cd', '3')) + assert_equal(Rack::Headers['AB'=>'1', 'CD'=>'2'], @fh.slice('Ab', 'CD')) + assert_equal(Rack::Headers[], @fh.slice('ad')) + assert_equal('1', @fh.slice('AB', 'cd')['Ab']) + end + + def test_transform_keys + map = {'ab'=>'Xy', 'cd'=>'dC', '3'=>'5'} + dh = @fh.dup + fh = @fh.transform_keys{|k| map[k]} + assert_equal(dh, @fh) + assert_equal('1', fh['xY']) + assert_equal('2', fh['Dc']) + assert_equal('4', fh['5']) + end + + def test_transform_keys! + map = {'ab'=>'Xy', 'cd'=>'dC', '3'=>'5'} + dh = @fh.dup + @fh.transform_keys!{|k| map[k]} + assert_equal(false, dh == @fh) + assert_equal('1', @fh['xY']) + assert_equal('2', @fh['DC']) + assert_equal('4', @fh['5']) + end + end + + if RUBY_VERSION >= '2.6' + def test_filter! + assert_nil @h.filter!{|k, v| true} + assert_nil @fh.filter!{|k, v| true} + assert_equal @h, @fh.dup.filter!{|k, v| false} + assert_equal Rack::Headers["AB"=>'1'], @fh.filter!{|k, v| k == "ab"} + end + end + + if RUBY_VERSION >= '2.7' + def test_deconstruct_keys + assert_equal(@fh.to_hash, @fh.deconstruct_keys([])) + assert_equal(Rack::Headers, @fh.deconstruct_keys([]).class) + end + end + + if RUBY_VERSION >= '3.0' + def test_except + @fh = Rack::Headers['AB'=>'1', 'Cd'=>'2', '3'=>'4'] + assert_equal(@fh, @fh.except) + assert_equal(Rack::Headers['cD'=>'2', '3'=>'4'], @fh.except('AB', 5)) + assert_equal(Rack::Headers['AB'=>'1'], @fh.except('cD', '3')) + end + end +end diff --git a/test/spec_lint.rb b/test/spec_lint.rb old mode 100644 new mode 100755 index 5df61435d87210ce75c2caa42107a9ca3e349746..398c7719c9e0c1f1fed8b82b54083c7d565d1572 --- a/test/spec_lint.rb +++ b/test/spec_lint.rb @@ -3,15 +3,22 @@ require_relative 'helper' require 'tempfile' +separate_testing do + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Lint do + valid_app = lambda do |env| + [200, { "content-type" => "test/plain", "content-length" => "3" }, ["foo"]] + end + def env(*args) Rack::MockRequest.env_for("/", *args) end it "pass valid request" do - Rack::Lint.new(lambda { |env| - [200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]] - }).call(env({})).first.must_equal 200 + Rack::Lint.new(valid_app).call(env({})).first.must_equal 200 end it "notice fatal errors" do @@ -26,7 +33,6 @@ describe Rack::Lint do lambda { Rack::Lint.new(nil).call({}.freeze) }.must_raise(Rack::Lint::LintError). message.must_match(/env should not be frozen, but is/) - lambda { e = env e.delete("REQUEST_METHOD") @@ -41,6 +47,26 @@ describe Rack::Lint do }.must_raise(Rack::Lint::LintError). message.must_match(/missing required key SERVER_NAME/) + lambda { + e = env + e.delete("SERVER_PROTOCOL") + Rack::Lint.new(nil).call(e) + }.must_raise(Rack::Lint::LintError). + message.must_match(/missing required key SERVER_PROTOCOL/) + + lambda { + e = env + e["SERVER_PROTOCOL"] = 'Foo' + Rack::Lint.new(nil).call(e) + }.must_raise(Rack::Lint::LintError). + message.must_match(/env\[SERVER_PROTOCOL\] does not match HTTP/) + + lambda { + e = env + e["HTTP_VERSION"] = 'HTTP/1.0' + Rack::Lint.new(nil).call(e) + }.must_raise(Rack::Lint::LintError). + message.must_match(/env\[HTTP_VERSION\] does not equal env\[SERVER_PROTOCOL\]/) lambda { Rack::Lint.new(nil).call(env("HTTP_CONTENT_TYPE" => "text/plain")) @@ -57,11 +83,6 @@ describe Rack::Lint do }.must_raise(Rack::Lint::LintError). message.must_match(/non-string value/) - lambda { - Rack::Lint.new(nil).call(env("rack.version" => "0.2")) - }.must_raise(Rack::Lint::LintError). - message.must_match(/must be an Array/) - lambda { Rack::Lint.new(nil).call(env("rack.url_scheme" => "gopher")) }.must_raise(Rack::Lint::LintError). @@ -72,6 +93,8 @@ describe Rack::Lint do }.must_raise(Rack::Lint::LintError). message.must_equal "session [] must respond to store and []=" + Rack::Lint.new(valid_app).call(env("rack.session" => {}))[0].must_equal 200 + lambda { Rack::Lint.new(nil).call(env("rack.session" => {}.freeze)) }.must_raise(Rack::Lint::LintError). @@ -133,11 +156,16 @@ describe Rack::Lint do }.must_raise(Rack::Lint::LintError). message.must_equal "logger [] must respond to fatal" + def obj.fatal(*) end + Rack::Lint.new(valid_app).call(env("rack.logger" => obj))[0].must_equal 200 + lambda { Rack::Lint.new(nil).call(env("rack.multipart.buffer_size" => 0)) }.must_raise(Rack::Lint::LintError). message.must_equal "rack.multipart.buffer_size must be an Integer > 0 if specified" + Rack::Lint.new(valid_app).call(env("rack.multipart.buffer_size" => 1))[0].must_equal 200 + lambda { Rack::Lint.new(nil).call(env("rack.multipart.tempfile_factory" => Tempfile)) }.must_raise(Rack::Lint::LintError). @@ -158,6 +186,21 @@ describe Rack::Lint do }.must_raise(Rack::Lint::LintError). message.must_equal "response array has 0 elements instead of 3" + lambda { + Rack::Lint.new(nil).call(env("SERVER_PORT" => "howdy")) + }.must_raise(Rack::Lint::LintError). + message.must_equal 'env[SERVER_PORT] is not an Integer' + + lambda { + Rack::Lint.new(nil).call(env("SERVER_NAME" => "\u1234")) + }.must_raise(Rack::Lint::LintError). + message.must_equal "\u1234 must be a valid authority" + + lambda { + Rack::Lint.new(nil).call(env("HTTP_HOST" => "\u1234")) + }.must_raise(Rack::Lint::LintError). + message.must_equal "\u1234 must be a valid authority" + lambda { Rack::Lint.new(nil).call(env("REQUEST_METHOD" => "FUCKUP?")) }.must_raise(Rack::Lint::LintError). @@ -209,6 +252,16 @@ describe Rack::Lint do Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "/")) }.must_raise(Rack::Lint::LintError). message.must_match(/cannot be .* make it ''/) + + lambda { + Rack::Lint.new(nil).call(env("rack.response_finished" => "not a callable")) + }.must_raise(Rack::Lint::LintError). + message.must_match(/rack.response_finished must be an array of callable objects/) + + lambda { + Rack::Lint.new(nil).call(env("rack.response_finished" => [-> (env) {}, "not a callable"])) + }.must_raise(Rack::Lint::LintError). + message.must_match(/rack.response_finished values must respond to call/) end it "notice input errors" do @@ -271,26 +324,42 @@ describe Rack::Lint do ["cc", {}, ""] }).call(env({})) }.must_raise(Rack::Lint::LintError). - message.must_match(/must be >=100 seen as integer/) + message.must_match(/must be an Integer >=100/) lambda { Rack::Lint.new(lambda { |env| [42, {}, ""] }).call(env({})) }.must_raise(Rack::Lint::LintError). - message.must_match(/must be >=100 seen as integer/) + message.must_match(/must be an Integer >=100/) + + lambda { + Rack::Lint.new(lambda { |env| + ["200", {}, ""] + }).call(env({})) + }.must_raise(Rack::Lint::LintError). + message.must_match(/must be an Integer >=100/) end it "notice header errors" do + obj = Object.new + def obj.each; end lambda { io = StringIO.new('a') io.binmode Rack::Lint.new(lambda { |env| env['rack.input'].each{ |x| } - [200, Object.new, []] + [200, obj, []] }).call(env({ "rack.input" => io })) }.must_raise(Rack::Lint::LintError). - message.must_equal "headers object should respond to #each, but doesn't (got Object as headers)" + message.must_equal "headers object should be a hash, but isn't (got Object as headers)" + lambda { + Rack::Lint.new(lambda { |env| + [200, {}.freeze, []] + }).call(env({})) + }.must_raise(Rack::Lint::LintError). + message.must_equal "headers object should not be frozen, but is" + lambda { Rack::Lint.new(lambda { |env| @@ -301,17 +370,18 @@ describe Rack::Lint do 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/) + message.must_match(/must not contain status/) # From RFC 7230:<F24><F25> # Most HTTP header field values are defined using common syntax # components (token, quoted-string, and comment) separated by # whitespace or specific delimiting characters. Delimiters are chosen # from the set of US-ASCII visual characters not allowed in a token - # (DQUOTE and "(),/:;<=>?@[\]{}"). + # (DQUOTE and "(),/:;<=>?@[\]{}"). Rack also doesn't allow uppercase + # ASCII (A-Z) in header keys. # # token = 1*tchar # @@ -328,7 +398,15 @@ describe Rack::Lint do }.must_raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}"). message.must_equal("invalid header name: #{invalid_header}") end - valid_headers = 0.upto(127).map(&:chr) - invalid_headers + ('A'..'Z').each do |invalid_header| + lambda { + Rack::Lint.new(lambda { |env| + [200, { invalid_header => "text/plain" }, []] + }).call(env({})) + }.must_raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}"). + message.must_equal("uppercase character in header name: #{invalid_header}") + end + valid_headers = 0.upto(127).map(&:chr) - invalid_headers - ('A'..'Z').to_a valid_headers.each do |valid_header| Rack::Lint.new(lambda { |env| [200, { valid_header => "text/plain" }, []] @@ -337,52 +415,42 @@ describe Rack::Lint do 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" + message.must_equal "a header value must be a String or Array of Strings, but the value of 'foo' is a Object" lambda { Rack::Lint.new(lambda { |env| - [200, { "Foo" => [1, 2, 3] }, []] + [200, { "foo-bar" => "text\000plain" }, []] }).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" - + message.must_match(/invalid header/) lambda { Rack::Lint.new(lambda { |env| - [200, { "Foo-Bar" => "text\000plain" }, []] + [200, [%w(content-type text/plain), %w(content-length 0)], []] }).call(env({})) }.must_raise(Rack::Lint::LintError). - message.must_match(/invalid header/) + message.must_equal "headers object should be a hash, but isn't (got Array as headers)" - # 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" }, []] - }).call(env({})).first.must_equal 200 - - # non-Hash header responses.must_be :allowed? - Rack::Lint.new(lambda { |env| - [200, [%w(Content-Type text/plain), %w(Content-Length 0)], []] - }).call(env({})).first.must_equal 200 end it "notice content-type errors" do # lambda { # Rack::Lint.new(lambda { |env| - # [200, {"Content-length" => "0"}, []] + # [200, {"content-length" => "0"}, []] # }).call(env({})) # }.must_raise(Rack::Lint::LintError). - # message.must_match(/No Content-Type/) + # message.must_match(/No content-type/) [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/) + message.must_match(/content-type header found/) end end @@ -390,35 +458,186 @@ 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/) + message.must_match(/content-length header found/) end 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/) + message.must_match(/content-length header was 1, but should be 0/) + end + + it "responds to to_path" do + body = Object.new + def body.each; end + def body.to_path; __FILE__ end + app = lambda { |env| [200, {}, body] } + + body = Rack::Lint.new(app).call(env({}))[2] + body.must_respond_to(:to_path) + body.to_path.must_equal __FILE__ end it "notice body errors" do lambda { body = Rack::Lint.new(lambda { |env| - [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). message.must_match(/yielded non-string/) + + lambda { + body = Rack::Lint.new(lambda { |env| + [200, { "content-type" => "text/plain", "content-length" => "3" }, Object.new] + }).call(env({}))[2] + body.respond_to?(:to_ary).must_equal false + body.each { |part| } + }.must_raise(Rack::Lint::LintError). + message.must_equal 'Enumerable Body must respond to each' + + lambda { + body = Rack::Lint.new(lambda { |env| + [200, { "content-type" => "text/plain", "content-length" => "0" }, []] + }).call(env({}))[2] + body.each { |part| } + body.each { |part| } + }.must_raise(Rack::Lint::LintError). + message.must_equal 'Response body must only be invoked once (each)' + + # Lint before and after the Rack middleware being tested. + def stacked_lint(app) + Rack::Lint.new(lambda do |env| + Rack::Lint.new(app).call(env).tap {|response| response[2] = yield response[2]} + end) + end + + yielder_app = lambda do |_| + input = Object.new + def input.each; 10.times {yield 'foo'}; end + [200, {"content-type" => "text/plain", "content-length" => "30"}, input] + end + + lambda { + body = stacked_lint(yielder_app) {|body| + new_body = Struct.new(:body) do + def each(&block) + body.each { |part| yield part.upcase } + body.close + end + end + new_body.new(body) + }.call(env({}))[2] + body.each {|part| part.must_equal 'FOO'} + body.close + }.call + + lambda { + body = stacked_lint(yielder_app) { |body| + body.enum_for.to_a + }.call(env({}))[2] + body.each {} + body.close + }.must_raise(Rack::Lint::LintError). + message.must_match(/Middleware must not call #each directly/) + + lambda { + body = stacked_lint(yielder_app) { |body| + new_body = Struct.new(:body) do + def each(&block) + body.enum_for.each_slice(2) { |parts| yield parts.join } + end + end + new_body.new(body) + }.call(env({}))[2] + body.each {} + body.close + }.must_raise(Rack::Lint::LintError). + message.must_match(/New body must yield at least once per iteration of old body/) + + lambda { + body = stacked_lint(yielder_app) { |body| + Struct.new(:body) do + def each; body.each {|part| yield part} end + end.new(body) + }.call(env({}))[2] + body.each {} + body.close + }.must_raise(Rack::Lint::LintError). + message.must_match(/Body has not been closed/) + + static_app = lambda do |_| + input = ['foo'] * 10 + [200, {"content-type" => "text/plain", "content-length" => "30"}, input] + end + + lambda { + body = stacked_lint(static_app) { |body| body.to_ary}.call(env({}))[2] + body.each {} + body.close + }.call + + array_mismatch = lambda do |_| + input = Object.new + def input.to_ary; ['bar'] * 10; end + def input.each; 10.times {yield 'foo'}; end + [200, {"content-type" => "text/plain", "content-length" => "30"}, input] + end + + lambda { + body = stacked_lint(array_mismatch) { |body| body.to_ary}.call(env({}))[2] + body.each {} + body.close + }.must_raise(Rack::Lint::LintError). + message.must_match(/#to_ary not identical to contents produced by calling #each/) + + lambda { + body = Rack::Lint.new(lambda { |env| + to_path = Object.new + def to_path.each; end + def to_path.to_path; 'non-existent' end + [200, { "content-type" => "text/plain", "content-length" => "0" }, to_path] + }).call(env({}))[2] + body.each { |part| } + }.must_raise(Rack::Lint::LintError). + message.must_equal 'The file identified by body.to_path does not exist' + + lambda { + body = Rack::Lint.new(lambda { |env| + [200, { "content-type" => "text/plain", "content-length" => "0" }, Object.new] + }).call(env({}))[2] + body.call(nil) + }.must_raise(Rack::Lint::LintError). + message.must_equal 'Streaming Body must respond to call' + + lambda { + body = Rack::Lint.new(lambda { |env| + [200, { "content-type" => "text/plain", "content-length" => "0" }, proc{}] + }).call(env({}))[2] + body.call(StringIO.new) + body.call(nil) + }.must_raise(Rack::Lint::LintError). + message.must_equal 'Response body must only be invoked once (call)' + + lambda { + body = Rack::Lint.new(lambda { |env| + [200, { "content-type" => "text/plain", "content-length" => "0" }, proc{}] + }).call(env({}))[2] + body.call(nil) + }.must_raise(Rack::Lint::LintError). + message.must_equal 'Stream must respond to read' end it "notice input handling errors" 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/) @@ -427,7 +646,7 @@ describe Rack::Lint do Rack::Lint.new(lambda { |env| env["rack.input"].gets 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/) @@ -435,7 +654,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/) @@ -443,7 +662,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/) @@ -451,7 +670,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/) @@ -459,19 +678,11 @@ 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/) - lambda { - Rack::Lint.new(lambda { |env| - env["rack.input"].rewind(0) - [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] - }).call(env({})) - }.must_raise(Rack::Lint::LintError). - message.must_match(/rewind called with arguments/) - weirdio = Object.new class << weirdio def gets @@ -486,10 +697,6 @@ describe Rack::Lint do yield 23 yield 42 end - - def rewind - raise Errno::ESPIPE, "Errno::ESPIPE" - end end eof_weirdio = Object.new @@ -504,23 +711,28 @@ describe Rack::Lint do def each end - - def rewind - end end 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/) + lambda { + Rack::Lint.new(lambda { |env| + env["rack.input"].each(1) { |x| } + [201, { "content-type" => "text/plain", "content-length" => "0" }, []] + }).call(env("rack.input" => weirdio)) + }.must_raise(Rack::Lint::LintError). + message.must_match(/rack.input#each called with arguments/) + 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/) @@ -528,7 +740,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/) @@ -536,34 +748,28 @@ 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/) + end - lambda { - Rack::Lint.new(lambda { |env| - env["rack.input"].rewind - [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/) + it "can call close" do + app = lambda do |env| + env["rack.input"].close + [201, {"content-type" => "text/plain", "content-length" => "0"}, []] + end + response = Rack::Lint.new(app).call(env({})) - lambda { - Rack::Lint.new(lambda { |env| - env["rack.input"].close - [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] - }).call(env({})) - }.must_raise(Rack::Lint::LintError). - message.must_match(/close must not be called/) + response.first.must_equal 201 end it "notice error handling errors" 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/) @@ -571,7 +777,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/) @@ -579,12 +785,12 @@ describe Rack::Lint do it "notice HEAD errors" do Rack::Lint.new(lambda { |env| - [200, { "Content-type" => "test/plain", "Content-length" => "3" }, []] + [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"]] + [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/) @@ -596,7 +802,7 @@ describe Rack::Lint do Rack::Lint.new(lambda { |env| env["rack.input"].send(:read, *args) - [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] + [201, { "content-type" => "text/plain", "content-length" => "0" }, []] }).call(env({ "rack.input" => StringIO.new(hello_str) })). first.must_equal 201 end @@ -610,36 +816,33 @@ describe Rack::Lint do assert_lint 1, ''.dup end - it "notice hijack errors" do + it "notices when request env doesn't have a valid rack.hijack callback" do lambda { Rack::Lint.new(lambda { |env| env['rack.hijack'].call - [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] - }).call(env({ 'rack.hijack?' => true, 'rack.hijack' => lambda { Object.new } })) + [201, { "content-type" => "text/plain", "content-length" => "0" }, []] + }).call(env({ 'rack.hijack' => Object.new })) }.must_raise(Rack::Lint::LintError). - message.must_match(/rack.hijack_io must respond to read/) - - Rack::Lint.new(lambda { |env| - env['rack.hijack'].call - [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []] - }).call(env({ 'rack.hijack?' => true, 'rack.hijack' => lambda { StringIO.new }, 'rack.hijack_io' => StringIO.new })). - first.must_equal 201 + message.must_match(/rack.hijack must respond to call/) + end + it "notices when the response headers don't have a valid rack.hijack callback" do + lambda { Rack::Lint.new(lambda { |env| - env['rack.hijack?'] = true - [201, { "Content-type" => "text/plain", "Content-length" => "0", 'rack.hijack' => lambda {|io| io }, 'rack.hijack_io' => StringIO.new }, []] - }).call(env({}))[1]['rack.hijack'].call(StringIO.new).read.must_equal '' + [201, { "content-type" => "text/plain", "content-length" => "0", 'rack.hijack' => Object.new }, []] + }).call(env({ 'rack.hijack?' => true })) + }.must_raise(Rack::Lint::LintError). + message.must_equal 'rack.hijack header must respond to #call' end -end + it "pass valid rack.response_finished" do + callable_object = Class.new do + def call(env, status, headers, error) + end + end.new -describe "Rack::Lint::InputWrapper" do - it "delegate :rewind to underlying IO object" do - io = StringIO.new("123") - wrapper = Rack::Lint::InputWrapper.new(io) - wrapper.read.must_equal "123" - wrapper.read.must_equal "" - wrapper.rewind - wrapper.read.must_equal "123" + Rack::Lint.new(lambda { |env| + [200, {}, ["foo"]] + }).call(env({ "rack.response_finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200 end end diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb deleted file mode 100644 index ac3f11934c258b4e29e7dfebe50b58e6bbe1e8e7..0000000000000000000000000000000000000000 --- a/test/spec_lobster.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'rack/lobster' - -module LobsterHelpers - def lobster - Rack::MockRequest.new Rack::Lint.new(Rack::Lobster.new) - end - - def lambda_lobster - Rack::MockRequest.new Rack::Lint.new(Rack::Lobster::LambdaLobster) - end -end - -describe Rack::Lobster::LambdaLobster do - include LobsterHelpers - - it "be a single lambda" do - Rack::Lobster::LambdaLobster.must_be_kind_of Proc - end - - it "look like a lobster" do - res = lambda_lobster.get("/") - res.must_be :ok? - res.body.must_include "(,(,,(,,,(" - res.body.must_include "?flip" - end - - it "be flippable" do - res = lambda_lobster.get("/?flip") - res.must_be :ok? - res.body.must_include "(,,,(,,(,(" - end -end - -describe Rack::Lobster do - include LobsterHelpers - - it "look like a lobster" do - res = lobster.get("/") - res.must_be :ok? - res.body.must_include "(,(,,(,,,(" - res.body.must_include "?flip" - res.body.must_include "crash" - end - - it "be flippable" do - res = lobster.get("/?flip=left") - res.must_be :ok? - res.body.must_include "),,,),,),)" - end - - it "provide crashing for testing purposes" do - lambda { - lobster.get("/?flip=crash") - }.must_raise RuntimeError - end -end diff --git a/test/spec_lock.rb b/test/spec_lock.rb index 895704986b6c75ae2b147d036ca740499520628c..dfc07449c9062dd343e5bf4cf85fe4b7f2326f46 100644 --- a/test/spec_lock.rb +++ b/test/spec_lock.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/lock' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/lint' +end + class Lock attr_reader :synchronized @@ -43,7 +49,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 } @@ -57,7 +63,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 @@ -69,7 +75,7 @@ describe Rack::Lock do 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 @@ -78,13 +84,13 @@ describe Rack::Lock do it 'call super on close' do env = Rack::MockRequest.env_for("/") - response = Class.new { + response = Class.new do attr_accessor :close_called def initialize; @close_called = false; end def close; @close_called = true; end - }.new + 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 @@ -95,7 +101,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 @@ -105,7 +111,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) @@ -117,7 +123,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 @@ -139,28 +145,6 @@ describe Rack::Lock do lock.synchronized.must_equal false end - 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 }] - }, false) - env = Rack::MockRequest.env_for("/") - env['rack.multithread'].must_equal true - _, _, body = app.call(env) - body.close - env['rack.multithread'].must_equal true - end - - it "reset original multithread flag when exiting lock" do - app = Class.new(Rack::Lock) { - def call(env) - env['rack.multithread'].must_equal true - super - end - }.new(lambda { |env| [200, { "Content-Type" => "text/plain" }, %w{ a b c }] }) - Rack::Lint.new(app).call(Rack::MockRequest.env_for("/")) - end - it 'not unlock if an error is raised before the mutex is locked' do lock = Class.new do def initialize() @unlocked = false end @@ -169,21 +153,11 @@ 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 - it "not reset the environment while the body is proxied" do - proxy = Class.new 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)] } - response = app.call(Rack::MockRequest.env_for("/"))[2] - response.env['rack.multithread'].must_equal false - end - it "unlock if an exception occurs before returning" do lock = Lock.new env = Rack::MockRequest.env_for("/") @@ -194,7 +168,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 8355fc828485a2a118e1618121f6b18f5cef9016..ab20d3638715fb52426f7b7ac3d6d38f619fb034 100644 --- a/test/spec_logger.rb +++ b/test/spec_logger.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/logger' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Logger do app = lambda { |env| log = env['rack.logger'] @@ -9,7 +15,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 a00a767e04a7a09ab2a59a1a12d47396b15ef6ab..1e701f25ce0056d24c7bfa86e5cbac893d599524 100644 --- a/test/spec_media_type.rb +++ b/test/spec_media_type.rb @@ -2,6 +2,10 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/media_type' +end + describe Rack::MediaType do before { @empty_hash = {} } diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb index ddb105bdfce0883c581c2db21db8eb6ab78b287a..f3b8ad729f790441098b6d5081ca40fcfc627192 100644 --- a/test/spec_method_override.rb +++ b/test/spec_method_override.rb @@ -2,10 +2,16 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/method_override' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + 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 @@ -94,10 +100,10 @@ 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/ + env[Rack::RACK_ERRORS].read.must_include 'Bad request content body' end it "not modify REQUEST_METHOD for POST requests when the params are unparseable because too deep" do @@ -113,4 +119,15 @@ EOF env["REQUEST_METHOD"].must_equal "POST" end + + it "not set form input when the content type is JSON" do + env = Rack::MockRequest.env_for("/", + "CONTENT_TYPE" => "application/json", + method: "POST", + input: '{"_method":"options"}') + app.call env + + env["REQUEST_METHOD"].must_equal "POST" + env["rack.request.form_input"].must_be_nil + end end diff --git a/test/spec_mime.rb b/test/spec_mime.rb index 65a77f6f0f0294d1b4965656c2ac5427b762a5ea..4a8ff7c97a2669732e186313680fce77fa6d0c06 100644 --- a/test/spec_mime.rb +++ b/test/spec_mime.rb @@ -2,6 +2,10 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/mime' +end + describe Rack::Mime do it "should return the fallback mime-type for files with no extension" do diff --git a/test/spec_mock.rb b/test/spec_mock_request.rb similarity index 57% rename from test/spec_mock.rb rename to test/spec_mock_request.rb index ed679c3e9bee614cde4832f0f417cfdc64f98695..b2a16fdedf8ffed7a73c5ed15f12b03357dba161 100644 --- a/test/spec_mock.rb +++ b/test/spec_mock_request.rb @@ -4,6 +4,13 @@ require_relative 'helper' require 'yaml' require_relative 'psych_fix' +separate_testing do + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/request' + require_relative '../lib/rack/body_proxy' +end + app = Rack::Lint.new(lambda { |env| req = Rack::Request.new(env) @@ -17,11 +24,13 @@ app = Rack::Lint.new(lambda { |env| response = Rack::Response.new( body, req.GET["status"] || 200, - "Content-Type" => "text/yaml" + "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.set_cookie("persistent_with_expires_test", { value: "persistent_with_expires_test", expires: Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT"), path: "/" }) + response.set_cookie("expires_and_max-age_test", { value: "expires_and_max-age_test", expires: Time.now + 15552000 * 2, max_age: 15552000, path: "/" }) response.finish }) @@ -34,7 +43,13 @@ describe Rack::MockRequest do it "be able to only return the environment" do env = Rack::MockRequest.env_for("") env.must_be_kind_of Hash - env.must_include "rack.version" + end + + it "should handle a non-GET request with both :input and :params" do + env = Rack::MockRequest.env_for("/", method: :post, input: nil, params: {}) + env["PATH_INFO"].must_equal "/" + env.must_be_kind_of Hash + env['rack.input'].read.must_equal '' end it "return an environment with a path" do @@ -42,7 +57,6 @@ describe Rack::MockRequest do env["QUERY_STRING"].must_equal "location[]=1&location[]=2&age_group[]=2" env["PATH_INFO"].must_equal "/parse" env.must_be_kind_of Hash - env.must_include "rack.version" end it "provide sensible defaults" do @@ -52,6 +66,7 @@ describe Rack::MockRequest do env["REQUEST_METHOD"].must_equal "GET" env["SERVER_NAME"].must_equal "example.org" env["SERVER_PORT"].must_equal "80" + env["SERVER_PROTOCOL"].must_equal "HTTP/1.1" env["QUERY_STRING"].must_equal "" env["PATH_INFO"].must_equal "/" env["SCRIPT_NAME"].must_equal "" @@ -160,22 +175,34 @@ describe Rack::MockRequest do env["REQUEST_METHOD"].must_equal "GET" end + it "accept :script_name option to set SCRIPT_NAME" do + res = Rack::MockRequest.new(app).get("/", script_name: '/foo') + env = YAML.unsafe_load(res.body) + env["SCRIPT_NAME"].must_equal "/foo" + end + + it "accept :http_version option to set SERVER_PROTOCOL" do + res = Rack::MockRequest.new(app).get("/", http_version: 'HTTP/1.0') + env = YAML.unsafe_load(res.body) + env["SERVER_PROTOCOL"].must_equal "HTTP/1.0" + 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" } }) env = YAML.unsafe_load(res.body) env["REQUEST_METHOD"].must_equal "GET" env["QUERY_STRING"].must_include "baz=2" - env["QUERY_STRING"].must_include "foo[bar]=1" + env["QUERY_STRING"].must_include "foo%5Bbar%5D=1" env["PATH_INFO"].must_equal "/foo" env["mock.postdata"].must_equal "" end it "accept raw input in params for GET requests" do - res = Rack::MockRequest.new(app).get("/foo?baz=2", params: "foo[bar]=1") + res = Rack::MockRequest.new(app).get("/foo?baz=2", params: "foo%5Bbar%5D=1") env = YAML.unsafe_load(res.body) env["REQUEST_METHOD"].must_equal "GET" env["QUERY_STRING"].must_include "baz=2" - env["QUERY_STRING"].must_include "foo[bar]=1" + env["QUERY_STRING"].must_include "foo%5Bbar%5D=1" env["PATH_INFO"].must_equal "/foo" env["mock.postdata"].must_equal "" end @@ -187,17 +214,17 @@ describe Rack::MockRequest do env["QUERY_STRING"].must_equal "" env["PATH_INFO"].must_equal "/foo" env["CONTENT_TYPE"].must_equal "application/x-www-form-urlencoded" - env["mock.postdata"].must_equal "foo[bar]=1" + env["mock.postdata"].must_equal "foo%5Bbar%5D=1" end it "accept raw input in params for POST requests" do - res = Rack::MockRequest.new(app).post("/foo", params: "foo[bar]=1") + res = Rack::MockRequest.new(app).post("/foo", params: "foo%5Bbar%5D=1") env = YAML.unsafe_load(res.body) env["REQUEST_METHOD"].must_equal "POST" env["QUERY_STRING"].must_equal "" env["PATH_INFO"].must_equal "/foo" env["CONTENT_TYPE"].must_equal "application/x-www-form-urlencoded" - env["mock.postdata"].must_equal "foo[bar]=1" + env["mock.postdata"].must_equal "foo%5Bbar%5D=1" end it "accept params and build multipart encoded params for POST requests" do @@ -221,7 +248,7 @@ describe Rack::MockRequest do 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) called.must_equal true @@ -244,185 +271,3 @@ describe Rack::MockRequest do end end end - -describe Rack::MockResponse do - it 'has standard constructor' do - headers = { "header" => "value" } - body = ["body"] - - response = Rack::MockResponse[200, headers, body] - - response.status.must_equal 200 - response.headers.must_equal headers - response.body.must_equal body.join - end - - it "provide access to the HTTP status" do - res = Rack::MockRequest.new(app).get("") - res.must_be :successful? - res.must_be :ok? - - res = Rack::MockRequest.new(app).get("/?status=404") - res.wont_be :successful? - res.must_be :client_error? - res.must_be :not_found? - - res = Rack::MockRequest.new(app).get("/?status=501") - res.wont_be :successful? - res.must_be :server_error? - - res = Rack::MockRequest.new(app).get("/?status=307") - res.must_be :redirect? - - res = Rack::MockRequest.new(app).get("/?status=201", lint: true) - res.must_be :empty? - end - - it "provide access to the HTTP headers" do - res = Rack::MockRequest.new(app).get("") - res.must_include "Content-Type" - res.headers["Content-Type"].must_equal "text/yaml" - res.original_headers["Content-Type"].must_equal "text/yaml" - res["Content-Type"].must_equal "text/yaml" - res.content_type.must_equal "text/yaml" - res.content_length.wont_equal 0 - 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/) - assert_match(res, /rack/) - - res.match('rack')[0].must_equal 'rack' - res.match('banana').must_be_nil - end - - it "provide access to the Rack errors" do - res = Rack::MockRequest.new(app).get("/?error=foo", lint: true) - res.must_be :ok? - res.errors.wont_be :empty? - res.errors.must_include "foo" - end - - it "allow calling body.close afterwards" do - # this is exactly what rack-test does - body = StringIO.new("hi") - res = Rack::MockResponse.new(200, {}, body) - body.close if body.respond_to?(:close) - res.body.must_equal 'hi' - end - - it "optionally make Rack errors fatal" do - lambda { - Rack::MockRequest.new(app).get("/?error=foo", fatal: true) - }.must_raise Rack::MockRequest::FatalWarning - - lambda { - Rack::MockRequest.new(lambda { |env| env['rack.errors'].write(env['rack.errors'].string) }).get("/", fatal: true) - }.must_raise(Rack::MockRequest::FatalWarning).message.must_equal '' - end -end - -describe Rack::MockResponse, 'headers' do - before do - @res = Rack::MockRequest.new(app).get('') - @res.set_header 'FOO', '1' - end - - it 'has_header?' do - lambda { @res.has_header? nil }.must_raise NoMethodError - - @res.has_header?('FOO').must_equal true - @res.has_header?('Foo').must_equal true - end - - it 'get_header' do - lambda { @res.get_header nil }.must_raise NoMethodError - - @res.get_header('FOO').must_equal '1' - @res.get_header('Foo').must_equal '1' - end - - it 'set_header' do - lambda { @res.set_header nil, '1' }.must_raise NoMethodError - - @res.set_header('FOO', '2').must_equal '2' - @res.get_header('FOO').must_equal '2' - - @res.set_header('Foo', '3').must_equal '3' - @res.get_header('Foo').must_equal '3' - @res.get_header('FOO').must_equal '3' - - @res.set_header('FOO', nil).must_be_nil - @res.get_header('FOO').must_be_nil - @res.has_header?('FOO').must_equal true - end - - it 'add_header' do - lambda { @res.add_header nil, '1' }.must_raise NoMethodError - - # Sets header on first addition - @res.add_header('FOO', '1').must_equal '1,1' - @res.get_header('FOO').must_equal '1,1' - - # Ignores nil additions - @res.add_header('FOO', nil).must_equal '1,1' - @res.get_header('FOO').must_equal '1,1' - - # Converts additions to strings - @res.add_header('FOO', 2).must_equal '1,1,2' - @res.get_header('FOO').must_equal '1,1,2' - - # Respects underlying case-sensitivity - @res.add_header('Foo', 'yep').must_equal '1,1,2,yep' - @res.get_header('Foo').must_equal '1,1,2,yep' - @res.get_header('FOO').must_equal '1,1,2,yep' - end - - it 'delete_header' do - lambda { @res.delete_header nil }.must_raise NoMethodError - - @res.delete_header('FOO').must_equal '1' - @res.has_header?('FOO').must_equal false - - @res.has_header?('Foo').must_equal false - @res.delete_header('Foo').must_be_nil - end -end diff --git a/test/spec_mock_response.rb b/test/spec_mock_response.rb new file mode 100644 index 0000000000000000000000000000000000000000..83fba9287315e1c5a41d532f5e441bd2c6301dde --- /dev/null +++ b/test/spec_mock_response.rb @@ -0,0 +1,276 @@ +# frozen_string_literal: true + +require_relative 'helper' +require 'yaml' +require_relative 'psych_fix' + +separate_testing do + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/mock_response' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/request' +end + +app = Rack::Lint.new(lambda { |env| + req = Rack::Request.new(env) + + env["mock.postdata"] = env["rack.input"].read + if req.GET["error"] + env["rack.errors"].puts req.GET["error"] + env["rack.errors"].flush + end + + body = req.head? ? "" : env.to_yaml + 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.set_cookie("persistent_with_expires_test", { value: "persistent_with_expires_test", expires: Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT"), path: "/" }) + response.set_cookie("expires_and_max-age_test", { value: "expires_and_max-age_test", expires: Time.now + 15552000 * 2, max_age: 15552000, path: "/" }) + response.finish +}) + +describe Rack::MockResponse do + it 'has standard constructor' do + headers = { "header" => "value" } + body = ["body"] + + response = Rack::MockResponse[200, headers, body] + + response.status.must_equal 200 + response.headers.must_equal headers + response.body.must_equal body.join + end + + it "provide access to the HTTP status" do + res = Rack::MockRequest.new(app).get("") + res.must_be :successful? + res.must_be :ok? + + res = Rack::MockRequest.new(app).get("/?status=404") + res.wont_be :successful? + res.must_be :client_error? + res.must_be :not_found? + + res = Rack::MockRequest.new(app).get("/?status=501") + res.wont_be :successful? + res.must_be :server_error? + + res = Rack::MockRequest.new(app).get("/?status=307") + res.must_be :redirect? + + res = Rack::MockRequest.new(app).get("/?status=201", lint: true) + res.must_be :empty? + end + + it "provide access to the HTTP headers" do + res = Rack::MockRequest.new(app).get("") + res.must_include "content-type" + res.headers["content-type"].must_equal "text/yaml" + res.original_headers["content-type"].must_equal "text/yaml" + res["content-type"].must_equal "text/yaml" + res.content_type.must_equal "text/yaml" + res.content_length.wont_equal 0 + 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 "provides access to persistent cookies set with max-age" 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 "provides access to persistent cookies set with expires" do + res = Rack::MockRequest.new(app).get("") + persistent_cookie = res.cookie("persistent_with_expires_test") + persistent_cookie.value[0].must_equal "persistent_with_expires_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_equal Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT") + end + + it "parses cookies giving max-age precedence over expires" do + res = Rack::MockRequest.new(app).get("") + persistent_cookie = res.cookie("expires_and_max-age_test") + persistent_cookie.value[0].must_equal "expires_and_max-age_test" + 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 "parses cookie headers with equals sign at the end" do + res = Rack::MockRequest.new(->(env) { [200, { "Set-Cookie" => "__cf_bm=_somebase64encodedstringwithequalsatthened=; array=awesome" }, [""]] }).get("") + cookie = res.cookie("__cf_bm") + cookie.value[0].must_equal "_somebase64encodedstringwithequalsatthened=" + 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 + + deprecated "parses cookie headers provided as an array" do + res = Rack::MockRequest.new(->(env) { [200, [["set-cookie", "array=awesome"]], [""]] }).get("") + array_cookie = res.cookie("array") + array_cookie.value[0].must_equal "awesome" + end + + deprecated "parses multiple set-cookie headers provided as an array" do + cookie_headers = [["set-cookie", "array=awesome\nmultiple=times"]] + res = Rack::MockRequest.new(->(env) { [200, cookie_headers, [""]] }).get("") + array_cookie = res.cookie("array") + array_cookie.value[0].must_equal "awesome" + second_cookie = res.cookie("multiple") + second_cookie.value[0].must_equal "times" + end + + it "parses multiple set-cookie headers provided as hash with array value" do + cookie_headers = { "set-cookie" => ["array=awesome", "multiple=times"]} + res = Rack::MockRequest.new(->(env) { [200, cookie_headers, [""]] }).get("") + array_cookie = res.cookie("array") + array_cookie.value[0].must_equal "awesome" + second_cookie = res.cookie("multiple") + second_cookie.value[0].must_equal "times" + end + + it "provide access to the HTTP body" do + res = Rack::MockRequest.new(app).get("") + res.body.must_match(/rack/) + assert_match(res, /rack/) + + res.match('rack')[0].must_equal 'rack' + res.match('banana').must_be_nil + end + + it "provide access to the Rack errors" do + res = Rack::MockRequest.new(app).get("/?error=foo", lint: true) + res.must_be :ok? + res.errors.wont_be :empty? + res.errors.must_include "foo" + end + + deprecated "handle enumerable headers that are not a hash" do + # this is exactly what rack-test does + res = Rack::MockResponse.new(200, [], []) + res.cookies.must_equal({}) + end + + it "allow calling body.close afterwards" do + # this is exactly what rack-test does + body = StringIO.new("hi") + res = Rack::MockResponse.new(200, {}, body) + body.close if body.respond_to?(:close) + res.body.must_equal 'hi' + end + + it "ignores plain strings passed as errors" do + Rack::MockResponse.new(200, {}, [], 'e').errors.must_be_nil + end + + it "optionally make Rack errors fatal" do + lambda { + Rack::MockRequest.new(app).get("/?error=foo", fatal: true) + }.must_raise Rack::MockRequest::FatalWarning + + lambda { + Rack::MockRequest.new(lambda { |env| env['rack.errors'].write(env['rack.errors'].string) }).get("/", fatal: true) + }.must_raise(Rack::MockRequest::FatalWarning).message.must_equal '' + end +end + +describe Rack::MockResponse, 'headers' do + before do + @res = Rack::MockRequest.new(app).get('') + @res.set_header 'FOO', '1' + end + + it 'has_header?' do + lambda { @res.has_header? nil }.must_raise ArgumentError + + @res.has_header?('FOO').must_equal true + @res.has_header?('Foo').must_equal true + end + + it 'get_header' do + lambda { @res.get_header nil }.must_raise ArgumentError + + @res.get_header('FOO').must_equal '1' + @res.get_header('Foo').must_equal '1' + end + + it 'set_header' do + lambda { @res.set_header nil, '1' }.must_raise ArgumentError + + @res.set_header('FOO', '2').must_equal '2' + @res.get_header('FOO').must_equal '2' + + @res.set_header('Foo', '3').must_equal '3' + @res.get_header('Foo').must_equal '3' + @res.get_header('FOO').must_equal '3' + + @res.set_header('FOO', nil).must_be_nil + @res.get_header('FOO').must_be_nil + @res.has_header?('FOO').must_equal true + end + + it 'add_header' do + lambda { @res.add_header nil, '1' }.must_raise ArgumentError + + # Sets header on first addition + @res.add_header('FOO', '1').must_equal ['1', '1'] + @res.get_header('FOO').must_equal ['1', '1'] + + # Ignores nil additions + @res.add_header('FOO', nil).must_equal ['1', '1'] + @res.get_header('FOO').must_equal ['1', '1'] + + # Converts additions to strings + @res.add_header('FOO', 2).must_equal ['1', '1', '2'] + @res.get_header('FOO').must_equal ['1', '1', '2'] + + # Respects underlying case-sensitivity + @res.add_header('Foo', 'yep').must_equal ['1', '1', '2', 'yep'] + @res.get_header('Foo').must_equal ['1', '1', '2', 'yep'] + @res.get_header('FOO').must_equal ['1', '1', '2', 'yep'] + end + + it 'delete_header' do + lambda { @res.delete_header nil }.must_raise ArgumentError + + @res.delete_header('FOO').must_equal '1' + @res.has_header?('FOO').must_equal false + + @res.has_header?('Foo').must_equal false + @res.delete_header('Foo').must_be_nil + end +end diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb index d5bf30d3b9fece6fdd1f5209e53bbd4af634d392..c415056b6afa485611760b6dac1d1229e750d75d 100644 --- a/test/spec_multipart.rb +++ b/test/spec_multipart.rb @@ -3,6 +3,15 @@ require_relative 'helper' require 'timeout' +separate_testing do + require_relative '../lib/rack/multipart' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/query_parser' + require_relative '../lib/rack/utils' + require_relative '../lib/rack/request' +end + describe Rack::Multipart do def multipart_fixture(name, boundary = "AaB03x") file = multipart_file(name) @@ -26,6 +35,13 @@ describe Rack::Multipart do Rack::Multipart.parse_multipart(env).must_be_nil end + it "raises exception if boundary is too long" do + env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename, "A"*71)) + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise Rack::Multipart::Error + end + it "parse multipart content when content type present but disposition is not" do env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_disposition)) params = Rack::Multipart.parse_multipart(env) @@ -49,18 +65,49 @@ describe Rack::Multipart do params["text"].must_equal "contents" end + it "raises for invalid data preceding the boundary" do + env = Rack::MockRequest.env_for '/', multipart_fixture(:preceding_boundary) + lambda { + Rack::Multipart.parse_multipart(env) + }.must_raise Rack::Multipart::EmptyContentError + end + + it "ignores initial end boundaries" do + env = Rack::MockRequest.env_for '/', multipart_fixture(:end_boundary_first) + params = Rack::Multipart.parse_multipart(env) + params["files"][:filename].must_equal "foo" + end + + it "parse multipart content with different filename and filename*" do + env = Rack::MockRequest.env_for '/', multipart_fixture(:filename_multi) + params = Rack::Multipart.parse_multipart(env) + params["files"][:filename].must_equal "bar" + end + it "set US_ASCII encoding based on charset" do env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename)) params = Rack::Multipart.parse_multipart(env) params["text"].encoding.must_equal Encoding::US_ASCII # I'm not 100% sure if making the param name encoding match the - # Content-Type charset is the right thing to do. We should revisit this. + # content-type charset is the right thing to do. We should revisit this. params.keys.each do |key| key.encoding.must_equal Encoding::US_ASCII end end + it "sets BINARY encoding for invalid charsets" do + env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_unknown_charset)) + params = Rack::Multipart.parse_multipart(env) + params["text"].encoding.must_equal Encoding::BINARY + + # I'm not 100% sure if making the param name encoding match the + # content-type charset is the right thing to do. We should revisit this. + params.keys.each do |key| + key.encoding.must_equal Encoding::BINARY + end + end + it "set BINARY encoding on things without content type" do env = Rack::MockRequest.env_for("/", multipart_fixture(:none)) params = Rack::Multipart.parse_multipart(env) @@ -92,17 +139,6 @@ describe Rack::Multipart do params['user_sid'].encoding.must_equal Encoding::UTF_8 end - it "raise ParamsTooDeepError if the key space is exhausted" do - env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename)) - - old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1 - begin - lambda { Rack::Multipart.parse_multipart(env) }.must_raise(Rack::QueryParser::ParamsTooDeepError) - ensure - Rack::Utils.key_space_limit = old - end - end - it "parse multipart form webkit style" do env = Rack::MockRequest.env_for '/', multipart_fixture(:webkit) env['CONTENT_TYPE'] = "multipart/form-data; boundary=----WebKitFormBoundaryWLHCs9qmcJJoyjKR" @@ -128,12 +164,16 @@ describe Rack::Multipart do # make the initial boundary a few gigs long longer = "0123456789" * 1024 * 1024 - (1024 * 1024).times { wr.write(longer) } + (1024 * 1024).times do + while wr.write_nonblock(longer, exception: false) == :wait_writable + Thread.pass + end + end wr.write("\r\n") - wr.write('Content-Disposition: form-data; name="a"; filename="a.txt"') + wr.write('content-disposition: form-data; name="a"; filename="a.txt"') wr.write("\r\n") - wr.write("Content-Type: text/plain\r\n") + wr.write("content-type: text/plain\r\n") wr.write("\r\na") wr.write("--AaB03x--\r\n") wr.close @@ -151,7 +191,7 @@ describe Rack::Multipart do env = Rack::MockRequest.env_for '/', fixture lambda { Rack::Multipart.parse_multipart(env) - }.must_raise EOFError + }.must_raise Rack::Multipart::EmptyContentError rd.close err = thr.value @@ -166,13 +206,14 @@ describe Rack::Multipart do data = StringIO.new data.write("--#{boundary}") data.write("\r\n") - data.write('Content-Disposition: form-data; name="a"; filename="a.pdf"') + 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("content-type:application/pdf\r\n") data.write("\r\n") data.write("-" * (1024 * 1024)) data.write("\r\n") data.write("--#{boundary}--\r\n") + data.rewind fixture = { "CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}", @@ -184,7 +225,7 @@ describe Rack::Multipart do Timeout::timeout(10) { Rack::Multipart.parse_multipart(env) } end - it 'raises an EOF error on content-length mistmatch' do + it 'raises an EOF error on content-length mismatch' do env = Rack::MockRequest.env_for("/", multipart_fixture(:empty)) env['rack.input'] = StringIO.new assert_raises(EOFError) do @@ -199,9 +240,9 @@ describe Rack::Multipart do params["submit-name-with-content"].must_equal "Berry" params["files"][:type].must_equal "text/plain" params["files"][:filename].must_equal "file1.txt" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; filename=\"file1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -213,7 +254,7 @@ describe Rack::Multipart do @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end - query_parser = Rack::QueryParser.new c, 65536, 100 + query_parser = Rack::QueryParser.new c, 100 env = Rack::MockRequest.env_for("/", multipart_fixture(:text)) params = Rack::Multipart.parse_multipart(env, query_parser) params[:files][:type].must_equal "text/plain" @@ -230,9 +271,9 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["file1.txt"][:type].must_equal "text/plain" params["file1.txt"][:filename].must_equal "file1.txt" - params["file1.txt"][:head].must_equal "Content-Disposition: form-data; " + + params["file1.txt"][:head].must_equal "content-disposition: form-data; " + "filename=\"file1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["file1.txt"][:name].must_equal "file1.txt" params["file1.txt"][:tempfile].read.must_equal "contents" end @@ -252,9 +293,9 @@ describe Rack::Multipart do params["foo"]["submit-name"].must_equal "Larry" params["foo"]["files"][:type].must_equal "text/plain" params["foo"]["files"][:filename].must_equal "file1.txt" - params["foo"]["files"][:head].must_equal "Content-Disposition: form-data; " + + params["foo"]["files"][:head].must_equal "content-disposition: form-data; " + "name=\"foo[files]\"; filename=\"file1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["foo"]["files"][:name].must_equal "foo[files]" params["foo"]["files"][:tempfile].read.must_equal "contents" end @@ -266,9 +307,9 @@ describe Rack::Multipart do params["files"][:type].must_equal "image/png" params["files"][:filename].must_equal "rack-logo.png" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; filename=\"rack-logo.png\"\r\n" + - "Content-Type: image/png\r\n" + "content-type: image/png\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.length.must_equal 26473 end @@ -279,9 +320,9 @@ describe Rack::Multipart do params["submit-name"].must_equal "Larry" params["files"][:type].must_equal "text/plain" params["files"][:filename].must_equal "file1.txt" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; filename=\"file1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "" end @@ -291,9 +332,9 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "text/plain" params["files"][:filename].must_equal "fi;le1.txt" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; filename=\"fi;le1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -305,9 +346,9 @@ describe Rack::Multipart do params["submit-name-with-content"].must_equal "Berry" params["files"][:type].must_equal "text/plain" params["files"][:filename].must_equal "file1.txt" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; filename=\"file1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -317,9 +358,9 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "text/plain" params["files"][:filename].must_match(/invalid/) - head = "Content-Disposition: form-data; " + + head = "content-disposition: form-data; " + "name=\"files\"; filename=\"invalid\xC3.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" head = head.force_encoding(Encoding::ASCII_8BIT) params["files"][:head].must_equal head params["files"][:name].must_equal "files" @@ -344,7 +385,7 @@ describe Rack::Multipart do params["files"][:filename].must_equal "flowers.exe\u0000.jpg" end - it "is robust separating Content-Disposition fields" do + 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" @@ -371,10 +412,10 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "text/plain" params["files"][:filename].must_equal "file1.txt" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; " + 'filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"' + - "\r\nContent-Type: text/plain\r\n" + "\r\ncontent-type: text/plain\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -384,8 +425,8 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "image/jpeg" params["files"][:filename].must_equal "genome.jpeg" - params["files"][:head].must_equal "Content-Type: image/jpeg\r\n" + - "Content-Disposition: attachment; " + + params["files"][:head].must_equal "content-type: image/jpeg\r\n" + + "content-disposition: attachment; " + "name=\"files\"; " + "filename=genome.jpeg; " + "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";\r\n" + @@ -399,10 +440,10 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "application/octet-stream" params["files"][:filename].must_equal "escape \"quotes" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; " + "filename=\"escape \\\"quotes\"\r\n" + - "Content-Type: application/octet-stream\r\n" + "content-type: application/octet-stream\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -412,10 +453,10 @@ describe Rack::Multipart do 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; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; " + "filename=\"foo+bar\"\r\n" + - "Content-Type: application/octet-stream\r\n" + "content-type: application/octet-stream\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -425,10 +466,10 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "application/octet-stream" params["files"][:filename].must_equal "escape \"quotes" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; " + "filename=\"escape %22quotes\"\r\n" + - "Content-Type: application/octet-stream\r\n" + "content-type: application/octet-stream\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -438,8 +479,8 @@ describe Rack::Multipart do params = Rack::Multipart.parse_multipart(env) params["files"][:type].must_equal "image/jpeg" params["files"][:filename].must_equal "\"human\" genome.jpeg" - params["files"][:head].must_equal "Content-Type: image/jpeg\r\n" + - "Content-Disposition: attachment; " + + params["files"][:head].must_equal "content-type: image/jpeg\r\n" + + "content-disposition: attachment; " + "name=\"files\"; " + "filename=\"\\\"human\\\" genome.jpeg\"; " + "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";\r\n" + @@ -455,8 +496,8 @@ describe Rack::Multipart do files[:type].must_equal "image/jpeg" files[:filename].must_equal "100% of a photo.jpeg" files[:head].must_equal <<-MULTIPART -Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"\r -Content-Type: image/jpeg\r +content-disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"\r +content-type: image/jpeg\r MULTIPART files[:name].must_equal "document[attachment]" @@ -470,8 +511,8 @@ Content-Type: image/jpeg\r files[:type].must_equal "image/jpeg" files[:filename].must_equal "100%a" files[:head].must_equal <<-MULTIPART -Content-Disposition: form-data; name="document[attachment]"; filename="100%a"\r -Content-Type: image/jpeg\r +content-disposition: form-data; name="document[attachment]"; filename="100%a"\r +content-type: image/jpeg\r MULTIPART files[:name].must_equal "document[attachment]" @@ -485,26 +526,41 @@ Content-Type: image/jpeg\r files[:type].must_equal "image/jpeg" files[:filename].must_equal "100%" files[:head].must_equal <<-MULTIPART -Content-Disposition: form-data; name="document[attachment]"; filename="100%"\r -Content-Type: image/jpeg\r +content-disposition: form-data; name="document[attachment]"; filename="100%"\r +content-type: image/jpeg\r MULTIPART files[:name].must_equal "document[attachment]" files[:tempfile].read.must_equal "contents" end - it "rewinds input after parsing upload" do - options = multipart_fixture(:text) - input = options[:input] + it "raises RuntimeError for invalid file path" do + proc{Rack::Multipart::UploadedFile.new('non-existant')}.must_raise RuntimeError + end + + it "supports uploading files in binary mode" do + Rack::Multipart::UploadedFile.new(multipart_file("file1.txt")).wont_be :binmode? + Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"), binary: true).must_be :binmode? + end + + it "builds multipart body" do + files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt")) + data = Rack::Multipart.build_multipart("submit-name" => "Larry", "files" => files) + + options = { + "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x", + "CONTENT_LENGTH" => data.length.to_s, + :input => StringIO.new(data) + } env = Rack::MockRequest.env_for("/", options) params = Rack::Multipart.parse_multipart(env) params["submit-name"].must_equal "Larry" params["files"][:filename].must_equal "file1.txt" - input.read.length.must_equal 307 + params["files"][:tempfile].read.must_equal "contents" end - it "builds multipart body" do - files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt")) + it "builds multipart filename with space" do + files = Rack::Multipart::UploadedFile.new(multipart_file("space case.txt")) data = Rack::Multipart.build_multipart("submit-name" => "Larry", "files" => files) options = { @@ -515,7 +571,7 @@ Content-Type: image/jpeg\r env = Rack::MockRequest.env_for("/", options) params = Rack::Multipart.parse_multipart(env) params["submit-name"].must_equal "Larry" - params["files"][:filename].must_equal "file1.txt" + params["files"][:filename].must_equal "space case.txt" params["files"][:tempfile].read.must_equal "contents" end @@ -620,7 +676,22 @@ Content-Type: image/jpeg\r end end - it "reach a multipart limit" do + it "treat a multipart limit of 0 as no limit" do + begin + previous_limit = Rack::Utils.multipart_part_limit + Rack::Utils.multipart_part_limit = 0 + + env = Rack::MockRequest.env_for '/', multipart_fixture(:three_files_three_fields) + params = Rack::Multipart.parse_multipart(env) + params['reply'].must_equal 'yes' + params['to'].must_equal 'people' + params['from'].must_equal 'others' + ensure + Rack::Utils.multipart_part_limit = previous_limit + end + end + + it "reach a multipart file limit" do begin previous_limit = Rack::Utils.multipart_part_limit Rack::Utils.multipart_part_limit = 3 @@ -658,8 +729,8 @@ Content-Type: image/jpeg\r it "can parse fields with a content type" do data = <<-EOF --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon\r -Content-Disposition: form-data; name="description"\r -Content-Type: text/plain"\r +content-disposition: form-data; name="description"\r +content-type: text/plain"\r \r Very very blue\r --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon--\r @@ -686,8 +757,8 @@ EOF it "parse very long unquoted multipart file names" do data = <<-EOF --AaB03x\r -Content-Type: text/plain\r -Content-Disposition: attachment; name=file; filename=#{'long' * 100}\r +content-type: text/plain\r +content-disposition: attachment; name=file; filename=#{'long' * 100}\r \r contents\r --AaB03x--\r @@ -707,8 +778,8 @@ contents\r it "parse unquoted parameter values at end of line" do data = <<-EOF --AaB03x\r -Content-Type: text/plain\r -Content-Disposition: attachment; name=inline\r +content-type: text/plain\r +content-disposition: attachment; name=inline\r \r true\r --AaB03x--\r @@ -727,8 +798,8 @@ true\r it "parse quoted chars in name parameter" do data = <<-EOF --AaB03x\r -Content-Type: text/plain\r -Content-Disposition: attachment; name="quoted\\\\chars\\"in\rname"\r +content-type: text/plain\r +content-disposition: attachment; name="quoted\\\\chars\\"in\rname"\r \r true\r --AaB03x--\r @@ -761,9 +832,9 @@ true\r params["submit-name-with-content"].must_equal "Berry" params["files"][:type].must_equal "text/plain" params["files"][:filename].must_equal "file1.txt" - params["files"][:head].must_equal "Content-Disposition: form-data; " + + params["files"][:head].must_equal "content-disposition: form-data; " + "name=\"files\"; filename=\"file1.txt\"\r\n" + - "Content-Type: text/plain\r\n" + "content-type: text/plain\r\n" params["files"][:name].must_equal "files" params["files"][:tempfile].read.must_equal "contents" end @@ -773,15 +844,15 @@ true\r data = <<-EOF.dup --AaB03x\r -Content-Type: text/plain\r +content-type: text/plain\r \r some text\r --AaB03x\r \r \r -some more text (I didn't specify Content-Type)\r +some more text (I didn't specify content-type)\r --AaB03x\r -Content-Type: image/png\r +content-type: image/png\r \r #{rack_logo}\r --AaB03x--\r @@ -795,7 +866,7 @@ Content-Type: image/png\r env = Rack::MockRequest.env_for("/", options) params = Rack::Multipart.parse_multipart(env) - params["text/plain"].must_equal ["some text", "some more text (I didn't specify Content-Type)"] + params["text/plain"].must_equal ["some text", "some more text (I didn't specify content-type)"] params["image/png"].length.must_equal 1 f = Tempfile.new("rack-logo") diff --git a/test/spec_null_logger.rb b/test/spec_null_logger.rb index 435d051eadab6efd6b07649b632675178d69e18c..65ddb279e2b306f821ac0e553b36fb871fc5bb21 100644 --- a/test/spec_null_logger.rb +++ b/test/spec_null_logger.rb @@ -2,18 +2,24 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/null_logger' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + 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_query_parser.rb b/test/spec_query_parser.rb new file mode 100644 index 0000000000000000000000000000000000000000..dbb8b14eda730928ea07f7a97d4aa9a91c09adb4 --- /dev/null +++ b/test/spec_query_parser.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative 'helper' + +separate_testing do + require_relative '../lib/rack/query_parser' +end + +describe Rack::QueryParser do + query_parser ||= Rack::QueryParser.make_default(8) + + it "can normalize values with missing values" do + query_parser.parse_nested_query("a=a").must_equal({"a" => "a"}) + query_parser.parse_nested_query("a=").must_equal({"a" => ""}) + query_parser.parse_nested_query("a").must_equal({"a" => nil}) + end +end diff --git a/test/spec_recursive.rb b/test/spec_recursive.rb index 62e3a4f16bb547d7e84a754b610fa7f35cc21bd5..650626c0f9ee320847c0470b49a9fa6d6348b864 100644 --- a/test/spec_recursive.rb +++ b/test/spec_recursive.rb @@ -2,12 +2,19 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/recursive' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/urlmap' +end + describe Rack::Recursive do before do @app1 = lambda { |env| res = Rack::Response.new - res["X-Path-Info"] = env["PATH_INFO"] - res["X-Query-String"] = env["QUERY_STRING"] + res["x-path-info"] = env["PATH_INFO"] + res["x-query-string"] = env["QUERY_STRING"] res.finish do |inner_res| inner_res.write "App1" end @@ -68,7 +75,7 @@ describe Rack::Recursive do res = Rack::MockRequest.new(app).get("/app4") res.must_be :ok? res.body.must_equal "App1" - res["X-Path-Info"].must_equal "/quux" - res["X-Query-String"].must_equal "meh" + res["x-path-info"].must_equal "/quux" + res["x-query-string"].must_equal "meh" end end diff --git a/test/spec_request.rb b/test/spec_request.rb index 51cfcdc88c329c8b7d89d3655ad6dd377f91ce16..169118635a1cd74d1757139fd88615cd9d48b5fa 100644 --- a/test/spec_request.rb +++ b/test/spec_request.rb @@ -5,9 +5,20 @@ require 'cgi' require 'forwardable' require 'securerandom' +separate_testing do + require_relative '../lib/rack/request' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/lint' +end + class RackRequestTest < Minitest::Spec it "copies the env when duping" do req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) + + if req.delegate? + skip "delegate requests don't dup environments" + end + refute_same req.env, req.dup.env end @@ -36,6 +47,20 @@ class RackRequestTest < Minitest::Spec assert_equal "example.com:443", req.authority end + it 'can calculate the server authority' do + req = make_request('SERVER_NAME' => 'example.com') + assert_equal "example.com", req.server_authority + req = make_request('SERVER_NAME' => 'example.com', 'SERVER_PORT' => 8080) + assert_equal "example.com:8080", req.server_authority + end + + it 'can calculate the port without an authority' do + req = make_request('SERVER_PORT' => 8080) + assert_equal 8080, req.port + req = make_request('HTTPS' => 'on') + assert_equal 443, req.port + end + it 'yields to the block if no value has been set' do req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) yielded = false @@ -121,11 +146,68 @@ class RackRequestTest < Minitest::Spec req.host.must_equal "123foo.example.com" req.hostname.must_equal "123foo.example.com" + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "♡.com") + req.host.must_equal "♡.com" + req.hostname.must_equal "♡.com" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "♡.com:80") + req.host.must_equal "♡.com" + req.hostname.must_equal "♡.com" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.è°·æŒ") + req.host.must_equal "nic.è°·æŒ" + req.hostname.must_equal "nic.è°·æŒ" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.è°·æŒ:80") + req.host.must_equal "nic.è°·æŒ" + req.hostname.must_equal "nic.è°·æŒ" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "technically_invalid.example.com") + req.host.must_equal "technically_invalid.example.com" + req.hostname.must_equal "technically_invalid.example.com" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "technically_invalid.example.com:80") + req.host.must_equal "technically_invalid.example.com" + req.hostname.must_equal "technically_invalid.example.com" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "trailing_newline.com\n") + req.host.must_be_nil + req.hostname.must_be_nil + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "really\nbad\ninput") + req.host.must_be_nil + req.hostname.must_be_nil + req = make_request \ Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org", "SERVER_PORT" => "9292") req.host.must_equal "example.org" req.hostname.must_equal "example.org" + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=example.org:9292") + req.host.must_equal "example.org" + + # Test obfuscated identifier: https://tools.ietf.org/html/rfc7239#section-6.3 + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=ObFuScaTeD") + req.host.must_equal "ObFuScaTeD" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=example.com; host=example.org:9292") + req.host.must_equal "example.org" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292", "HTTP_FORWARDED" => "host=example.com") + req.host.must_equal "example.com" + req = make_request \ Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292") req.host.must_equal "example.org" @@ -141,6 +223,71 @@ class RackRequestTest < Minitest::Spec req.host.must_equal "[2001:db8:cafe::17]" req.hostname.must_equal "2001:db8:cafe::17" + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[::]:47011") + req.host.must_equal "[::]" + req.hostname.must_equal "::" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[1111:2222:3333:4444:5555:6666:123.123.123.123]") + req.host.must_equal "[1111:2222:3333:4444:5555:6666:123.123.123.123]" + req.hostname.must_equal "1111:2222:3333:4444:5555:6666:123.123.123.123" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[1111:2222:3333:4444:5555:6666:123.123.123.123]:47011") + req.host.must_equal "[1111:2222:3333:4444:5555:6666:123.123.123.123]" + req.hostname.must_equal "1111:2222:3333:4444:5555:6666:123.123.123.123" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "0.0.0.0") + req.host.must_equal "0.0.0.0" + req.hostname.must_equal "0.0.0.0" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "0.0.0.0:47011") + req.host.must_equal "0.0.0.0" + req.hostname.must_equal "0.0.0.0" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "255.255.255.255") + req.host.must_equal "255.255.255.255" + req.hostname.must_equal "255.255.255.255" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "255.255.255.255:47011") + req.host.must_equal "255.255.255.255" + req.hostname.must_equal "255.255.255.255" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "really\nbad\ninput") + req.host.must_be_nil + req.hostname.must_be_nil + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[0]") + req.host.must_be_nil + req.hostname.must_be_nil + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[:::]") + req.host.must_be_nil + req.hostname.must_be_nil + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[1111:2222:3333:4444:5555:6666:7777:88888]") + req.host.must_be_nil + req.hostname.must_be_nil + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "0.0..0.0") + req.host.must_equal '0.0..0.0' + req.hostname.must_equal '0.0..0.0' + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "255.255.255.0255") + req.host.must_equal "255.255.255.0255" + req.hostname.must_equal "255.255.255.0255" + env = Rack::MockRequest.env_for("/") env.delete("SERVER_NAME") req = make_request(env) @@ -203,6 +350,187 @@ class RackRequestTest < Minitest::Spec req = make_request \ Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_X_FORWARDED_PROTO" => "https,https", "SERVER_PORT" => "80") req.port.must_equal 443 + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_FORWARDED" => "proto=https", "HTTP_X_FORWARDED_PROTO" => "http", "SERVER_PORT" => "9393") + req.port.must_equal 443 + end + + it "have forwarded_* methods respect forwarded_priority" do + begin + default_priority = Rack::Request.forwarded_priority + default_proto_priority = Rack::Request.x_forwarded_proto_priority + + def self.req(headers) + req = make_request Rack::MockRequest.env_for("/", headers) + req.singleton_class.send(:public, :forwarded_scheme) + req + end + + req("HTTP_FORWARDED"=>"for=1.2.3.4", + "HTTP_X_FORWARDED_FOR" => "2.3.4.5"). + forwarded_for.must_equal ['1.2.3.4'] + + req("HTTP_FORWARDED"=>"for=1.2.3.4:1234", + "HTTP_X_FORWARDED_PORT" => "2345"). + forwarded_port.must_equal [1234] + + req("HTTP_FORWARDED"=>"for=1.2.3.4", + "HTTP_X_FORWARDED_PORT" => "2345"). + forwarded_port.must_equal [] + + req("HTTP_FORWARDED"=>"host=1.2.3.4, host=3.4.5.6", + "HTTP_X_FORWARDED_HOST" => "2.3.4.5,4.5.6.7"). + forwarded_authority.must_equal '3.4.5.6' + + req("HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "ws" + + req("HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "http" + + Rack::Request.forwarded_priority = [nil, :x_forwarded, :forwarded] + + req("HTTP_FORWARDED"=>"for=1.2.3.4", + "HTTP_X_FORWARDED_FOR" => "2.3.4.5"). + forwarded_for.must_equal ['2.3.4.5'] + + req("HTTP_FORWARDED"=>"for=1.2.3.4", + "HTTP_X_FORWARDED_PORT" => "2345"). + forwarded_port.must_equal [2345] + + req("HTTP_FORWARDED"=>"host=1.2.3.4, host=3.4.5.6", + "HTTP_X_FORWARDED_HOST" => "2.3.4.5,4.5.6.7"). + forwarded_authority.must_equal '4.5.6.7' + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "ws" + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "http" + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_equal "https" + + Rack::Request.x_forwarded_proto_priority = [nil, :scheme, :proto] + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "http" + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws"). + forwarded_scheme.must_equal "ws" + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_equal "https" + + Rack::Request.forwarded_priority = [:x_forwarded] + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "http" + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws"). + forwarded_scheme.must_equal "ws" + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_be_nil + + Rack::Request.x_forwarded_proto_priority = [:scheme] + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "http" + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws"). + forwarded_scheme.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_be_nil + + Rack::Request.x_forwarded_proto_priority = [:proto] + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal "ws" + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_be_nil + + Rack::Request.x_forwarded_proto_priority = [] + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_be_nil + + Rack::Request.x_forwarded_proto_priority = default_proto_priority + Rack::Request.forwarded_priority = [:forwarded] + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_equal 'https' + + req("HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_be_nil + + req("HTTP_X_FORWARDED_PROTO" => "ws"). + forwarded_scheme.must_be_nil + + Rack::Request.forwarded_priority = [] + + req("HTTP_FORWARDED"=>"for=1.2.3.4", + "HTTP_X_FORWARDED_FOR" => "2.3.4.5"). + forwarded_for.must_be_nil + + req("HTTP_FORWARDED"=>"for=1.2.3.4", + "HTTP_X_FORWARDED_PORT" => "2345"). + forwarded_port.must_be_nil + + req("HTTP_FORWARDED"=>"host=1.2.3.4, host=3.4.5.6", + "HTTP_X_FORWARDED_HOST" => "2.3.4.5,4.5.6.7"). + forwarded_authority.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_PROTO" => "ws", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https", + "HTTP_X_FORWARDED_SCHEME" => "http"). + forwarded_scheme.must_be_nil + + req("HTTP_FORWARDED"=>"proto=https"). + forwarded_scheme.must_be_nil + + ensure + Rack::Request.forwarded_priority = default_priority + Rack::Request.x_forwarded_proto_priority = default_proto_priority + end end it "figure out the correct host with port" do @@ -237,14 +565,18 @@ class RackRequestTest < Minitest::Spec req = make_request \ Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org", "SERVER_PORT" => "9393") req.host_with_port.must_equal "example.org" + + req = make_request \ + Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org", "HTTP_FORWARDED" => "host=example.com:9292", "SERVER_PORT" => "9393") + req.host_with_port.must_equal "example.com:9292" end it "parse the query string" do - req = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=bla")) - req.query_string.must_equal "foo=bar&quux=bla" - req.GET.must_equal "foo" => "bar", "quux" => "bla" - req.POST.must_be :empty? - req.params.must_equal "foo" => "bar", "quux" => "bla" + request = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=bla¬hing&empty=")) + request.query_string.must_equal "foo=bar&quux=bla¬hing&empty=" + request.GET.must_equal "foo" => "bar", "quux" => "bla", "nothing" => nil, "empty" => "" + request.POST.must_be :empty? + request.params.must_equal "foo" => "bar", "quux" => "bla", "nothing" => nil, "empty" => "" end it "not truncate query strings containing semi-colons #543 only in POST" do @@ -265,7 +597,7 @@ class RackRequestTest < Minitest::Spec @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end - parser = Rack::QueryParser.new(c, 65536, 100) + parser = Rack::QueryParser.new(c, 100) c = Class.new(Rack::Request) do define_method(:query_parser) do parser @@ -278,49 +610,23 @@ class RackRequestTest < Minitest::Spec req.params[:quux].must_equal "bla" end - it "use semi-colons as separators for query strings in GET" do + it "does not use semi-colons as separators for query strings in GET" do req = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=b;la;wun=duh")) req.query_string.must_equal "foo=bar&quux=b;la;wun=duh" - req.GET.must_equal "foo" => "bar", "quux" => "b", "la" => nil, "wun" => "duh" + req.GET.must_equal "foo" => "bar", "quux" => "b;la;wun=duh" req.POST.must_be :empty? - req.params.must_equal "foo" => "bar", "quux" => "b", "la" => nil, "wun" => "duh" - end - - it "limit the keys from the GET query string" do - env = Rack::MockRequest.env_for("/?foo=bar") - - old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1 - begin - req = make_request(env) - lambda { req.GET }.must_raise Rack::QueryParser::ParamsTooDeepError - ensure - Rack::Utils.key_space_limit = old - end - end - - it "limit the key size per nested params hash" do - nested_query = Rack::MockRequest.env_for("/?foo%5Bbar%5D%5Bbaz%5D%5Bqux%5D=1") - plain_query = Rack::MockRequest.env_for("/?foo_bar__baz__qux_=1") - - old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3 - begin - exp = { "foo" => { "bar" => { "baz" => { "qux" => "1" } } } } - make_request(nested_query).GET.must_equal exp - lambda { make_request(plain_query).GET }.must_raise Rack::QueryParser::ParamsTooDeepError - ensure - Rack::Utils.key_space_limit = old - end + req.params.must_equal "foo" => "bar", "quux" => "b;la;wun=duh" end it "limit the allowed parameter depth when parsing parameters" do - env = Rack::MockRequest.env_for("/?a#{'[a]' * 110}=b") + env = Rack::MockRequest.env_for("/?a#{'[a]' * 40}=b") req = make_request(env) lambda { req.GET }.must_raise Rack::QueryParser::ParamsTooDeepError - env = Rack::MockRequest.env_for("/?a#{'[a]' * 90}=b") + env = Rack::MockRequest.env_for("/?a#{'[a]' * 30}=b") req = make_request(env) params = req.GET - 90.times { params = params['a'] } + 30.times { params = params['a'] } params['a'].must_equal 'b' old, Rack::Utils.param_depth_limit = Rack::Utils.param_depth_limit, 3 @@ -358,7 +664,7 @@ class RackRequestTest < Minitest::Spec @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)} end end - parser = Rack::QueryParser.new(c, 65536, 100) + parser = Rack::QueryParser.new(c, 100) c = Class.new(Rack::Request) do define_method(:query_parser) do parser @@ -390,12 +696,7 @@ class RackRequestTest < Minitest::Spec message.must_equal "invalid %-encoding (a%)" end - it "raise if rack.input is missing" do - req = make_request({}) - lambda { req.POST }.must_raise RuntimeError - end - - it "parse POST data when method is POST and no Content-Type given" do + it "parse POST data when method is POST and no content-type given" do req = make_request \ Rack::MockRequest.env_for("/?foo=quux", "REQUEST_METHOD" => 'POST', @@ -408,20 +709,6 @@ class RackRequestTest < Minitest::Spec req.params.must_equal "foo" => "bar", "quux" => "bla" end - it "limit the keys from the POST form data" do - env = Rack::MockRequest.env_for("", - "REQUEST_METHOD" => 'POST', - :input => "foo=bar&quux=bla") - - old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1 - begin - req = make_request(env) - lambda { req.POST }.must_raise Rack::QueryParser::ParamsTooDeepError - ensure - Rack::Utils.key_space_limit = old - end - end - it "parse POST data with explicit content type regardless of method" do req = make_request \ Rack::MockRequest.env_for("/", @@ -444,7 +731,9 @@ class RackRequestTest < Minitest::Spec req.media_type.must_equal 'text/plain' req.media_type_params['charset'].must_equal 'utf-8' req.content_charset.must_equal 'utf-8' - req.POST.must_be :empty? + post = req.POST + post.must_be_empty + req.POST.must_be_same_as post req.params.must_equal "foo" => "quux" req.body.read.must_equal "foo=bar&quux=bla" end @@ -456,17 +745,6 @@ class RackRequestTest < Minitest::Spec "CONTENT_TYPE" => 'application/x-www-form-urlencoded', :input => "foo=bar&quux=bla") req.POST.must_equal "foo" => "bar", "quux" => "bla" - req.body.read.must_equal "foo=bar&quux=bla" - end - - it "rewind input after parsing POST data" do - input = StringIO.new("foo=bar&quux=bla") - req = make_request \ - Rack::MockRequest.env_for("/", - "CONTENT_TYPE" => 'application/x-www-form-urlencoded;foo=bar', - :input => input) - req.params.must_equal "foo" => "bar", "quux" => "bla" - input.read.must_equal "foo=bar&quux=bla" end it "safely accepts POST requests with empty body" do @@ -493,19 +771,21 @@ class RackRequestTest < Minitest::Spec it "get value by key from params with #[]" do req = make_request \ Rack::MockRequest.env_for("?foo=quux") - req['foo'].must_equal 'quux' - req[:foo].must_equal 'quux' + assert_output(nil, /deprecated/) do + req['foo'].must_equal 'quux' + req[:foo].must_equal 'quux' + end next if self.class == TestProxyRequest verbose = $VERBOSE warn_arg = nil - req.define_singleton_method(:warn) do |arg| - warn_arg = arg + req.define_singleton_method(:warn) do |*args| + warn_arg = args end begin $VERBOSE = true req['foo'].must_equal 'quux' - warn_arg.must_equal "Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead" + warn_arg.must_equal ["Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", { uplevel: 1 }] ensure $VERBOSE = verbose end @@ -514,33 +794,43 @@ class RackRequestTest < Minitest::Spec it "set value to key on params with #[]=" do req = make_request \ Rack::MockRequest.env_for("?foo=duh") - req['foo'].must_equal 'duh' - req[:foo].must_equal 'duh' + assert_output(nil, /deprecated/) do + req['foo'].must_equal 'duh' + req[:foo].must_equal 'duh' + end req.params.must_equal 'foo' => 'duh' if req.delegate? skip "delegate requests don't cache params, so mutations have no impact" end - req['foo'] = 'bar' + assert_output(nil, /deprecated/) do + req['foo'] = 'bar' + end req.params.must_equal 'foo' => 'bar' - req['foo'].must_equal 'bar' - req[:foo].must_equal 'bar' + assert_output(nil, /deprecated/) do + req['foo'].must_equal 'bar' + req[:foo].must_equal 'bar' + end - req[:foo] = 'jaz' + assert_output(nil, /deprecated/) do + req[:foo] = 'jaz' + end req.params.must_equal 'foo' => 'jaz' - req['foo'].must_equal 'jaz' - req[:foo].must_equal 'jaz' + assert_output(nil, /deprecated/) do + req['foo'].must_equal 'jaz' + req[:foo].must_equal 'jaz' + end verbose = $VERBOSE warn_arg = nil - req.define_singleton_method(:warn) do |arg| - warn_arg = arg + req.define_singleton_method(:warn) do |*args| + warn_arg = args end begin $VERBOSE = true req['foo'] = 'quux' - warn_arg.must_equal "Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead" + warn_arg.must_equal ["Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", { uplevel: 1 }] req.params['foo'].must_equal 'quux' ensure $VERBOSE = verbose @@ -625,6 +915,25 @@ class RackRequestTest < Minitest::Spec request.scheme.must_equal "http" request.wont_be :ssl? + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_SCHEME' => 'ws')) + request.scheme.must_equal "ws" + request.wont_be :ssl? + + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'ws')) + request.scheme.must_equal "ws" + + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=https')) + request.scheme.must_equal "https" + request.must_be :ssl? + + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=https, proto=http')) + request.scheme.must_equal "http" + request.wont_be :ssl? + + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=http, proto=https')) + request.scheme.must_equal "https" + request.must_be :ssl? + request = make_request(Rack::MockRequest.env_for("/", 'HTTPS' => 'on')) request.scheme.must_equal "https" request.must_be :ssl? @@ -653,12 +962,20 @@ class RackRequestTest < Minitest::Spec request.scheme.must_equal "https" request.must_be :ssl? + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_SCHEME' => 'wss')) + request.scheme.must_equal "wss" + request.must_be :ssl? + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'https')) request.scheme.must_equal "https" request.must_be :ssl? request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'https, http, http')) - request.scheme.must_equal "https" + request.scheme.must_equal "http" + request.wont_be :ssl? + + request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'wss')) + request.scheme.must_equal "wss" request.must_be :ssl? end @@ -896,6 +1213,22 @@ class RackRequestTest < Minitest::Spec req.media_type_params['weird'].must_equal 'lol"' end + it "returns the same error for invalid post inputs" do + env = { + 'REQUEST_METHOD' => 'POST', + 'PATH_INFO' => '/foo', + 'rack.input' => StringIO.new('invalid=bar&invalid[foo]=bar'), + 'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded", + } + + 2.times do + # The actual exception type here is unimportant - just that it fails. + assert_raises(Rack::Utils::ParameterTypeError) do + Rack::Request.new(env).POST + end + end + end + it "parse with junk before boundary" do # Adapted from RFC 1867. input = <<EOF @@ -907,8 +1240,8 @@ content-disposition: form-data; name="reply"\r yes\r --AaB03x\r content-disposition: form-data; name="fileupload"; filename="dj.jpg"\r -Content-Type: image/jpeg\r -Content-Transfer-Encoding: base64\r +content-type: image/jpeg\r +content-transfer-encoding: base64\r \r /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r --AaB03x--\r @@ -946,8 +1279,8 @@ content-disposition: form-data; name="reply" yes --AaB03x content-disposition: form-data; name="fileupload"; filename="dj.jpg" -Content-Type: image/jpeg -Content-Transfer-Encoding: base64 +content-type: image/jpeg +content-transfer-encoding: base64 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg --AaB03x-- @@ -970,8 +1303,8 @@ content-disposition: form-data; name="reply"\r yes\r --AaB03x\r content-disposition: form-data; name="fileupload"; filename="dj.jpg"\r -Content-Type: image/jpeg\r -Content-Transfer-Encoding: base64\r +content-type: image/jpeg\r +content-transfer-encoding: base64\r \r /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r --AaB03x--\r @@ -1002,7 +1335,7 @@ EOF it "MultipartPartLimitError when request has too many multipart file parts if limit set" do begin - data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n") + data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n") data += "--AaB03x--\r" options = { @@ -1034,7 +1367,7 @@ EOF it 'closes tempfiles it created in the case of too many created' do begin - data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n") + data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n") data += "--AaB03x--\r" files = [] @@ -1084,14 +1417,14 @@ EOF input = <<EOF --AaB03x\r content-disposition: form-data; name="fileupload"; filename="foo.jpg"\r -Content-Type: image/jpeg\r -Content-Transfer-Encoding: base64\r +content-type: image/jpeg\r +content-transfer-encoding: base64\r \r /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r --AaB03x\r content-disposition: form-data; name="fileupload"; filename="bar.jpg"\r -Content-Type: image/jpeg\r -Content-Transfer-Encoding: base64\r +content-type: image/jpeg\r +content-transfer-encoding: base64\r \r /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r --AaB03x--\r @@ -1161,9 +1494,9 @@ EOF it "correctly parse the part name from Content-Id header" do input = <<EOF --AaB03x\r -Content-Type: text/xml; charset=utf-8\r +content-type: text/xml; charset=utf-8\r Content-Id: <soap-start>\r -Content-Transfer-Encoding: 7bit\r +content-transfer-encoding: 7bit\r \r foo\r --AaB03x--\r @@ -1201,19 +1534,23 @@ EOF rack_input.write(input) rack_input.rewind - req = make_request Rack::MockRequest.env_for("/", - "rack.request.form_hash" => { 'foo' => 'bar' }, - "rack.request.form_input" => rack_input, - :input => rack_input) + form_hash = {} + + req = make_request Rack::MockRequest.env_for( + "/", + "rack.request.form_hash" => form_hash, + "rack.request.form_input" => rack_input, + :input => rack_input + ) - req.POST.must_equal req.env['rack.request.form_hash'] + req.POST.must_be_same_as form_hash end it "conform to the Rack spec" do 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.dup @@ -1223,8 +1560,8 @@ content-disposition: form-data; name="reply"\r yes\r --AaB03x\r content-disposition: form-data; name="fileupload"; filename="dj.jpg"\r -Content-Type: image/jpeg\r -Content-Transfer-Encoding: base64\r +content-type: image/jpeg\r +content-transfer-encoding: base64\r \r /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r --AaB03x--\r @@ -1290,7 +1627,7 @@ EOF res.body.must_equal 'fe80::202:b3ff:fe1e:8329' res = mock.get '/', 'REMOTE_ADDR' => '1.2.3.4,3.4.5.6' - res.body.must_equal '1.2.3.4' + res.body.must_equal '3.4.5.6' res = mock.get '/', 'REMOTE_ADDR' => '127.0.0.1' res.body.must_equal '127.0.0.1' @@ -1302,6 +1639,21 @@ EOF it 'deals with proxies' do mock = Rack::MockRequest.new(Rack::Lint.new(ip_app)) + res = mock.get '/', + 'REMOTE_ADDR' => '1.2.3.4', + 'HTTP_FORWARDED' => 'for=3.4.5.6' + res.body.must_equal '1.2.3.4' + + res = mock.get '/', + 'HTTP_X_FORWARDED_FOR' => '3.4.5.6', + 'HTTP_FORWARDED' => 'for=5.6.7.8' + res.body.must_equal '5.6.7.8' + + res = mock.get '/', + 'HTTP_X_FORWARDED_FOR' => '3.4.5.6', + 'HTTP_FORWARDED' => 'for=5.6.7.8, for=7.8.9.0' + res.body.must_equal '7.8.9.0' + res = mock.get '/', 'REMOTE_ADDR' => '1.2.3.4', 'HTTP_X_FORWARDED_FOR' => '3.4.5.6' @@ -1336,6 +1688,9 @@ EOF res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '[2001:db8:cafe::17]:47011' res.body.must_equal '2001:db8:cafe::17' + res = mock.get '/', 'HTTP_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' @@ -1431,14 +1786,21 @@ EOF it "regards local addresses as proxies" do req = make_request(Rack::MockRequest.env_for("/")) req.trusted_proxy?('127.0.0.1').must_equal true + req.trusted_proxy?('127.000.000.001').must_equal true + req.trusted_proxy?('127.0.0.6').must_equal true + req.trusted_proxy?('127.0.0.30').must_equal true req.trusted_proxy?('10.0.0.1').must_equal true + req.trusted_proxy?('10.000.000.001').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?('172.31.000.001').must_equal true req.trusted_proxy?('192.168.0.1').must_equal true + req.trusted_proxy?('192.168.000.001').must_equal true req.trusted_proxy?('::1').must_equal true req.trusted_proxy?('fd00::').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 @@ -1446,15 +1808,35 @@ EOF 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?("127.256.0.1").must_equal false + req.trusted_proxy?("127.0.256.1").must_equal false + req.trusted_proxy?("127.0.0.256").must_equal false + req.trusted_proxy?('127.0.0.300').must_equal false + req.trusted_proxy?("10.256.0.1").must_equal false + req.trusted_proxy?("10.0.256.1").must_equal false + req.trusted_proxy?("10.0.0.256").must_equal false req.trusted_proxy?("11.0.0.1").must_equal false + req.trusted_proxy?("11.000.000.001").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?("172.16.256.1").must_equal false + req.trusted_proxy?("172.16.0.256").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 req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) - assert_equal Hash.new, req.session + session = req.session + assert_equal Hash.new, session + req.env['rack.session'].must_be_same_as session + end + + it "sets the default session options to an empty hash" do + req = make_request(Rack::MockRequest.env_for("http://example.com:8080/")) + session_options = req.session_options + assert_equal Hash.new, session_options + req.env['rack.session.options'].must_be_same_as session_options + assert_equal Hash.new, req.session_options end class MyRequest < Rack::Request @@ -1506,6 +1888,30 @@ EOF end } + (24..27).each do |exp| + length = 2**exp + it "handles ASCII NUL input of #{length} bytes" do + mr = Rack::MockRequest.env_for("/", + "REQUEST_METHOD" => 'POST', + :input => "\0"*length) + req = make_request mr + req.query_string.must_equal "" + req.GET.must_be :empty? + keys = req.POST.keys + keys.length.must_equal 1 + keys.first.length.must_equal(length-1) + keys.first.must_equal("\0"*(length-1)) + end + end + + it "Env sets @env on initialization" do + c = Class.new do + include Rack::Request::Env + end + h = {} + c.new(h).env.must_be_same_as h + end + class NonDelegate < Rack::Request def delegate?; false; end end @@ -1519,7 +1925,7 @@ EOF include Rack::Request::Helpers extend Forwardable - def_delegators :@req, :has_header?, :get_header, :fetch_header, + def_delegators :@req, :env, :has_header?, :get_header, :fetch_header, :each_header, :set_header, :add_header, :delete_header def_delegators :@req, :[], :[]=, :values_at @@ -1529,8 +1935,6 @@ EOF end def delegate?; true; end - - def env; @req.env.dup; end end def make_request(env) diff --git a/test/spec_response.rb b/test/spec_response.rb index 1dfafcdb5ff68e01f47569a5eedbc23137a43b33..ef8aa481c11fb66bf5b141443b4c9fa293bfeb54 100644 --- a/test/spec_response.rb +++ b/test/spec_response.rb @@ -2,7 +2,15 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/response' +end + describe Rack::Response do + deprecated "#header returns headers" do + Rack::Response[200, { "v" => "1" }, []].header['v'].must_equal '1' + end + it 'has standard constructor' do headers = { "header" => "value" } body = ["body"] @@ -19,7 +27,7 @@ describe Rack::Response do cc = 'foo' response.cache_control = cc assert_equal cc, response.cache_control - assert_equal cc, response.to_a[1]['Cache-Control'] + assert_equal cc, response.to_a[1]['cache-control'] end it 'has an etag method' do @@ -27,7 +35,7 @@ describe Rack::Response do etag = 'foo' response.etag = etag assert_equal etag, response.etag - assert_equal etag, response.to_a[1]['ETag'] + assert_equal etag, response.to_a[1]['etag'] end it 'has a content-type method' do @@ -35,7 +43,7 @@ describe Rack::Response do content_type = 'foo' response.content_type = content_type assert_equal content_type, response.content_type - assert_equal content_type, response.to_a[1]['Content-Type'] + assert_equal content_type, response.to_a[1]['content-type'] end it "have sensible default values" do @@ -43,7 +51,7 @@ describe Rack::Response do status, header, body = response.finish status.must_equal 200 header.must_equal({}) - body.each { |part| + response.each { |part| part.must_equal "" } @@ -56,7 +64,7 @@ describe Rack::Response do } end - it "can be written to inside finish block, but does not update Content-Length" do + it "can be written to inside finish block, but does not update content-length" do response = Rack::Response.new('foo') response.write "bar" @@ -68,139 +76,156 @@ describe Rack::Response do body.each { |part| parts << part } parts.must_equal ["foo", "bar", "baz"] - h['Content-Length'].must_equal '6' + h['content-length'].must_equal '6' + end + + it "#write calls #<< on non-iterable body" do + content = [] + body = proc{|x| content << x} + body.singleton_class.class_eval{alias << call} + response = Rack::Response.new(body) + response.write "bar" + content.must_equal ["bar"] end it "can set and read headers" do response = Rack::Response.new - response["Content-Type"].must_be_nil - response["Content-Type"] = "text/plain" - response["Content-Type"].must_equal "text/plain" + response["content-type"].must_be_nil + response["content-type"] = "text/plain" + response["content-type"].must_equal "text/plain" end it "doesn't mutate given headers" do - headers = {} + headers = {}.freeze response = Rack::Response.new([], 200, headers) - response.headers["Content-Type"] = "text/plain" - response.headers["Content-Type"].must_equal "text/plain" + response.headers["content-type"] = "text/plain" + response.headers["content-type"].must_equal "text/plain" - headers.wont_include("Content-Type") + headers.wont_include("content-type") end - it "can override the initial Content-Type with a different case" do + 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" + response["content-type"].must_equal "text/plain" + end + + it "can get and set set-cookie header" do + response = Rack::Response.new + response.set_cookie_header.must_be_nil + response.set_cookie_header = 'v=1;' + response.set_cookie_header.must_equal 'v=1;' + response.headers['set-cookie'].must_equal 'v=1;' end it "can set cookies" do response = Rack::Response.new response.set_cookie "foo", "bar" - response["Set-Cookie"].must_equal "foo=bar" + response["set-cookie"].must_equal "foo=bar" response.set_cookie "foo2", "bar2" - response["Set-Cookie"].must_equal ["foo=bar", "foo2=bar2"].join("\n") + response["set-cookie"].must_equal ["foo=bar", "foo2=bar2"] response.set_cookie "foo3", "bar3" - response["Set-Cookie"].must_equal ["foo=bar", "foo2=bar2", "foo3=bar3"].join("\n") + response["set-cookie"].must_equal ["foo=bar", "foo2=bar2", "foo3=bar3"] end 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"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n") + response["set-cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"] 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"].must_match( + 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"].must_equal "foo=bar; secure" + 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"].must_equal "foo=bar; HttpOnly" + 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"].must_equal "foo=bar; HttpOnly" + 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"].must_equal "foo=bar" + 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" + 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" + 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" + 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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar; SameSite=Strict" + response["set-cookie"].must_equal "foo=bar; SameSite=Strict" end it "validates the SameSite option value" do @@ -214,14 +239,14 @@ describe Rack::Response do 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"].must_equal "foo=bar; SameSite=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"].must_equal "foo=bar" + response["set-cookie"].must_equal "foo=bar" end end @@ -230,135 +255,131 @@ describe Rack::Response do response.set_cookie "foo", "bar" response.set_cookie "foo2", "bar2" response.delete_cookie "foo" - response["Set-Cookie"].must_equal [ + response["set-cookie"].must_equal [ + "foo=bar", "foo2=bar2", "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"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n") + response["set-cookie"].must_equal [ + "foo=bar; domain=sample.example.com", + "foo=bar; domain=.example.com" + ] + 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["set-cookie"].must_equal [ + "foo=bar; domain=sample.example.com", + "foo=bar; domain=.example.com", + "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] + response.delete_cookie "foo", domain: "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") + response["set-cookie"].must_equal [ + "foo=bar; domain=sample.example.com", + "foo=bar; domain=.example.com", + "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" + ] end it "only deletes cookies for the domain specified" do response = Rack::Response.new response.set_cookie "foo", { value: "bar", domain: "example.com.example.com" } response.set_cookie "foo", { value: "bar", domain: "example.com" } - response["Set-Cookie"].must_equal ["foo=bar; domain=example.com.example.com", "foo=bar; domain=example.com"].join("\n") - response.delete_cookie "foo", domain: "example.com" - response["Set-Cookie"].must_equal ["foo=bar; domain=example.com.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: "example.com.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=example.com.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") + response["set-cookie"].must_equal [ + "foo=bar; domain=example.com.example.com", + "foo=bar; domain=example.com" + ] + + response.delete_cookie "foo", { domain: "example.com" } + response["set-cookie"].must_equal [ + "foo=bar; domain=example.com.example.com", + "foo=bar; domain=example.com", + "foo=; domain=example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] + + response.delete_cookie "foo", { domain: "example.com.example.com" } + response["set-cookie"].must_equal [ + "foo=bar; domain=example.com.example.com", + "foo=bar; domain=example.com", + "foo=; domain=example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", + "foo=; domain=example.com.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] 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"].must_equal ["foo=bar; path=/", - "foo=bar; path=/path"].join("\n") + + response["set-cookie"].must_equal [ + "foo=bar; path=/", + "foo=bar; 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 GMT"].join("\n") + response["set-cookie"].must_equal [ + "foo=bar; path=/", + "foo=bar; path=/path", + "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] end it "only delete cookies with the path specified" do response = Rack::Response.new - response.set_cookie "foo", value: "bar", path: "/" - response.set_cookie "foo", value: "bar", path: "/a" response.set_cookie "foo", value: "bar", path: "/a/b" - response["Set-Cookie"].must_equal ["foo=bar; path=/", - "foo=bar; path=/a", - "foo=bar; path=/a/b"].join("\n") + response["set-cookie"].must_equal( + "foo=bar; path=/a/b" + ) response.delete_cookie "foo", path: "/a" - response["Set-Cookie"].must_equal ["foo=bar; path=/", - "foo=bar; path=/a/b", - "foo=; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n") + response["set-cookie"].must_equal [ + "foo=bar; path=/a/b", + "foo=; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] end it "only delete cookies with the domain and path specified" do response = Rack::Response.new - response.set_cookie "foo", value: "bar", path: "/" - response.set_cookie "foo", value: "bar", path: "/a" - response.set_cookie "foo", value: "bar", path: "/a/b" - response.set_cookie "foo", value: "bar", path: "/", domain: "example.com.example.com" - response.set_cookie "foo", value: "bar", path: "/a", domain: "example.com.example.com" - response.set_cookie "foo", value: "bar", path: "/a/b", domain: "example.com.example.com" - response.set_cookie "foo", value: "bar", path: "/", domain: "example.com" - response.set_cookie "foo", value: "bar", path: "/a", domain: "example.com" - response.set_cookie "foo", value: "bar", path: "/a/b", domain: "example.com" - response["Set-Cookie"].must_equal [ - "foo=bar; path=/", - "foo=bar; path=/a", - "foo=bar; path=/a/b", - "foo=bar; domain=example.com.example.com; path=/", - "foo=bar; domain=example.com.example.com; path=/a", - "foo=bar; domain=example.com.example.com; path=/a/b", - "foo=bar; domain=example.com; path=/", - "foo=bar; domain=example.com; path=/a", - "foo=bar; domain=example.com; path=/a/b", - ].join("\n") - response.delete_cookie "foo", path: "/a", domain: "example.com" - response["Set-Cookie"].must_equal [ - "foo=bar; path=/", - "foo=bar; path=/a", - "foo=bar; path=/a/b", - "foo=bar; domain=example.com.example.com; path=/", - "foo=bar; domain=example.com.example.com; path=/a", - "foo=bar; domain=example.com.example.com; path=/a/b", - "foo=bar; domain=example.com; path=/", - "foo=bar; domain=example.com; path=/a/b", + response["set-cookie"].must_equal( "foo=; domain=example.com; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", - ].join("\n") + ) response.delete_cookie "foo", path: "/a/b", domain: "example.com" - response["Set-Cookie"].must_equal [ - "foo=bar; path=/", - "foo=bar; path=/a", - "foo=bar; path=/a/b", - "foo=bar; domain=example.com.example.com; path=/", - "foo=bar; domain=example.com.example.com; path=/a", - "foo=bar; domain=example.com.example.com; path=/a/b", - "foo=bar; domain=example.com; path=/", + response["set-cookie"].must_equal [ "foo=; domain=example.com; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", "foo=; domain=example.com; path=/a/b; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", - ].join("\n") + ] end it "can do redirects" do response = Rack::Response.new response.redirect "/foo" - status, header, body = response.finish + status, header = response.finish status.must_equal 302 - header["Location"].must_equal "/foo" + header["location"].must_equal "/foo" response = Rack::Response.new response.redirect "/foo", 307 - status, header, body = response.finish + status, = response.finish status.must_equal 307 end it "has a useful constructor" do r = Rack::Response.new("foo") - status, header, body = r.finish + body = r.finish[2] str = "".dup; body.each { |part| str << part } str.must_equal "foo" r = Rack::Response.new(["foo", "bar"]) - status, header, body = r.finish + body = r.finish[2] str = "".dup; body.each { |part| str << part } str.must_equal "foobar" @@ -369,7 +390,7 @@ describe Rack::Response do end r = Rack::Response.new(object_with_each) r.write "foo" - status, header, body = r.finish + body = r.finish[2] str = "".dup; body.each { |part| str << part } str.must_equal "foobarfoo" @@ -391,7 +412,7 @@ describe Rack::Response do status.must_equal 404 end - it "correctly updates Content-Type when writing when not initialized with body" do + it "correctly updates content-type when writing when not initialized with body" do r = Rack::Response.new r.write('foo') r.write('bar') @@ -399,10 +420,10 @@ describe Rack::Response do _, header, body = r.finish str = "".dup; body.each { |part| str << part } str.must_equal "foobarbaz" - header['Content-Length'].must_equal '9' + header['content-length'].must_equal '9' end - it "correctly updates Content-Type when writing when initialized with body" do + it "correctly updates content-type when writing when initialized with body" do obj = Object.new def obj.each yield 'foo' @@ -414,7 +435,7 @@ describe Rack::Response do _, header, body = r.finish str = "".dup; body.each { |part| str << part } str.must_equal "foobarbaz" - header['Content-Length'].must_equal '9' + header['content-length'].must_equal '9' end end @@ -423,8 +444,8 @@ describe Rack::Response do _, header, body = r.finish str = "".dup; body.each { |part| str << part } str.must_be :empty? - header["Content-Type"].must_be_nil - header['Content-Length'].must_be_nil + header["content-type"].must_be_nil + header['content-length'].must_be_nil lambda { Rack::Response.new(Object.new).each{} @@ -503,6 +524,16 @@ describe Rack::Response do res.must_be :client_error? res.must_be :method_not_allowed? + res.status = 406 + res.wont_be :successful? + res.must_be :client_error? + res.must_be :not_acceptable? + + res.status = 408 + res.wont_be :successful? + res.must_be :client_error? + res.must_be :request_timeout? + res.status = 412 res.wont_be :successful? res.must_be :client_error? @@ -520,11 +551,11 @@ describe Rack::Response do it "provide access to the HTTP headers" do res = Rack::Response.new - res["Content-Type"] = "text/yaml; charset=UTF-8" + res["content-type"] = "text/yaml; charset=UTF-8" - res.must_include "Content-Type" - res.headers["Content-Type"].must_equal "text/yaml; charset=UTF-8" - res["Content-Type"].must_equal "text/yaml; charset=UTF-8" + res.must_include "content-type" + res.headers["content-type"].must_equal "text/yaml; charset=UTF-8" + res["content-type"].must_equal "text/yaml; charset=UTF-8" res.content_type.must_equal "text/yaml; charset=UTF-8" res.media_type.must_equal "text/yaml" res.media_type_params.must_equal "charset" => "UTF-8" @@ -532,27 +563,27 @@ describe Rack::Response do res.location.must_be_nil end - it "does not add or change Content-Length when #finish()ing" do + it "does not add or change content-length when #finish()ing" do res = Rack::Response.new res.status = 200 res.finish - res.headers["Content-Length"].must_be_nil + res.headers["content-length"].must_be_nil res = Rack::Response.new res.status = 200 - res.headers["Content-Length"] = "10" + res.headers["content-length"] = "10" res.finish - res.headers["Content-Length"].must_equal "10" + res.headers["content-length"].must_equal "10" end - it "updates Content-Length when body appended to using #write" do + it "updates content-length when body appended to using #write" do res = Rack::Response.new res.status = 200 - res.headers["Content-Length"].must_be_nil + res.headers["content-length"].must_be_nil res.write "Hi" - res.headers["Content-Length"].must_equal "2" + res.headers["content-length"].must_equal "2" res.write " there" - res.headers["Content-Length"].must_equal "8" + res.headers["content-length"].must_equal "8" end it "does not wrap body" do @@ -607,10 +638,20 @@ describe Rack::Response do res.body = StringIO.new res.status = 205 - _, _, b = res.finish + res.finish res.body.wont_be :closed? end + it "doesn't clear #body when 101 and streaming" do + res = Rack::Response.new + + streaming_body = proc{|stream| stream.close} + res.body = streaming_body + res.status = 101 + res.finish + res.body.must_equal streaming_body + end + it "flatten doesn't cause infinite loop" do # https://github.com/rack/rack/issues/419 res = Rack::Response.new("Hello World") @@ -624,9 +665,21 @@ describe Rack::Response do response.cache!(1000) response.do_not_cache! - expect(response['Cache-Control']).must_equal "no-cache, must-revalidate" + expect(response['cache-control']).must_equal "no-cache, must-revalidate" - expires_header = Time.parse(response['Expires']) + expires_header = Time.parse(response['expires']) + expect(expires_header).must_be :<=, Time.now + end + + it "should not cache content if calling cache! after do_not_cache!" do + response = Rack::Response.new + + response.do_not_cache! + response.cache!(1000) + + expect(response['cache-control']).must_equal "no-cache, must-revalidate" + + expires_header = Time.parse(response['expires']) expect(expires_header).must_be :<=, Time.now end @@ -637,77 +690,115 @@ describe Rack::Response do expires = Time.now + 100 # At least this far into the future response.cache!(duration) - expect(response['Cache-Control']).must_equal "public, max-age=120" + expect(response['cache-control']).must_equal "public, max-age=120" - expires_header = Time.parse(response['Expires']) + expires_header = Time.parse(response['expires']) expect(expires_header).must_be :>=, expires end end describe Rack::Response, 'headers' do before do - @response = Rack::Response.new([], 200, { 'Foo' => '1' }) + @response = Rack::Response.new([], 200, { 'foo' => '1' }) end it 'has_header?' do - lambda { @response.has_header? nil }.must_raise NoMethodError + lambda { @response.has_header? nil }.must_raise ArgumentError - @response.has_header?('Foo').must_equal true @response.has_header?('foo').must_equal true end it 'get_header' do - lambda { @response.get_header nil }.must_raise NoMethodError + lambda { @response.get_header nil }.must_raise ArgumentError - @response.get_header('Foo').must_equal '1' @response.get_header('foo').must_equal '1' end it 'set_header' do - lambda { @response.set_header nil, '1' }.must_raise NoMethodError + lambda { @response.set_header nil, '1' }.must_raise ArgumentError - @response.set_header('Foo', '2').must_equal '2' - @response.has_header?('Foo').must_equal true - @response.get_header('Foo').must_equal('2') + @response.set_header('foo', '2').must_equal '2' + @response.has_header?('foo').must_equal true + @response.get_header('foo').must_equal('2') - @response.set_header('Foo', nil).must_be_nil - @response.has_header?('Foo').must_equal true - @response.get_header('Foo').must_be_nil + @response.set_header('foo', nil).must_be_nil + @response.get_header('foo').must_be_nil end it 'add_header' do - lambda { @response.add_header nil, '1' }.must_raise NoMethodError + lambda { @response.add_header nil, '1' }.must_raise ArgumentError # Add a value to an existing header - @response.add_header('Foo', '2').must_equal '1,2' - @response.get_header('Foo').must_equal '1,2' + @response.add_header('foo', '2').must_equal ["1", "2"] + @response.get_header('foo').must_equal ["1", "2"] # Add nil to an existing header - @response.add_header('Foo', nil).must_equal '1,2' - @response.get_header('Foo').must_equal '1,2' + @response.add_header('foo', nil).must_equal ["1", "2"] + @response.get_header('foo').must_equal ["1", "2"] # Add nil to a nonexistent header - @response.add_header('Bar', nil).must_be_nil - @response.has_header?('Bar').must_equal false - @response.get_header('Bar').must_be_nil + @response.add_header('bar', nil).must_be_nil + @response.has_header?('bar').must_equal false + @response.get_header('bar').must_be_nil # Add a value to a nonexistent header - @response.add_header('Bar', '1').must_equal '1' - @response.has_header?('Bar').must_equal true - @response.get_header('Bar').must_equal '1' + @response.add_header('bar', '1').must_equal '1' + @response.has_header?('bar').must_equal true + @response.get_header('bar').must_equal '1' end it 'delete_header' do - lambda { @response.delete_header nil }.must_raise NoMethodError + lambda { @response.delete_header nil }.must_raise ArgumentError + + @response.delete_header('foo').must_equal '1' + @response.has_header?('foo').must_equal false - @response.delete_header('Foo').must_equal '1' - (!!@response.has_header?('Foo')).must_equal false + @response.delete_header('foo').must_be_nil + @response.has_header?('foo').must_equal false + + @response.set_header('foo', 1) + @response.delete_header('foo').must_equal 1 + @response.has_header?('foo').must_equal false + end +end + +describe Rack::Response::Raw do + before do + @response = Rack::Response::Raw.new(200, { 'foo' => '1' }) + end + + it 'has_header?' do + @response.has_header?('foo').must_equal true + @response.has_header?(nil).must_equal false + end + + it 'get_header' do + @response.get_header('foo').must_equal '1' + @response.get_header(nil).must_be_nil + end + + it 'set_header' do + + @response.set_header('foo', '2').must_equal '2' + @response.has_header?('foo').must_equal true + @response.get_header('foo').must_equal('2') + + @response.set_header(nil, '1').must_equal '1' + @response.get_header(nil).must_equal '1' + + @response.set_header('foo', nil).must_be_nil + @response.get_header('foo').must_be_nil + end + + it 'delete_header' do + @response.delete_header('foo').must_equal '1' + @response.has_header?('foo').must_equal false - @response.delete_header('Foo').must_be_nil - @response.has_header?('Foo').must_equal false + @response.delete_header('foo').must_be_nil + @response.has_header?('foo').must_equal false - @response.set_header('Foo', 1) + @response.set_header('foo', 1) @response.delete_header('foo').must_equal 1 - @response.has_header?('Foo').must_equal false + @response.has_header?('foo').must_equal false end end diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb index 4efe7dc29c1658096c1707ebbbccf435c9948d51..603afbda934d8caf1f8f3d58ddccd92fe23ff1df 100644 --- a/test/spec_rewindable_input.rb +++ b/test/spec_rewindable_input.rb @@ -2,6 +2,10 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/rewindable_input' +end + module RewindableTest extend Minitest::Spec::DSL @@ -9,10 +13,6 @@ module RewindableTest @rio = Rack::RewindableInput.new(@io) end - class << self # HACK to get this running w/ as few changes as possible - alias_method :should, :it - end - it "be able to handle to read()" do @rio.read.must_equal "hello world" end @@ -40,13 +40,23 @@ module RewindableTest end it "rewind to the beginning when #rewind is called" do - @rio.read(1) + @rio.rewind + @rio.read(1).must_equal 'h' @rio.rewind @rio.read.must_equal "hello world" end it "be able to handle gets" do @rio.gets.must_equal "hello world" + @rio.rewind + @rio.gets.must_equal "hello world" + end + + it "be able to handle size" do + @rio.size.must_equal "hello world".size + @rio.size.must_equal "hello world".size + @rio.rewind + @rio.gets.must_equal "hello world" end it "be able to handle each" do @@ -55,6 +65,13 @@ module RewindableTest array << data end array.must_equal ["hello world"] + + @rio.rewind + array = [] + @rio.each do |data| + array << data + end + array.must_equal ["hello world"] end it "not buffer into a Tempfile if no data has been read yet" do @@ -147,3 +164,12 @@ describe Rack::RewindableInput do include RewindableTest end end + +describe Rack::RewindableInput::Middleware do + it "wraps rack.input in RewindableInput" do + app = proc{|env| [200, {}, [env['rack.input'].class.to_s]]} + app.call('rack.input'=>StringIO.new(''))[2].must_equal ['StringIO'] + app = Rack::RewindableInput::Middleware.new(app) + app.call('rack.input'=>StringIO.new(''))[2].must_equal ['Rack::RewindableInput'] + end +end diff --git a/test/spec_runtime.rb b/test/spec_runtime.rb index e4fc3f95a8c4f7e72f51be902395b61403c34ad9..5f23dce742cc7efdb5390816358ea928d67460bf 100644 --- a/test/spec_runtime.rb +++ b/test/spec_runtime.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/runtime' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Runtime do def runtime_app(app, *args) Rack::Lint.new Rack::Runtime.new(app, *args) @@ -11,32 +17,26 @@ describe Rack::Runtime do Rack::MockRequest.env_for end - it "works even if headers is an array" do - 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 "sets X-Runtime is none is set" do - app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] } + it "sets x-runtime is none is set" do + app = lambda { |env| [200, { 'content-type' => 'text/plain' }, "Hello, World!"] } response = runtime_app(app).call(request) - response[1]['X-Runtime'].must_match(/[\d\.]+/) + 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!"] } + 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!"] } response = runtime_app(app).call(request) - response[1]['X-Runtime'].must_equal "foobar" + 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\.]+/) + 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 @@ -47,9 +47,9 @@ describe Rack::Runtime do response = runtime.call(request) - response[1]['X-Runtime-App'].must_match(/[\d\.]+/) - response[1]['X-Runtime-All'].must_match(/[\d\.]+/) + response[1]['x-runtime-app'].must_match(/[\d\.]+/) + response[1]['x-runtime-all'].must_match(/[\d\.]+/) - Float(response[1]['X-Runtime-All']).must_be :>, Float(response[1]['X-Runtime-App']) + Float(response[1]['x-runtime-all']).must_be :>, Float(response[1]['x-runtime-app']) end end diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb index 09e810e9b6cc6ca6a324fde7c71960248abf8940..5205b671cd7cc9881f38ae3659a33c4bec01555e 100644 --- a/test/spec_sendfile.rb +++ b/test/spec_sendfile.rb @@ -4,6 +4,12 @@ require_relative 'helper' require 'fileutils' require 'tmpdir' +separate_testing do + require_relative '../lib/rack/sendfile' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::Sendfile do def sendfile_body(filename = "rack_sendfile") FileUtils.touch File.join(Dir.tmpdir, filename) @@ -13,7 +19,7 @@ describe Rack::Sendfile do end def simple_app(body = sendfile_body) - lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] } + lambda { |env| [200, { 'content-type' => 'text/plain' }, body] } end def sendfile_app(body, mappings = []) @@ -32,87 +38,100 @@ describe Rack::Sendfile do end.open(path, 'wb+') end - it "does nothing when no X-Sendfile-Type header present" do + it "does nothing when no x-sendfile-type header present" do request do |response| response.must_be :ok? response.body.must_equal 'Hello World' - response.headers.wont_include 'X-Sendfile' + response.headers.wont_include 'x-sendfile' end end - it "does nothing and logs to rack.errors when incorrect X-Sendfile-Type header present" do + it "does nothing and logs to rack.errors when incorrect x-sendfile-type header present" do io = StringIO.new request 'HTTP_X_SENDFILE_TYPE' => 'X-Banana', 'rack.errors' => io do |response| response.must_be :ok? response.body.must_equal 'Hello World' - response.headers.wont_include 'X-Sendfile' + response.headers.wont_include 'x-sendfile' io.rewind io.read.must_equal "Unknown x-sendfile variation: 'X-Banana'.\n" end end - it "sets X-Sendfile response header and discards body" do - request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response| + it "sets x-sendfile response header and discards body" do + request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response| + response.must_be :ok? + response.body.must_be :empty? + response.headers['content-length'].must_equal '0' + response.headers['x-sendfile'].must_equal File.join(Dir.tmpdir, "rack_sendfile") + end + end + + it "closes body when x-sendfile used" do + body = sendfile_body + closed = false + body.define_singleton_method(:close){closed = true} + request({'HTTP_X_SENDFILE_TYPE' => 'x-sendfile'}, body) do |response| response.must_be :ok? response.body.must_be :empty? - response.headers['Content-Length'].must_equal '0' - response.headers['X-Sendfile'].must_equal File.join(Dir.tmpdir, "rack_sendfile") + response.headers['content-length'].must_equal '0' + response.headers['x-sendfile'].must_equal File.join(Dir.tmpdir, "rack_sendfile") end + closed.must_equal true end - it "sets X-Lighttpd-Send-File response header and discards body" do - request 'HTTP_X_SENDFILE_TYPE' => 'X-Lighttpd-Send-File' do |response| + it "sets x-lighttpd-send-file response header and discards body" do + request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response| response.must_be :ok? response.body.must_be :empty? - response.headers['Content-Length'].must_equal '0' - response.headers['X-Lighttpd-Send-File'].must_equal File.join(Dir.tmpdir, "rack_sendfile") + response.headers['content-length'].must_equal '0' + response.headers['x-lighttpd-send-file'].must_equal File.join(Dir.tmpdir, "rack_sendfile") end end - it "sets X-Accel-Redirect response header and discards body" do + it "sets x-accel-redirect response header and discards body" do headers = { - 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', + 'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect', 'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar/" } request headers 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' + response.headers['content-length'].must_equal '0' + response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile' end end - it "sets X-Accel-Redirect response header to percent-encoded path" do + it "sets x-accel-redirect response header to percent-encoded path" do headers = { - 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', + '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' + 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| + 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? response.body.must_equal 'Hello World' - response.headers.wont_include 'X-Accel-Redirect' - response.errors.must_include 'X-Accel-Mapping' + response.headers.wont_include 'x-accel-redirect' + response.errors.must_include 'x-accel-mapping' end 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' + response.headers.wont_include 'x-sendfile' end end - it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings" do + it "sets x-accel-redirect response header and discards body when initialized with multiple mappings" do begin dir1 = Dir.mktmpdir dir2 = Dir.mktmpdir @@ -128,18 +147,18 @@ 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' + 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({ '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' + response.headers['content-length'].must_equal '0' + response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile' end ensure FileUtils.remove_entry_secure dir1 @@ -147,7 +166,7 @@ describe Rack::Sendfile do end end - it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings via header" do + 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 @@ -163,29 +182,29 @@ describe Rack::Sendfile do third_body.puts 'hello again world' headers = { - 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect', + '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' + response.headers['content-length'].must_equal '0' + response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile' end request(headers, second_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 '/wibble/rack_sendfile' + response.headers['content-length'].must_equal '0' + response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile' end request(headers, third_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 "#{dir3}/rack_sendfile" + response.headers['content-length'].must_equal '0' + response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile" end ensure FileUtils.remove_entry_secure dir1 diff --git a/test/spec_server.rb b/test/spec_server.rb deleted file mode 100644 index 34912b4d8e2f8f473b6a56851b9e38e5307ff753..0000000000000000000000000000000000000000 --- a/test/spec_server.rb +++ /dev/null @@ -1,471 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'tempfile' -require 'socket' -require 'webrick' -require 'open-uri' -require 'net/http' -require 'net/https' - -module Minitest::Spec::DSL - alias :should :it -end - -describe Rack::Server do - SPEC_ARGV = [] - - before { SPEC_ARGV[0..-1] = [] } - - def app - lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['success']] } - end - - def with_stderr - old, $stderr = $stderr, StringIO.new - yield $stderr - ensure - $stderr = old - end - - it "overrides :config if :app is passed in" do - 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.app.class.must_equal Proc - Rack::MockRequest.new(server.app).get("/").body.to_s.must_equal 'success' - end - - 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 [] - 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 [] - end - - it "only provide default middleware for development and deployment environments" do - Rack::Server.default_middleware_by_environment.keys.sort.must_equal %w(deployment development) - end - - it "always return an empty array for unknown environments" do - 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.middleware['deployment'].flatten.wont_include Rack::Lint - end - - it "not include Rack::ShowExceptions in deployment environment" do - 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.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.server.name =~ /CGI/ - Rack::Server.logging_middleware.call(server).must_be_nil - ensure - ENV['REQUEST_METHOD'] = o - end - end - - it "be quiet if said so" do - server = Rack::Server.new(app: "FOO", quiet: true) - Rack::Server.logging_middleware.call(server).must_be_nil - end - - it "use a full path to the pidfile" do - # avoids issues with daemonize chdir - opts = Rack::Server.new.send(:parse_options, %w[--pid testing.pid]) - opts[:pid].must_equal ::File.expand_path('testing.pid') - end - - it "get options from ARGV" do - SPEC_ARGV[0..-1] = ['--debug', '-sthin', '--env', 'production', '-w', '-q', '-o', '127.0.0.1', '-O', 'NAME=VALUE', '-ONAME2', '-D'] - server = Rack::Server.new - server.options[:debug].must_equal true - server.options[:server].must_equal 'thin' - server.options[:environment].must_equal 'production' - server.options[:warn].must_equal true - server.options[:quiet].must_equal true - server.options[:Host].must_equal '127.0.0.1' - server.options[:NAME].must_equal 'VALUE' - server.options[:NAME2].must_equal true - server.options[:daemonize].must_equal true - end - - it "only override non-passed options from parsed .ru file" do - builder_file = File.join(File.dirname(__FILE__), 'builder', 'options.ru') - SPEC_ARGV[0..-1] = ['--debug', '-sthin', '--env', 'production', builder_file] - server = Rack::Server.new - server.app # force .ru file to be parsed - - server.options[:debug].must_equal true - server.options[:server].must_equal 'thin' - server.options[:environment].must_equal 'production' - server.options[:Port].must_equal '2929' - end - - def test_options_server(*args) - SPEC_ARGV[0..-1] = args - output = String.new - server = Class.new(Rack::Server) do - define_method(:opt_parser) do - Class.new(Rack::Server::Options) do - define_method(:puts) do |*args| - output << args.join("\n") << "\n" - end - alias warn puts - alias abort puts - define_method(:exit) do - output << "exited" - end - end.new - end - end.new - output - end - - it "support -h option to get help" do - test_options_server('-scgi', '-h').must_match(/\AUsage: rackup.*Ruby options:.*Rack options.*Profiling options.*Common options.*exited\z/m) - end - - it "support -h option to get handler-specific help" do - cgi = Rack::Handler.get('cgi') - begin - def cgi.valid_options; { "FOO=BAR" => "BAZ" } end - test_options_server('-scgi', '-h').must_match(/\AUsage: rackup.*Ruby options:.*Rack options.*Profiling options.*Common options.*Server-specific options for Rack::Handler::CGI.*-O +FOO=BAR +BAZ.*exited\z/m) - ensure - cgi.singleton_class.send(:remove_method, :valid_options) - end - end - - it "support -h option to display warning for invalid handler" do - test_options_server('-sbanana', '-h').must_match(/\AUsage: rackup.*Ruby options:.*Rack options.*Profiling options.*Common options.*Warning: Could not find handler specified \(banana\) to determine handler-specific options.*exited\z/m) - end - - it "support -v option to get version" do - test_options_server('-v').must_match(/\ARack \d\.\d \(Release: \d+\.\d+\.\d+(\.\d+)?\)\nexited\z/) - end - - it "warn for invalid --profile-mode option" do - test_options_server('--profile-mode', 'foo').must_match(/\Ainvalid option: --profile-mode unknown profile mode: foo.*Usage: rackup/m) - end - - it "warn for invalid options" do - test_options_server('--banana').must_match(/\Ainvalid option: --banana.*Usage: rackup/m) - end - - it "support -b option to specify inline rackup config" do - SPEC_ARGV[0..-1] = ['-scgi', '-E', 'development', '-b', 'use Rack::ContentLength; run ->(env){[200, {}, []]}'] - server = Rack::Server.new - def (server.server).run(app, **) app end - s, h, b = server.start.call('rack.errors' => StringIO.new) - s.must_equal 500 - h['Content-Type'].must_equal 'text/plain' - b.join.must_include 'Rack::Lint::LintError' - end - - it "support -e option to evaluate ruby code" do - SPEC_ARGV[0..-1] = ['-scgi', '-e', 'Object::XYZ = 2'] - begin - server = Rack::Server.new - Object::XYZ.must_equal 2 - ensure - Object.send(:remove_const, :XYZ) - end - end - - it "abort if config file does not exist" do - SPEC_ARGV[0..-1] = ['-scgi'] - server = Rack::Server.new - def server.abort(s) throw :abort, s end - message = catch(:abort) do - server.start - end - message.must_match(/\Aconfiguration .*config\.ru not found/) - end - - it "support -I option to change the load path and -r to require" do - SPEC_ARGV[0..-1] = ['-scgi', '-Ifoo/bar', '-Itest/load', '-rrack-test-a', '-rrack-test-b'] - begin - server = Rack::Server.new - def (server.server).run(*) end - def server.handle_profiling(*) end - def server.app(*) end - server.start - $LOAD_PATH.must_include('foo/bar') - $LOAD_PATH.must_include('test/load') - $LOADED_FEATURES.must_include(File.join(Dir.pwd, "test/load/rack-test-a.rb")) - $LOADED_FEATURES.must_include(File.join(Dir.pwd, "test/load/rack-test-b.rb")) - ensure - $LOAD_PATH.delete('foo/bar') - $LOAD_PATH.delete('test/load') - $LOADED_FEATURES.delete(File.join(Dir.pwd, "test/load/rack-test-a.rb")) - $LOADED_FEATURES.delete(File.join(Dir.pwd, "test/load/rack-test-b.rb")) - end - end - - it "support -w option to warn and -d option to debug" do - SPEC_ARGV[0..-1] = ['-scgi', '-d', '-w'] - warn = $-w - debug = $DEBUG - begin - server = Rack::Server.new - def (server.server).run(*) end - def server.handle_profiling(*) end - def server.app(*) end - def server.p(*) end - def server.pp(*) end - def server.require(*) end - server.start - $-w.must_equal true - $DEBUG.must_equal true - ensure - $-w = warn - $DEBUG = debug - end - end - - if RUBY_ENGINE == "ruby" - it "support --heap option for heap profiling" do - begin - require 'objspace' - rescue LoadError - else - t = Tempfile.new - begin - SPEC_ARGV[0..-1] = ['-scgi', '--heap', t.path, '-E', 'production', '-b', 'run ->(env){[200, {}, []]}'] - server = Rack::Server.new - def (server.server).run(*) end - def server.exit; throw :exit end - catch :exit do - server.start - end - File.file?(t.path).must_equal true - ensure - File.delete t.path - end - end - end - - it "support --profile-mode option for stackprof profiling" do - begin - require 'stackprof' - rescue LoadError - else - t = Tempfile.new - begin - SPEC_ARGV[0..-1] = ['-scgi', '--profile', t.path, '--profile-mode', 'cpu', '-E', 'production', '-b', 'run ->(env){[200, {}, []]}'] - server = Rack::Server.new - def (server.server).run(*) end - def server.puts(*) end - def server.exit; throw :exit end - catch :exit do - server.start - end - File.file?(t.path).must_equal true - ensure - File.delete t.path - end - end - end - - it "support --profile-mode option for stackprof profiling without --profile option" do - begin - require 'stackprof' - rescue LoadError - else - begin - SPEC_ARGV[0..-1] = ['-scgi', '--profile-mode', 'cpu', '-E', 'production', '-b', 'run ->(env){[200, {}, []]}'] - server = Rack::Server.new - def (server.server).run(*) end - filename = nil - server.define_singleton_method(:make_profile_name) do |fname, &block| - super(fname) do |fn| - filename = fn - block.call(filename) - end - end - def server.puts(*) end - def server.exit; throw :exit end - catch :exit do - server.start - end - File.file?(filename).must_equal true - ensure - File.delete filename - end - end - end - end - - it "support exit for INT signal when server does not respond to shutdown" do - SPEC_ARGV[0..-1] = ['-scgi'] - server = Rack::Server.new - def (server.server).run(*) end - def server.handle_profiling(*) end - def server.app(*) end - exited = false - server.define_singleton_method(:exit) do - exited = true - end - server.start - exited.must_equal false - Process.kill(:INT, $$) - sleep 1 unless RUBY_ENGINE == 'ruby' - exited.must_equal true - end - - it "support support Server.start for starting" do - SPEC_ARGV[0..-1] = ['-scgi'] - c = Class.new(Rack::Server) do - def start(*) [self.class, :started] end - end - c.start.must_equal [c, :started] - end - - - it "run a 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' - ) - t = Thread.new { server.start { |s| Thread.current[:server] = s } } - t.join(0.01) until t[:server] && t[:server].status != :Stop - 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, $$) - t.join - open(pidfile.path) { |f| f.read.must_equal $$.to_s } - end - - it "run a secure 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', - SSLEnable: true, - SSLCertName: [['CN', 'nobody'], ['DC', 'example']] - ) - t = Thread.new { server.start { |s| Thread.current[:server] = s } } - t.join(0.01) until t[:server] && t[:server].status != :Stop - - uri = URI.parse("https://127.0.0.1:#{server.options[:Port]}/") - - Net::HTTP.start("127.0.0.1", uri.port, use_ssl: true, - verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| - - request = Net::HTTP::Get.new uri - - body = http.request(request).body - body.must_equal 'success' - end - - Process.kill(:INT, $$) - t.join - open(pidfile.path) { |f| f.read.must_equal $$.to_s } - end if RUBY_VERSION >= "2.6" - - 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.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.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.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.send(:pidfile_process_status).must_equal :not_owned - end - - it "rewrite pid file when it does not reference a running process" do - pidfile = Tempfile.open('pidfile') { |f| break f }.path - server = Rack::Server.new(pid: pidfile) - ::File.open(pidfile, 'w') { } - server.send(:write_pid) - ::File.read(pidfile).to_i.must_equal $$ - end - - it "not write pid file when it references a running process" do - pidfile = Tempfile.open('pidfile') { |f| break f }.path - ::File.delete(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 - err.rewind - output = err.read - output.must_match(/already running/) - output.must_include pidfile - end - end - - 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) - with_stderr do |err| - lambda { server.start }.must_raise SystemExit - err.rewind - output = err.read - output.must_match(/already running/) - output.must_include pidfile - end - end - -end diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb deleted file mode 100644 index 17cdb3e55cd91855ee980f112eb2f53e3167cb06..0000000000000000000000000000000000000000 --- a/test/spec_session_abstract_id.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -### WARNING: there be hax in this file. - -require 'rack/session/abstract/id' - -describe Rack::Session::Abstract::ID do - attr_reader :id - - def setup - super - @id = Rack::Session::Abstract::ID - end - - it "use securerandom" do - assert_equal ::SecureRandom, id::DEFAULT_OPTIONS[:secure_random] - - id = @id.new nil - assert_equal ::SecureRandom, id.sid_secure - end - - it "allow to use another securerandom provider" do - secure_random = Class.new do - def hex(*args) - 'fake_hex' - end - end - id = Rack::Session::Abstract::ID.new nil, secure_random: secure_random.new - id.send(:generate_sid).must_equal 'fake_hex' - end - - it "should warn when subclassing" do - verbose = $VERBOSE - begin - $VERBOSE = true - warn_arg = nil - @id.define_singleton_method(:warn) do |arg| - warn_arg = arg - end - c = Class.new(@id) - regexp = /is inheriting from Rack::Session::Abstract::ID. Inheriting from Rack::Session::Abstract::ID is deprecated, please inherit from Rack::Session::Abstract::Persisted instead/ - warn_arg.must_match(regexp) - - warn_arg = nil - c = Class.new(c) - warn_arg.must_be_nil - ensure - $VERBOSE = verbose - @id.singleton_class.send(:remove_method, :warn) - end - end - - it "#find_session should find session in request" do - id = @id.new(nil) - def id.get_session(env, sid) - [env['rack.session'], generate_sid] - end - req = Rack::Request.new('rack.session' => {}) - session, sid = id.find_session(req, nil) - session.must_equal({}) - sid.must_match(/\A\h+\z/) - end - - it "#write_session should write session to request" do - id = @id.new(nil) - def id.set_session(env, sid, session, options) - [env, sid, session, options] - end - req = Rack::Request.new({}) - id.write_session(req, 1, 2, 3).must_equal [{}, 1, 2, 3] - end - - it "#delete_session should remove session from request" do - id = @id.new(nil) - def id.destroy_session(env, sid, options) - [env, sid, options] - end - req = Rack::Request.new({}) - id.delete_session(req, 1, 2).must_equal [{}, 1, 2] - end -end diff --git a/test/spec_session_abstract_persisted.rb b/test/spec_session_abstract_persisted.rb deleted file mode 100644 index 84ddf0728f115268665ba218f556622294b285c2..0000000000000000000000000000000000000000 --- a/test/spec_session_abstract_persisted.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'rack/session/abstract/id' - -describe Rack::Session::Abstract::Persisted do - def setup - @class = Rack::Session::Abstract::Persisted - @pers = @class.new(nil) - end - - it "#generated_sid generates a session identifier" do - @pers.send(:generate_sid).must_match(/\A\h+\z/) - @pers.send(:generate_sid, nil).must_match(/\A\h+\z/) - - obj = Object.new - def obj.hex(_); raise NotImplementedError end - @pers.send(:generate_sid, obj).must_match(/\A\h+\z/) - end - - it "#commit_session? returns false if :skip option is given" do - @pers.send(:commit_session?, Rack::Request.new({}), {}, skip: true).must_equal false - end - - it "#commit_session writes to rack.errors if session cannot be written" do - @pers = @class.new(nil) - def @pers.write_session(*) end - errors = StringIO.new - env = { 'rack.errors' => errors } - req = Rack::Request.new(env) - store = Class.new do - def load_session(req) - ["id", {}] - end - def session_exists?(req) - true - end - end - session = env['rack.session'] = Rack::Session::Abstract::SessionHash.new(store.new, req) - session['foo'] = 'bar' - @pers.send(:commit_session, req, Rack::Response.new) - errors.rewind - errors.read.must_equal "Warning! Rack::Session::Abstract::Persisted failed to save session. Content dropped.\n" - end - - it "#cookie_value returns its argument" do - obj = Object.new - @pers.send(:cookie_value, obj).must_equal(obj) - end - - it "#session_class returns the default session class" do - @pers.send(:session_class).must_equal Rack::Session::Abstract::SessionHash - end - - it "#find_session raises" do - proc { @pers.send(:find_session, nil, nil) }.must_raise RuntimeError - end - - it "#write_session raises" do - proc { @pers.send(:write_session, nil, nil, nil, nil) }.must_raise RuntimeError - end - - it "#delete_session raises" do - proc { @pers.send(:delete_session, nil, nil, nil) }.must_raise RuntimeError - end -end diff --git a/test/spec_session_abstract_persisted_secure_secure_session_hash.rb b/test/spec_session_abstract_persisted_secure_secure_session_hash.rb deleted file mode 100644 index 1a007eb4ae1831dd2a1bc47c6d287c58901f5333..0000000000000000000000000000000000000000 --- a/test/spec_session_abstract_persisted_secure_secure_session_hash.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -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(:unknown) { :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_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb deleted file mode 100644 index ac0b7bb3afed06b477ca35c78817610ad687a517..0000000000000000000000000000000000000000 --- a/test/spec_session_abstract_session_hash.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'rack/session/abstract/id' - -describe Rack::Session::Abstract::SessionHash do - attr_reader :hash - - def setup - super - store = Class.new do - def load_session(req) - ["id", { foo: :bar, baz: :qux, x: { y: 1 } }] - end - def session_exists?(req) - true - end - end - @class = Rack::Session::Abstract::SessionHash - @hash = @class.new(store.new, nil) - end - - it ".find finds entry in request" do - assert_equal({}, @class.find(Rack::Request.new('rack.session' => {}))) - end - - it ".set sets session in request" do - req = Rack::Request.new({}) - @class.set(req, {}) - req.env['rack.session'].must_equal({}) - end - - it ".set_options sets session options in request" do - req = Rack::Request.new({}) - h = {} - @class.set_options(req, h) - opts = req.env['rack.session.options'] - opts.must_equal(h) - opts.wont_be_same_as(h) - end - - it "#keys returns keys" do - assert_equal ["foo", "baz", "x"], hash.keys - end - - it "#values returns values" do - assert_equal [:bar, :qux, { y: 1 }], hash.values - end - - it "#dig operates like Hash#dig" do - assert_equal({ y: 1 }, hash.dig("x")) - assert_equal(1, hash.dig(:x, :y)) - assert_nil(hash.dig(:z)) - assert_nil(hash.dig(:x, :z)) - lambda { hash.dig(:x, :y, :z) }.must_raise TypeError - lambda { hash.dig }.must_raise ArgumentError - end - - it "#each iterates over entries" do - a = [] - @hash.each do |k, v| - a << [k, v] - end - a.must_equal [["foo", :bar], ["baz", :qux], ["x", { y: 1 }]] - end - - it "#has_key returns whether the key is in the hash" do - assert_equal true, hash.has_key?("foo") - assert_equal true, hash.has_key?(:foo) - assert_equal false, hash.has_key?("food") - assert_equal false, hash.has_key?(:food) - end - - it "#replace replaces hash" do - hash.replace({ bar: "foo" }) - assert_equal "foo", hash["bar"] - 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(:unknown) { :default } - end - - it "it raises when fetching unknown keys without defaults" do - lambda { hash.fetch(:unknown) }.must_raise KeyError - end - end - - it "#stringify_keys returns hash or session hash with keys stringified" do - assert_equal({ "foo" => :bar, "baz" => :qux, "x" => { y: 1 } }, hash.send(:stringify_keys, hash).to_h) - end -end diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb deleted file mode 100644 index ce85ba321232a9ce2c7567acf7887d434612c2b9..0000000000000000000000000000000000000000 --- a/test/spec_session_cookie.rb +++ /dev/null @@ -1,498 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' - -describe Rack::Session::Cookie do - incrementor = lambda do |env| - env["rack.session"]["counter"] ||= 0 - env["rack.session"]["counter"] += 1 - hash = env["rack.session"].dup - hash.delete("session_id") - Rack::Response.new(hash.inspect).to_a - end - - session_id = lambda do |env| - Rack::Response.new(env["rack.session"].to_hash.inspect).to_a - end - - session_option = lambda do |opt| - lambda do |env| - Rack::Response.new(env["rack.session.options"][opt].inspect).to_a - end - end - - nothing = lambda do |env| - Rack::Response.new("Nothing").to_a - end - - renewer = lambda do |env| - env["rack.session.options"][:renew] = true - Rack::Response.new("Nothing").to_a - end - - only_session_id = lambda do |env| - Rack::Response.new(env["rack.session"]["session_id"].to_s).to_a - end - - bigcookie = lambda do |env| - env["rack.session"]["cookie"] = "big" * 3000 - Rack::Response.new(env["rack.session"].inspect).to_a - end - - destroy_session = lambda do |env| - env["rack.session"].destroy - Rack::Response.new("Nothing").to_a - end - - def response_for(options = {}) - request_options = options.fetch(:request, {}) - cookie = if options[:cookie].is_a?(Rack::Response) - options[:cookie]["Set-Cookie"] - else - options[:cookie] - end - request_options["HTTP_COOKIE"] = cookie || "" - - app_with_cookie = Rack::Session::Cookie.new(*options[:app]) - app_with_cookie = Rack::Lint.new(app_with_cookie) - Rack::MockRequest.new(app_with_cookie).get("/", request_options) - end - - before do - @warnings = warnings = [] - Rack::Session::Cookie.class_eval do - define_method(:warn) { |m| warnings << m } - end - end - - after do - Rack::Session::Cookie.class_eval { remove_method :warn } - end - - describe 'Base64' do - it 'uses base64 to encode' do - coder = Rack::Session::Cookie::Base64.new - str = 'fuuuuu' - coder.encode(str).must_equal [str].pack('m0') - end - - it 'uses base64 to decode' do - coder = Rack::Session::Cookie::Base64.new - 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('m0') - end - - it 'marshals and base64 decodes' do - coder = Rack::Session::Cookie::Base64::Marshal.new - str = [::Marshal.dump('fuuuuu')].pack('m0') - coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first) - end - - it 'rescues failures on decode' do - coder = Rack::Session::Cookie::Base64::Marshal.new - coder.decode('lulz').must_be_nil - end - end - - describe 'JSON' 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('m0') - end - - it 'JSON and base64 decodes' do - coder = Rack::Session::Cookie::Base64::JSON.new - 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 - coder = Rack::Session::Cookie::Base64::JSON.new - coder.decode('lulz').must_be_nil - end - end - - describe 'ZipJSON' do - it 'jsons, deflates, and base64 encodes' 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('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('m0') - coder.decode(b64).must_equal obj - end - - it 'rescues failures on decode' do - coder = Rack::Session::Cookie::Base64::ZipJSON.new - coder.decode('lulz').must_be_nil - end - end - end - - it "warns if no secret is given" do - Rack::Session::Cookie.new(incrementor) - @warnings.first.must_match(/no secret/i) - @warnings.clear - 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) - @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) - @warnings.first.must_match(/no secret/i) - end - - it 'uses a coder' do - identity = Class.new { - attr_reader :calls - - def initialize - @calls = [] - end - - def encode(str); @calls << :encode; str; end - def decode(str); @calls << :decode; str; end - }.new - response = response_for(app: [incrementor, { coder: identity }]) - - response["Set-Cookie"].must_include "rack.session=" - response.body.must_equal '{"counter"=>1}' - identity.calls.must_equal [:decode, :encode] - end - - it "creates a new cookie" do - response = response_for(app: incrementor) - response["Set-Cookie"].must_include "rack.session=" - response.body.must_equal '{"counter"=>1}' - end - - it "passes through same_site option to session cookie" do - response = response_for(app: [incrementor, same_site: :none]) - response["Set-Cookie"].must_include "SameSite=None" - end - - it "allows using a lambda to specify same_site option, because some browsers require different settings" do - # Details of why this might need to be set dynamically: - # https://www.chromium.org/updates/same-site/incompatible-clients - # https://gist.github.com/bnorton/7dee72023787f367c48b3f5c2d71540f - - response = response_for(app: [incrementor, same_site: lambda { |req, res| :none }]) - response["Set-Cookie"].must_include "SameSite=None" - - response = response_for(app: [incrementor, same_site: lambda { |req, res| :lax }]) - response["Set-Cookie"].must_include "SameSite=Lax" - end - - it "loads from a cookie" do - response = response_for(app: incrementor) - - response = response_for(app: incrementor, cookie: response) - response.body.must_equal '{"counter"=>2}' - - response = response_for(app: incrementor, cookie: response) - response.body.must_equal '{"counter"=>3}' - end - - it "renew session id" do - response = response_for(app: incrementor) - cookie = response['Set-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) - cookie = response['Set-Cookie'] if response['Set-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.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.body.wont_equal "" - response.body.wont_equal old_session_id - end - - it "survives broken cookies" do - response = response_for( - app: incrementor, - cookie: "rack.session=blarghfasel" - ) - response.body.must_equal '{"counter"=>1}' - - response = response_for( - 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 }) - }.must_raise Rack::MockRequest::FatalWarning - end - - it "loads from a cookie with integrity hash" do - app = [incrementor, { secret: "test" }] - - 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.body.must_equal '{"counter"=>3}' - - app = [incrementor, { secret: "other" }] - - 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" }]) - - 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) - response.body.must_equal '{"counter"=>3}' - end - - it "ignores tampered with session cookies" do - app = [incrementor, { secret: "test" }] - response = response_for(app: app) - response.body.must_equal '{"counter"=>1}' - - 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.body.must_equal '{"counter"=>1}' - end - - it "supports either of secret or old_secret" do - app = [incrementor, { secret: "test" }] - response = response_for(app: app) - response.body.must_equal '{"counter"=>1}' - - response = response_for(app: app, cookie: response) - response.body.must_equal '{"counter"=>2}' - - app = [incrementor, { old_secret: "test" }] - response = response_for(app: app) - response.body.must_equal '{"counter"=>1}' - - 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 }] - - 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.body.must_equal '{"counter"=>3}' - - app = [incrementor, { secret: "other" }] - - 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) - - lint = Rack::Lint.new(session_id) - response = response_for(app: lint, cookie: response) - response.body.wont_be :nil? - end - - it "can handle middleware that inspects the env" do - class TestEnvInspector - def initialize(app) - @app = app - end - def call(env) - env.inspect - @app.call(env) - end - end - - response = response_for(app: incrementor) - - inspector = TestEnvInspector.new(session_id) - 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.body.must_equal '{"counter"=>1}' - - 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 }] - - response = response_for(app: app) - response["Set-Cookie"].must_be_nil - - 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["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["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) - 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) - 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.body.must_equal '"foo"' - end - - it "exposes :coder in env['rack.session.option']" do - 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) - 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) - response.body.must_match(/counter/) - response.body.must_match(/foo/) - end - - it "allows more than one '--' in the cookie when calculating digests" do - @counter = 0 - app = lambda do |env| - env["rack.session"]["message"] ||= "" - env["rack.session"]["message"] += "#{(@counter += 1).to_s}--" - hash = env["rack.session"].dup - hash.delete("session_id") - Rack::Response.new(hash["message"]).to_a - end - # another example of an unsafe coder is Base64.urlsafe_encode64 - unsafe_coder = Class.new { - 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) - response.body.must_equal "1--" - 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_pool.rb b/test/spec_session_pool.rb deleted file mode 100644 index aba93fb169bdec8cf51ff03897e0d74ff821c0e9..0000000000000000000000000000000000000000 --- a/test/spec_session_pool.rb +++ /dev/null @@ -1,264 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' - -describe Rack::Session::Pool do - session_key = Rack::Session::Pool::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 - - get_session_id = Rack::Lint.new(lambda do |env| - Rack::Response.new(env["rack.session"].inspect).to_a - end) - - nothing = Rack::Lint.new(lambda do |env| - Rack::Response.new("Nothing").to_a - end) - - drop_session = Rack::Lint.new(lambda do |env| - env['rack.session.options'][:drop] = true - incrementor.call(env) - end) - - renew_session = Rack::Lint.new(lambda do |env| - env['rack.session.options'][:renew] = true - incrementor.call(env) - end) - - defer_session = Rack::Lint.new(lambda do |env| - env['rack.session.options'][:defer] = true - incrementor.call(env) - end) - - incrementor = Rack::Lint.new(incrementor) - - it "creates a new cookie" do - pool = Rack::Session::Pool.new(incrementor) - res = Rack::MockRequest.new(pool).get("/") - res["Set-Cookie"].must_match(session_match) - res.body.must_equal '{"counter"=>1}' - end - - it "determines session from a cookie" do - pool = Rack::Session::Pool.new(incrementor) - req = Rack::MockRequest.new(pool) - cookie = req.get("/")["Set-Cookie"] - req.get("/", "HTTP_COOKIE" => cookie). - body.must_equal '{"counter"=>2}' - req.get("/", "HTTP_COOKIE" => cookie). - body.must_equal '{"counter"=>3}' - end - - it "survives nonexistent cookies" do - pool = Rack::Session::Pool.new(incrementor) - res = Rack::MockRequest.new(pool). - get("/", "HTTP_COOKIE" => "#{session_key}=blarghfasel") - res.body.must_equal '{"counter"=>1}' - end - - it "does not send the same session id if it did not change" do - pool = Rack::Session::Pool.new(incrementor) - req = Rack::MockRequest.new(pool) - - res0 = req.get("/") - cookie = res0["Set-Cookie"][session_match] - res0.body.must_equal '{"counter"=>1}' - pool.pool.size.must_equal 1 - - res1 = req.get("/", "HTTP_COOKIE" => cookie) - res1["Set-Cookie"].must_be_nil - res1.body.must_equal '{"counter"=>2}' - pool.pool.size.must_equal 1 - - res2 = req.get("/", "HTTP_COOKIE" => cookie) - res2["Set-Cookie"].must_be_nil - res2.body.must_equal '{"counter"=>3}' - pool.pool.size.must_equal 1 - end - - it "deletes cookies with :drop option" 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) - - res1 = req.get("/") - session = (cookie = res1["Set-Cookie"])[session_match] - res1.body.must_equal '{"counter"=>1}' - pool.pool.size.must_equal 1 - - res2 = dreq.get("/", "HTTP_COOKIE" => cookie) - res2["Set-Cookie"].must_be_nil - res2.body.must_equal '{"counter"=>2}' - pool.pool.size.must_equal 0 - - res3 = req.get("/", "HTTP_COOKIE" => cookie) - res3["Set-Cookie"][session_match].wont_equal session - res3.body.must_equal '{"counter"=>1}' - pool.pool.size.must_equal 1 - end - - it "provides new session id with :renew option" do - pool = Rack::Session::Pool.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}' - pool.pool.size.must_equal 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}' - pool.pool.size.must_equal 1 - - res3 = req.get("/", "HTTP_COOKIE" => new_cookie) - res3.body.must_equal '{"counter"=>3}' - pool.pool.size.must_equal 1 - - res4 = req.get("/", "HTTP_COOKIE" => cookie) - res4.body.must_equal '{"counter"=>1}' - pool.pool.size.must_equal 2 - end - - it "omits cookie with :defer option" do - pool = Rack::Session::Pool.new(incrementor) - defer = Rack::Utils::Context.new(pool, defer_session) - dreq = Rack::MockRequest.new(defer) - - res1 = dreq.get("/") - res1["Set-Cookie"].must_be_nil - res1.body.must_equal '{"counter"=>1}' - 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 - - it "passes through same_site option to session pool" do - pool = Rack::Session::Pool.new(incrementor, same_site: :none) - req = Rack::MockRequest.new(pool) - res = req.get("/") - res["Set-Cookie"].must_include "SameSite=None" - end - - it "allows using a lambda to specify same_site option, because some browsers require different settings" do - pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :none }) - req = Rack::MockRequest.new(pool) - res = req.get("/") - res["Set-Cookie"].must_include "SameSite=None" - - pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :lax }) - req = Rack::MockRequest.new(pool) - res = req.get("/") - res["Set-Cookie"].must_include "SameSite=Lax" - end - - # anyone know how to do this better? - it "should merge sessions when multithreaded" do - unless $DEBUG - 1.must_equal 1 - next - end - - warn 'Running multithread tests for Session::Pool' - pool = Rack::Session::Pool.new(incrementor) - req = Rack::MockRequest.new(pool) - - res = req.get('/') - res.body.must_equal '{"counter"=>1}' - cookie = res["Set-Cookie"] - 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 - 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 |resp| - resp['Set-Cookie'].must_equal cookie - resp.body.must_include '"counter"=>2' - end - - session = pool.pool[sess_id] - session.size.must_equal tnum + 1 # counter - session['counter'].must_equal 2 # meeeh - end - - it "does not return a cookie if cookie was not read/written" do - app = Rack::Session::Pool.new(nothing) - res = Rack::MockRequest.new(app).get("/") - res["Set-Cookie"].must_be_nil - end - - it "does not return a cookie if cookie was not written (only read)" do - 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' }) - 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) - res = Rack::MockRequest.new(app).get("/") - res["Set-Cookie"].must_be_nil - end -end diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb index 441599b4f86657323f15ab799c30d6315fe6d411..debc5c30bc95274be38a66e5300cecda6606d316 100644 --- a/test/spec_show_exceptions.rb +++ b/test/spec_show_exceptions.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/show_exceptions' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::ShowExceptions do def show_exceptions(app) Rack::Lint.new Rack::ShowExceptions.new(app) @@ -31,7 +37,7 @@ describe Rack::ShowExceptions do req = Rack::MockRequest.new( show_exceptions( - lambda{|env| raise RuntimeError, "foo", ["nonexistant.rb:2:in `a': adf (RuntimeError)", "bad-backtrace"] } + lambda{|env| raise RuntimeError, "foo", ["nonexistent.rb:2:in `a': adf (RuntimeError)", "bad-backtrace"] } )) res = req.get("/", "HTTP_ACCEPT" => "text/html") @@ -43,7 +49,7 @@ describe Rack::ShowExceptions do assert_includes(res.body, 'ShowExceptions') assert_includes(res.body, 'No GET data') assert_includes(res.body, 'No POST data') - assert_includes(res.body, 'nonexistant.rb') + assert_includes(res.body, 'nonexistent.rb') refute_includes(res.body, 'bad-backtrace') end @@ -171,4 +177,29 @@ describe Rack::ShowExceptions do assert_equal(expected, exc.prefers_plaintext?(env)) end end + + it "prefers Exception#detailed_message instead of Exception#message if available" do + res = nil + + custom_exc_class = Class.new(RuntimeError) do + def detailed_message(highlight: false) + "detailed_message_test" + end + end + + req = Rack::MockRequest.new( + show_exceptions( + lambda{|env| raise custom_exc_class } + )) + + res = req.get("/", "HTTP_ACCEPT" => "text/html") + + res.must_be :server_error? + res.status.must_equal 500 + + assert_match(res, /detailed_message_test/) + assert_match(res, /ShowExceptions/) + assert_match(res, /No GET data/) + assert_match(res, /No POST data/) + end end diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb index 486076b8d5b0c004e068334bd01950bc05002b1b..14c3da45a58d0212de9bd6e908a184d5e56fdfcf 100644 --- a/test/spec_show_status.rb +++ b/test/spec_show_status.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/show_status' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::ShowStatus do def show_status(app) Rack::Lint.new Rack::ShowStatus.new(app) @@ -10,14 +16,14 @@ 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.must_be :not_found? res.wont_be_empty - res["Content-Type"].must_equal "text/html" + res["content-type"].must_equal "text/html" assert_match(res, /404/) assert_match(res, /Not Found/) end @@ -27,14 +33,14 @@ 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.must_be :not_found? res.wont_be_empty - res["Content-Type"].must_equal "text/html" + res["content-type"].must_equal "text/html" assert_match(res, /404/) assert_match(res, /Not Found/) assert_match(res, /too meta/) @@ -45,14 +51,14 @@ 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.must_be :not_found? res.wont_be_empty - res["Content-Type"].must_equal "text/html" + res["content-type"].must_equal "text/html" assert_includes(res.body, '404') assert_includes(res.body, 'Not Found') assert_includes(res.body, '["gone too meta."]') @@ -64,13 +70,13 @@ 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.wont_be_empty - res["Content-Type"].must_equal "text/html" + res["content-type"].must_equal "text/html" assert_match(res, /500/) res.wont_include detail res.body.must_include Rack::Utils.escape_html(detail) @@ -80,7 +86,7 @@ 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) @@ -90,13 +96,13 @@ describe Rack::ShowStatus do 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["WWW-Authenticate"].must_equal "Basic blah" + res["www-authenticate"].must_equal "Basic blah" end it "replace existing messages if there is detail" do @@ -104,17 +110,33 @@ 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.must_be :not_found? res.wont_be_empty - res["Content-Type"].must_equal "text/html" - res["Content-Length"].wont_equal "4" + res["content-type"].must_equal "text/html" + res["content-length"].wont_equal "4" assert_match(res, /404/) assert_match(res, /too meta/) res.body.wont_match(/foo/) end + + it "close the original body" do + closed = false + + body = Object.new + def body.each; yield 's' end + body.define_singleton_method(:close) { closed = true } + + req = Rack::MockRequest.new( + show_status(lambda{|env| + [404, { "content-type" => "text/plain", "content-length" => "0" }, body] + })) + + req.get("/", lint: true) + closed.must_equal true + end end diff --git a/test/spec_static.rb b/test/spec_static.rb index 2a94d68cafbd75522123e42b5218a38393a5d5c9..7496c032cb7b93ff59e5812ca1ac771b1ced4f76 100644 --- a/test/spec_static.rb +++ b/test/spec_static.rb @@ -3,9 +3,15 @@ require_relative 'helper' require 'zlib' +separate_testing do + require_relative '../lib/rack/static' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + class DummyApp def call(env) - [200, { "Content-Type" => "text/plain" }, ["Hello World"]] + [200, { "content-type" => "text/plain" }, ["Hello World"]] end end @@ -115,24 +121,24 @@ describe Rack::Static do 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.must_be :ok? - res.headers['Content-Encoding'].must_equal 'gzip' - res.headers['Content-Type'].must_equal 'text/plain' + res.headers['content-encoding'].must_equal 'gzip' + res.headers['content-type'].must_equal 'text/plain' Zlib::GzipReader.wrap(StringIO.new(res.body), &:read).must_match(/ruby/) 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.must_be :ok? - res.headers['Content-Encoding'].must_be_nil - res.headers['Content-Type'].must_equal 'text/x-script.ruby' + res.headers['content-encoding'].must_be_nil + res.headers['content-type'].must_equal 'text/x-script.ruby' res.body.must_match(/ruby/) end it "serves regular files if client does not accept gzip encoding" do res = @gzip_request.get("/cgi/test") res.must_be :ok? - res.headers['Content-Encoding'].must_be_nil - res.headers['Content-Type'].must_equal 'text/plain' + res.headers['content-encoding'].must_be_nil + res.headers['content-type'].must_equal 'text/plain' res.body.must_match(/ruby/) end @@ -141,8 +147,16 @@ describe Rack::Static do res = @gzip_request.get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate) res.status.must_equal 304 res.body.must_be :empty? - res.headers['Content-Encoding'].must_be_nil - res.headers['Content-Type'].must_be_nil + res.headers['content-encoding'].must_be_nil + res.headers['content-type'].must_be_nil + end + + it "return 304 if gzipped file isn't modified since last serve" do + path = File.join(DOCROOT, "/cgi/test") + res = @gzip_request.get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path+'.gz').httpdate, 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip') + + res.status.must_equal 304 + res.body.must_be :empty? end it "supports serving fixed cache-control (legacy option)" do @@ -150,71 +164,71 @@ describe Rack::Static do request = Rack::MockRequest.new(static(DummyApp.new, opts)) res = request.get("/cgi/test") res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public' + 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' }], - [false, { 'Cache-Control' => 'public, max-age=600' }] + [: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' }], + [false, { 'cache-control' => 'public, max-age=600' }] ] } it "supports header rule :all" do # Headers for all files via :all shortcut res = @header_request.get('/cgi/assets/index.html') res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=100' + res.headers['cache-control'].must_equal 'public, max-age=100' end it "supports header rule :fonts" do # Headers for web fonts via :fonts shortcut res = @header_request.get('/cgi/assets/fonts/font.eot') res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=200' + res.headers['cache-control'].must_equal 'public, max-age=200' end it "supports file extension header rules provided as an Array" do # Headers for file extensions via array res = @header_request.get('/cgi/assets/images/image.png') res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=300' + res.headers['cache-control'].must_equal 'public, max-age=300' end it "supports folder rules provided as a String" do # Headers for files in folder via string res = @header_request.get('/cgi/assets/folder/test.js') res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=400' + res.headers['cache-control'].must_equal 'public, max-age=400' end it "supports folder header rules provided as a String not starting with a slash" do res = @header_request.get('/cgi/assets/javascripts/app.js') res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=500' + res.headers['cache-control'].must_equal 'public, max-age=500' end it "supports flexible header rules provided as Regexp" do # Flexible Headers via Regexp res = @header_request.get('/cgi/assets/stylesheets/app.css') res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=600' + res.headers['cache-control'].must_equal 'public, max-age=600' end 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' }] + [:all, { 'cache-control' => 'public, max-age=42' }] ]) request = Rack::MockRequest.new(static(DummyApp.new, opts)) res = request.get("/cgi/test") res.must_be :ok? - res.headers['Cache-Control'].must_equal 'public, max-age=42' + res.headers['cache-control'].must_equal 'public, max-age=42' end it "expands the root path upon the middleware initialization" do diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb index 063687a094aae61c490b53a50e354ec9de8d4006..7153297f29ed133cf1f6fcb772c2da47fdafc77b 100644 --- a/test/spec_tempfile_reaper.rb +++ b/test/spec_tempfile_reaper.rb @@ -2,6 +2,12 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/tempfile_reaper' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::TempfileReaper do class MockTempfile attr_reader :closed @@ -30,6 +36,24 @@ describe Rack::TempfileReaper do response[0].must_equal 200 end + it 'close env[rack.tempfiles] when app raises an error' do + tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new + @env['rack.tempfiles'] = [ tempfile1, tempfile2 ] + app = lambda { |_| raise 'foo' } + proc{call(app)}.must_raise RuntimeError + tempfile1.closed.must_equal true + tempfile2.closed.must_equal true + end + + it 'close env[rack.tempfiles] when app raises an non-StandardError' do + tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new + @env['rack.tempfiles'] = [ tempfile1, tempfile2 ] + app = lambda { |_| raise LoadError, 'foo' } + proc{call(app)}.must_raise LoadError + tempfile1.closed.must_equal true + tempfile2.closed.must_equal true + end + it 'close env[rack.tempfiles] when body is closed' do tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new @env['rack.tempfiles'] = [ tempfile1, tempfile2 ] @@ -60,4 +84,20 @@ describe Rack::TempfileReaper do tempfile1.closed.must_equal true tempfile2.closed.must_equal true end + + it 'handle missing rack.tempfiles on normal response' do + app = lambda do |env| + env.delete('rack.tempfiles') + [200, {}, ['Hello, World!']] + end + call(app)[2].close + end + + it 'handle missing rack.tempfiles on error' do + app = lambda do |env| + env.delete('rack.tempfiles') + raise 'Foo' + end + proc{call(app)}.must_raise RuntimeError + end end diff --git a/test/spec_thin.rb b/test/spec_thin.rb deleted file mode 100644 index f7a1211027d0ce037af68ea84b1cce278fc3fa78..0000000000000000000000000000000000000000 --- a/test/spec_thin.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -begin -require 'rack/handler/thin' -require_relative 'testrequest' -require 'timeout' - -describe Rack::Handler::Thin do - include TestRequest::Helpers - - before do - @app = Rack::Lint.new(TestRequest.new) - @server = nil - 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| - @server = server - end - end - - Thread.pass until @server && @server.running? - end - - after do - @server.stop! - @thread.join - end - - - it "respond" do - GET("/") - response.wont_be :nil? - end - - it "be a Thin" do - GET("/") - status.must_equal 200 - response["SERVER_SOFTWARE"].must_match(/thin/) - response["HTTP_VERSION"].must_equal "HTTP/1.1" - response["SERVER_PROTOCOL"].must_equal "HTTP/1.1" - response["SERVER_PORT"].must_equal "9204" - response["SERVER_NAME"].must_equal "127.0.0.1" - end - - it "have rack headers" do - GET("/") - 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 - end - - it "have CGI headers on GET" do - GET("/") - response["REQUEST_METHOD"].must_equal "GET" - response["REQUEST_PATH"].must_equal "/" - response["PATH_INFO"].must_equal "/" - response["QUERY_STRING"].must_equal "" - response["test.postdata"].must_equal "" - - GET("/test/foo?quux=1") - response["REQUEST_METHOD"].must_equal "GET" - response["REQUEST_PATH"].must_equal "/test/foo" - response["PATH_INFO"].must_equal "/test/foo" - response["QUERY_STRING"].must_equal "quux=1" - end - - it "have CGI headers on POST" do - POST("/", { "rack-form-data" => "23" }, { 'X-test-header' => '42' }) - status.must_equal 200 - response["REQUEST_METHOD"].must_equal "POST" - 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 - - it "set tag for server" do - @server.tag.must_equal 'tag' - end -end - -rescue LoadError - $stderr.puts "Skipping Rack::Handler::Thin tests (Thin is required). `gem install thin` and try again." -end diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb index 29af5587052f26291da3cefb18ff178f6a7ef33d..c3ecffe29e3a7c21d4716658e352cad22d94464d 100644 --- a/test/spec_urlmap.rb +++ b/test/spec_urlmap.rb @@ -2,13 +2,19 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/urlmap' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' +end + describe Rack::URLMap do it "dispatches paths correctly" do app = lambda { |env| [200, { - 'X-ScriptName' => env['SCRIPT_NAME'], - 'X-PathInfo' => env['PATH_INFO'], - 'Content-Type' => 'text/plain' + 'x-scriptname' => env['SCRIPT_NAME'], + 'x-pathinfo' => env['PATH_INFO'], + 'content-type' => 'text/plain' }, [""]] } map = Rack::Lint.new(Rack::URLMap.new({ @@ -25,111 +31,116 @@ describe Rack::URLMap do res = Rack::MockRequest.new(map).get("/foo") res.must_be :ok? - res["X-ScriptName"].must_equal "/foo" - res["X-PathInfo"].must_equal "" + res["x-scriptname"].must_equal "/foo" + res["x-pathinfo"].must_equal "" res = Rack::MockRequest.new(map).get("/foo/") res.must_be :ok? - res["X-ScriptName"].must_equal "/foo" - res["X-PathInfo"].must_equal "/" + res["x-scriptname"].must_equal "/foo" + res["x-pathinfo"].must_equal "/" res = Rack::MockRequest.new(map).get("/foo/bar") res.must_be :ok? - res["X-ScriptName"].must_equal "/foo/bar" - res["X-PathInfo"].must_equal "" + res["x-scriptname"].must_equal "/foo/bar" + res["x-pathinfo"].must_equal "" + + res = Rack::MockRequest.new(map).get("/foo/bard") + res.must_be :ok? + res["x-scriptname"].must_equal "/foo" + res["x-pathinfo"].must_equal "/bard" res = Rack::MockRequest.new(map).get("/foo/bar/") res.must_be :ok? - res["X-ScriptName"].must_equal "/foo/bar" - res["X-PathInfo"].must_equal "/" + res["x-scriptname"].must_equal "/foo/bar" + res["x-pathinfo"].must_equal "/" res = Rack::MockRequest.new(map).get("/foo///bar//quux") res.status.must_equal 200 res.must_be :ok? - res["X-ScriptName"].must_equal "/foo/bar" - res["X-PathInfo"].must_equal "//quux" + res["x-scriptname"].must_equal "/foo/bar" + res["x-pathinfo"].must_equal "//quux" res = Rack::MockRequest.new(map).get("/foo/quux", "SCRIPT_NAME" => "/bleh") res.must_be :ok? - res["X-ScriptName"].must_equal "/bleh/foo" - res["X-PathInfo"].must_equal "/quux" + res["x-scriptname"].must_equal "/bleh/foo" + res["x-pathinfo"].must_equal "/quux" res = Rack::MockRequest.new(map).get("/bar", 'HTTP_HOST' => 'foo.org') res.must_be :ok? - res["X-ScriptName"].must_equal "/bar" - res["X-PathInfo"].must_be :empty? + res["x-scriptname"].must_equal "/bar" + res["x-pathinfo"].must_be :empty? res = Rack::MockRequest.new(map).get("/bar/", 'HTTP_HOST' => 'foo.org') res.must_be :ok? - res["X-ScriptName"].must_equal "/bar" - res["X-PathInfo"].must_equal '/' + res["x-scriptname"].must_equal "/bar" + res["x-pathinfo"].must_equal '/' end it "dispatches hosts correctly" do map = Rack::Lint.new(Rack::URLMap.new("http://foo.org/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "foo.org", - "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"], + { "content-type" => "text/plain", + "x-position" => "foo.org", + "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"], }, [""]]}, "http://subdomain.foo.org/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "subdomain.foo.org", - "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"], + { "content-type" => "text/plain", + "x-position" => "subdomain.foo.org", + "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"], }, [""]]}, "http://bar.org/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "bar.org", - "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"], + { "content-type" => "text/plain", + "x-position" => "bar.org", + "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"], }, [""]]}, "/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "default.org", - "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"], + { "content-type" => "text/plain", + "x-position" => "default.org", + "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"], }, [""]]} )) res = Rack::MockRequest.new(map).get("/") res.must_be :ok? - res["X-Position"].must_equal "default.org" + res["x-position"].must_equal "default.org" res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "bar.org") res.must_be :ok? - res["X-Position"].must_equal "bar.org" + res["x-position"].must_equal "bar.org" res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "foo.org") res.must_be :ok? - res["X-Position"].must_equal "foo.org" + res["x-position"].must_equal "foo.org" res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "subdomain.foo.org", "SERVER_NAME" => "foo.org") res.must_be :ok? - res["X-Position"].must_equal "subdomain.foo.org" + res["x-position"].must_equal "subdomain.foo.org" res = Rack::MockRequest.new(map).get("http://foo.org/") res.must_be :ok? - res["X-Position"].must_equal "foo.org" + res["x-position"].must_equal "foo.org" res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "example.org") res.must_be :ok? - res["X-Position"].must_equal "default.org" + 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["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["x-position"].must_equal "default.org" res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "example.org:9292", "SERVER_PORT" => "9292") res.must_be :ok? - res["X-Position"].must_equal "default.org" + res["x-position"].must_equal "default.org" end it "be nestable" do @@ -137,10 +148,10 @@ describe Rack::URLMap do Rack::URLMap.new("/bar" => Rack::URLMap.new("/quux" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "/foo/bar/quux", - "X-PathInfo" => env["PATH_INFO"], - "X-ScriptName" => env["SCRIPT_NAME"], + { "content-type" => "text/plain", + "x-position" => "/foo/bar/quux", + "x-pathinfo" => env["PATH_INFO"], + "x-scriptname" => env["SCRIPT_NAME"], }, [""]]} )))) @@ -149,98 +160,98 @@ describe Rack::URLMap do res = Rack::MockRequest.new(map).get("/foo/bar/quux") res.must_be :ok? - res["X-Position"].must_equal "/foo/bar/quux" - res["X-PathInfo"].must_equal "" - res["X-ScriptName"].must_equal "/foo/bar/quux" + res["x-position"].must_equal "/foo/bar/quux" + res["x-pathinfo"].must_equal "" + res["x-scriptname"].must_equal "/foo/bar/quux" end it "route root apps correctly" do map = Rack::Lint.new(Rack::URLMap.new("/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "root", - "X-PathInfo" => env["PATH_INFO"], - "X-ScriptName" => env["SCRIPT_NAME"] + { "content-type" => "text/plain", + "x-position" => "root", + "x-pathinfo" => env["PATH_INFO"], + "x-scriptname" => env["SCRIPT_NAME"] }, [""]]}, "/foo" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "foo", - "X-PathInfo" => env["PATH_INFO"], - "X-ScriptName" => env["SCRIPT_NAME"] + { "content-type" => "text/plain", + "x-position" => "foo", + "x-pathinfo" => env["PATH_INFO"], + "x-scriptname" => env["SCRIPT_NAME"] }, [""]]} )) res = Rack::MockRequest.new(map).get("/foo/bar") res.must_be :ok? - res["X-Position"].must_equal "foo" - res["X-PathInfo"].must_equal "/bar" - res["X-ScriptName"].must_equal "/foo" + res["x-position"].must_equal "foo" + res["x-pathinfo"].must_equal "/bar" + res["x-scriptname"].must_equal "/foo" res = Rack::MockRequest.new(map).get("/foo") res.must_be :ok? - res["X-Position"].must_equal "foo" - res["X-PathInfo"].must_equal "" - res["X-ScriptName"].must_equal "/foo" + res["x-position"].must_equal "foo" + res["x-pathinfo"].must_equal "" + res["x-scriptname"].must_equal "/foo" res = Rack::MockRequest.new(map).get("/bar") res.must_be :ok? - res["X-Position"].must_equal "root" - res["X-PathInfo"].must_equal "/bar" - res["X-ScriptName"].must_equal "" + res["x-position"].must_equal "root" + res["x-pathinfo"].must_equal "/bar" + res["x-scriptname"].must_equal "" res = Rack::MockRequest.new(map).get("") res.must_be :ok? - res["X-Position"].must_equal "root" - res["X-PathInfo"].must_equal "/" - res["X-ScriptName"].must_equal "" + res["x-position"].must_equal "root" + res["x-pathinfo"].must_equal "/" + res["x-scriptname"].must_equal "" end it "not squeeze slashes" do map = Rack::Lint.new(Rack::URLMap.new("/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "root", - "X-PathInfo" => env["PATH_INFO"], - "X-ScriptName" => env["SCRIPT_NAME"] + { "content-type" => "text/plain", + "x-position" => "root", + "x-pathinfo" => env["PATH_INFO"], + "x-scriptname" => env["SCRIPT_NAME"] }, [""]]}, "/foo" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "foo", - "X-PathInfo" => env["PATH_INFO"], - "X-ScriptName" => env["SCRIPT_NAME"] + { "content-type" => "text/plain", + "x-position" => "foo", + "x-pathinfo" => env["PATH_INFO"], + "x-scriptname" => env["SCRIPT_NAME"] }, [""]]} )) res = Rack::MockRequest.new(map).get("/http://example.org/bar") res.must_be :ok? - res["X-Position"].must_equal "root" - res["X-PathInfo"].must_equal "/http://example.org/bar" - res["X-ScriptName"].must_equal "" + res["x-position"].must_equal "root" + res["x-pathinfo"].must_equal "/http://example.org/bar" + res["x-scriptname"].must_equal "" end it "not be case sensitive with hosts" do map = Rack::Lint.new(Rack::URLMap.new("http://example.org/" => lambda { |env| [200, - { "Content-Type" => "text/plain", - "X-Position" => "root", - "X-PathInfo" => env["PATH_INFO"], - "X-ScriptName" => env["SCRIPT_NAME"] + { "content-type" => "text/plain", + "x-position" => "root", + "x-pathinfo" => env["PATH_INFO"], + "x-scriptname" => env["SCRIPT_NAME"] }, [""]]} )) res = Rack::MockRequest.new(map).get("http://example.org/") res.must_be :ok? - res["X-Position"].must_equal "root" - res["X-PathInfo"].must_equal "/" - res["X-ScriptName"].must_equal "" + res["x-position"].must_equal "root" + res["x-pathinfo"].must_equal "/" + res["x-scriptname"].must_equal "" res = Rack::MockRequest.new(map).get("http://EXAMPLE.ORG/") res.must_be :ok? - res["X-Position"].must_equal "root" - res["X-PathInfo"].must_equal "/" - res["X-ScriptName"].must_equal "" + res["x-position"].must_equal "root" + res["x-pathinfo"].must_equal "/" + res["x-scriptname"].must_equal "" end it "not allow locations unless they start with /" do diff --git a/test/spec_utils.rb b/test/spec_utils.rb index 90676258fd9783b2203011812c3f834af1003d37..c4f9b27fa1b2fd847c81db7196dfa006e3ed05f7 100644 --- a/test/spec_utils.rb +++ b/test/spec_utils.rb @@ -3,6 +3,14 @@ require_relative 'helper' require 'timeout' +separate_testing do + require_relative '../lib/rack/utils' + require_relative '../lib/rack/lint' + require_relative '../lib/rack/mock_request' + require_relative '../lib/rack/request' + require_relative '../lib/rack/content_length' +end + describe Rack::Utils do def assert_sets(exp, act) @@ -114,7 +122,7 @@ describe Rack::Utils do ex = { "foo" => nil } ex["foo"] = ex - params = Rack::Utils::KeySpaceConstrainedParams.new(65536) + params = Rack::Utils::KeySpaceConstrainedParams.new params['foo'] = params params.to_params_hash.to_s.must_equal ex.to_s end @@ -123,6 +131,22 @@ describe Rack::Utils do Rack::Utils.parse_nested_query(nil).must_equal({}) end + deprecated "should warn for deprecated QueryParser.make_default call with key_space_limit" do + Rack::QueryParser.make_default(1, 1).must_be_kind_of Rack::QueryParser + end + + deprecated "should warn using deprecated QueryParser.new call with key_space_limit" do + Rack::QueryParser.new(Rack::QueryParser::Params, 1, 1).must_be_kind_of Rack::QueryParser + end + + deprecated "should warn using deprecated Rack::Util.key_space_limit=" do + Rack::Utils.key_space_limit = 65536 + end + + deprecated "should warn using deprecated Rack::Util.key_space_limit" do + Rack::Utils.key_space_limit + end + it "raise an exception if the params are too deep" do len = Rack::Utils.param_depth_limit @@ -133,12 +157,6 @@ describe Rack::Utils do Rack::Utils.parse_nested_query("foo#{"[a]" * (len - 1)}=bar") end - # ParamsTooDeepError was introduced in the middle of 2.2 releases - # and this test is here to ensure backwards compatibility - it "ParamsTooDeepError is inherited from originally used RangeError" do - (Rack::QueryParser::ParamsTooDeepError < RangeError).must_equal(true) - end - it "parse nested query strings correctly" do Rack::Utils.parse_nested_query("foo"). must_equal "foo" => nil @@ -191,6 +209,8 @@ describe Rack::Utils do Rack::Utils.parse_nested_query("foo[]=bar&baz[]=1&baz[]=2&baz[]=3"). must_equal "foo" => ["bar"], "baz" => ["1", "2", "3"] + Rack::Utils.parse_nested_query("x[y][z]"). + must_equal "x" => { "y" => { "z" => nil } } Rack::Utils.parse_nested_query("x[y][z]=1"). must_equal "x" => { "y" => { "z" => "1" } } Rack::Utils.parse_nested_query("x[y][z][]=1"). @@ -237,9 +257,8 @@ describe Rack::Utils do must_raise(Rack::Utils::ParameterTypeError). message.must_equal "expected Array (got String) for param `y'" - lambda { Rack::Utils.parse_nested_query("foo%81E=1") }. - must_raise(Rack::Utils::InvalidParameterError). - message.must_equal "invalid byte sequence in UTF-8" + Rack::Utils.parse_nested_query("foo%81E=1"). + must_equal "foo\x81E"=>"1" end it "only moves to a new array when the full key has been seen" do @@ -254,6 +273,20 @@ describe Rack::Utils do ] end + it "handles unexpected use of [ and ] in parameter keys as normal characters" do + Rack::Utils.parse_nested_query("[]=1&[a]=2&b[=3&c]=4"). + must_equal "[]" => "1", "[a]" => "2", "b[" => "3", "c]" => "4" + + Rack::Utils.parse_nested_query("d[[]=5&e][]=6&f[[]]=7"). + must_equal "d" => {"[" => "5"}, "e]" => ["6"], "f" => { "[" => { "]" => "7" } } + + Rack::Utils.parse_nested_query("g[h]i=8&j[k]l[m]=9"). + must_equal "g" => { "h" => { "i" => "8" } }, "j" => { "k" => { "l[m]" =>"9" } } + + Rack::Utils.parse_nested_query("l[[[[[[[[]]]]]]]=10"). + must_equal "l"=>{"[[[[[[["=>{"]]]]]]"=>"10"}} + end + it "allow setting the params hash class to use for parsing query strings" do begin default_parser = Rack::Utils.default_query_parser @@ -263,7 +296,7 @@ describe Rack::Utils do @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) + Rack::Utils.default_query_parser = Rack::QueryParser.new(param_parser_class, 100) h1 = Rack::Utils.parse_query(",foo=bar;,", ";,") h1[:foo].must_equal "bar" h2 = Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=2") @@ -295,9 +328,9 @@ describe Rack::Utils do assert_nested_query("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F", "my weird field" => "q1!2\"'w$5&7/z8)?") - Rack::Utils.build_nested_query("foo" => [nil]).must_equal "foo[]" - Rack::Utils.build_nested_query("foo" => [""]).must_equal "foo[]=" - Rack::Utils.build_nested_query("foo" => ["bar"]).must_equal "foo[]=bar" + Rack::Utils.build_nested_query("foo" => [nil]).must_equal "foo%5B%5D" + Rack::Utils.build_nested_query("foo" => [""]).must_equal "foo%5B%5D=" + Rack::Utils.build_nested_query("foo" => ["bar"]).must_equal "foo%5B%5D=bar" Rack::Utils.build_nested_query('foo' => []).must_equal '' Rack::Utils.build_nested_query('foo' => {}).must_equal '' Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => []).must_equal 'foo=bar' @@ -308,35 +341,39 @@ describe Rack::Utils do Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => ''). must_equal 'foo=bar&baz=' Rack::Utils.build_nested_query('foo' => ['1', '2']). - must_equal 'foo[]=1&foo[]=2' + must_equal 'foo%5B%5D=1&foo%5B%5D=2' Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => ['1', '2', '3']). - must_equal 'foo=bar&baz[]=1&baz[]=2&baz[]=3' + must_equal 'foo=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3' Rack::Utils.build_nested_query('foo' => ['bar'], 'baz' => ['1', '2', '3']). - must_equal 'foo[]=bar&baz[]=1&baz[]=2&baz[]=3' + must_equal 'foo%5B%5D=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3' Rack::Utils.build_nested_query('foo' => ['bar'], 'baz' => ['1', '2', '3']). - must_equal 'foo[]=bar&baz[]=1&baz[]=2&baz[]=3' + must_equal 'foo%5B%5D=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3' Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => '1' } }). - must_equal 'x[y][z]=1' + must_equal 'x%5By%5D%5Bz%5D=1' Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => ['1'] } }). - must_equal 'x[y][z][]=1' + must_equal 'x%5By%5D%5Bz%5D%5B%5D=1' Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => ['1', '2'] } }). - must_equal 'x[y][z][]=1&x[y][z][]=2' + must_equal 'x%5By%5D%5Bz%5D%5B%5D=1&x%5By%5D%5Bz%5D%5B%5D=2' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1' }] }). - must_equal 'x[y][][z]=1' + must_equal 'x%5By%5D%5B%5D%5Bz%5D=1' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => ['1'] }] }). - must_equal 'x[y][][z][]=1' + must_equal 'x%5By%5D%5B%5D%5Bz%5D%5B%5D=1' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => '2' }] }). - must_equal 'x[y][][z]=1&x[y][][w]=2' + must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bw%5D=2' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'v' => { 'w' => '1' } }] }). - must_equal 'x[y][][v][w]=1' + must_equal 'x%5By%5D%5B%5D%5Bv%5D%5Bw%5D=1' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'v' => { 'w' => '2' } }] }). - must_equal 'x[y][][z]=1&x[y][][v][w]=2' + must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bv%5D%5Bw%5D=2' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1' }, { 'z' => '2' }] }). - must_equal 'x[y][][z]=1&x[y][][z]=2' + must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bz%5D=2' Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => 'a' }, { 'z' => '2', 'w' => '3' }] }). - must_equal 'x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3' + must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bw%5D=a&x%5By%5D%5B%5D%5Bz%5D=2&x%5By%5D%5B%5D%5Bw%5D=3' Rack::Utils.build_nested_query({ "foo" => ["1", ["2"]] }). - must_equal 'foo[]=1&foo[][]=2' + must_equal 'foo%5B%5D=1&foo%5B%5D%5B%5D=2' + + # A nested hash is the same as string keys with brackets. + Rack::Utils.build_nested_query('foo' => { 'bar' => 'baz' }). + must_equal Rack::Utils.build_nested_query('foo[bar]' => 'baz') lambda { Rack::Utils.build_nested_query("foo=bar") }. must_raise(ArgumentError). @@ -344,7 +381,7 @@ describe Rack::Utils do end it 'performs the inverse function of #parse_nested_query' do - [{ "foo" => nil, "bar" => "" }, + [{ "bar" => "" }, { "foo" => "bar", "baz" => "" }, { "foo" => ["1", "2"] }, { "foo" => "bar", "baz" => ["1", "2", "3"] }, @@ -392,6 +429,45 @@ describe Rack::Utils do ] end + it "parses RFC 7239 Forwarded header" do + Rack::Utils.forwarded_values('for=3.4.5.6').must_equal({ + for: [ '3.4.5.6' ], + }) + + Rack::Utils.forwarded_values(';;;for=3.4.5.6,,').must_equal({ + for: [ '3.4.5.6' ], + }) + + Rack::Utils.forwarded_values('for=3.4.5.6').must_equal({ + for: [ '3.4.5.6' ], + }) + + Rack::Utils.forwarded_values('for = 3.4.5.6').must_equal({ + for: [ '3.4.5.6' ], + }) + + Rack::Utils.forwarded_values('for="3.4.5.6"').must_equal({ + for: [ '3.4.5.6' ], + }) + + Rack::Utils.forwarded_values('for=3.4.5.6;proto=https').must_equal({ + for: [ '3.4.5.6' ], + proto: [ 'https' ] + }) + + Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https').must_equal({ + for: [ '3.4.5.6' ], + proto: [ 'http', 'https' ] + }) + + Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https; for=1.2.3.4').must_equal({ + for: [ '3.4.5.6', '1.2.3.4' ], + proto: [ 'http', 'https' ] + }) + + Rack::Utils.forwarded_values('for=3.4.5.6; foo=bar').must_be_nil + end + it "select best quality match" do Rack::Utils.best_q_match("text/html", %w[text/html]).must_equal "text/html" @@ -463,6 +539,7 @@ describe Rack::Utils do it "should perform constant time string comparison" do Rack::Utils.secure_compare('a', 'a').must_equal true Rack::Utils.secure_compare('a', 'b').must_equal false + Rack::Utils.secure_compare('a', 'bb').must_equal false end it "return status code for integer" do @@ -487,10 +564,6 @@ describe Rack::Utils do Rack::Utils.rfc2822(Time.at(0).gmtime).must_equal "Thu, 01 Jan 1970 00:00:00 -0000" end - it "return rfc2109 format from rfc2109 helper" do - Rack::Utils.rfc2109(Time.at(0).gmtime).must_equal "Thu, 01-Jan-1970 00:00:00 GMT" - end - it "clean directory traversal" do Rack::Utils.clean_path_info("/cgi/../cgi/test").must_equal "/cgi/test" Rack::Utils.clean_path_info(".").must_be_empty @@ -513,6 +586,9 @@ end describe Rack::Utils, "cookies" do it "parses cookies" do + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "a=b; ; c=d") + Rack::Utils.parse_cookies(env).must_equal({ "a" => "b", "c" => "d" }) + env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m") Rack::Utils.parse_cookies(env).must_equal({ "zoo" => "m" }) @@ -536,236 +612,192 @@ describe Rack::Utils, "cookies" do cookies.must_equal({ "%66oo" => "baz", "foo" => "bar" }) end - it "adds new cookies to nil header" do + it "generates appropriate cookie header value" do + Rack::Utils.set_cookie_header('name', 'value').must_equal 'name=value' + Rack::Utils.set_cookie_header('name', %w[value]).must_equal 'name=value' + Rack::Utils.set_cookie_header('name', %w[va ue]).must_equal 'name=va&ue' + end + + deprecated "adds new cookies to nil header" do Rack::Utils.add_cookie_to_header(nil, 'name', 'value').must_equal 'name=value' end - it "adds new cookies to blank header" do + deprecated "adds new cookies to blank header" do header = '' Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal 'name=value' header.must_equal '' end - it "adds new cookies to string header" do + deprecated "adds new cookies to string header" do header = 'existing-cookie' - Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value" + Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal ["existing-cookie", "name=value"] header.must_equal 'existing-cookie' end - it "adds new cookies to array header" do + deprecated "adds new cookies to array header" do header = %w[ existing-cookie ] - Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value" + Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal ["existing-cookie", "name=value"] header.must_equal %w[ existing-cookie ] end - it "adds new cookies to an unrecognized header" do + deprecated "adds new cookies to an unrecognized header" do lambda { Rack::Utils.add_cookie_to_header(Object.new, 'name', 'value') }.must_raise ArgumentError end it "sets and deletes cookies in header hash" do - header = { 'Set-Cookie' => '' } - Rack::Utils.set_cookie_header!(header, 'name', 'value').must_be_nil - header['Set-Cookie'].must_equal 'name=value' - Rack::Utils.set_cookie_header!(header, 'name2', 'value2').must_be_nil - header['Set-Cookie'].must_equal "name=value\nname2=value2" - Rack::Utils.set_cookie_header!(header, 'name2', 'value3').must_be_nil - header['Set-Cookie'].must_equal "name=value\nname2=value2\nname2=value3" - - Rack::Utils.delete_cookie_header!(header, 'name2').must_be_nil - header['Set-Cookie'].must_equal "name=value\nname2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" - Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil - header['Set-Cookie'].must_equal "name2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT\nname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" - - header = { 'Set-Cookie' => nil } - Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil - header['Set-Cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" - - header = { 'Set-Cookie' => [] } - Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil - header['Set-Cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + headers = {} + Rack::Utils.set_cookie_header!(headers, 'name', 'value') + headers['set-cookie'].must_equal 'name=value' + Rack::Utils.set_cookie_header!(headers, 'name2', 'value2') + headers['set-cookie'].must_equal ['name=value', 'name2=value2'] + Rack::Utils.set_cookie_header!(headers, 'name2', 'value3') + headers['set-cookie'].must_equal ['name=value', 'name2=value2', 'name2=value3'] end -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 - # 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 - 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)] + it "encodes cookie key values by default" do + Rack::Utils.set_cookie_header('na e', 'value').must_equal 'na+e=value' 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)] + it "does not encode cookie key values if :escape_key is false" do + Rack::Utils.set_cookie_header('na e', value: 'value', escape_key: false).must_equal 'na e=value' 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)] - end + it "deletes cookies in header field" do + header = [] - 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 [] - end + Rack::Utils.delete_set_cookie_header!(header, 'name2') + header.must_equal [ + "name2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] - 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.delete_set_cookie_header!(header, 'name') + header.must_equal [ + "name2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT", + "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] end -end -describe Rack::Utils::HeaderHash do - it "retain header case" do - h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...") - h['ETag'] = 'Boo!' - h.to_hash.must_equal "Content-MD5" => "d5ff4e2a0 ...", "ETag" => 'Boo!' - end + it "deletes cookies in header field with domain" do + header = [] - it "check existence of keys case insensitively" do - h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...") - h.must_include 'content-md5' - h.wont_include 'ETag' + Rack::Utils.delete_set_cookie_header!(header, 'name', {domain: "mydomain.com"}) + header.must_equal [ + "name=; domain=mydomain.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] end - it "create deep HeaderHash copy on dup" do - h1 = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...") - h2 = h1.dup + it "deletes cookies in header field with path" do + header = [] - h1.must_include 'content-md5' - h2.must_include 'content-md5' - - h2.delete("Content-MD5") - - h2.wont_include 'content-md5' - h1.must_include 'content-md5' - end - - 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' + Rack::Utils.delete_set_cookie_header!(header, 'name', {path: "/a/b"}) + header.must_equal [ + "name=; path=/a/b; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ] end - it "overwrite case insensitively and assume the new key's case" do - h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz") - h["foo-bar"] = "bizzle" - h["FOO-BAR"].must_equal "bizzle" - h.length.must_equal 1 - h.to_hash.must_equal "foo-bar" => "bizzle" - end + it "sets and deletes cookies in header hash" do + header = { 'set-cookie' => nil } + Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil + header['set-cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" - it "be converted to real Hash" do - h = Rack::Utils::HeaderHash.new("foo" => "bar") - h.to_hash.must_be_instance_of Hash + header = { 'set-cookie' => nil } + Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil + header['set-cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" end - it "convert Array values to Strings when converting to Hash" do - h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"]) - h.to_hash.must_equal({ "foo" => "bar\nbaz" }) + deprecated "sets deleted cookie" do + Rack::Utils.make_delete_cookie_header(nil, 'name', {}). + must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" + Rack::Utils.add_remove_cookie_to_header(nil, 'name'). + must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT" end +end - it "replace hashes correctly" do - h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz") - j = { "foo" => "bar" } - h.replace(j) - h["foo"].must_equal "bar" +describe Rack::Utils, "get_byte_ranges" do + deprecated "pase simple byte ranges from env" do + Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 500).must_equal [(123..456)] end - it "be able to delete the given key case-sensitively" do - h = Rack::Utils::HeaderHash.new("foo" => "bar") - h.delete("foo") - h["foo"].must_be_nil - h["FOO"].must_be_nil + it "ignore missing or syntactically invalid byte ranges" do + Rack::Utils.get_byte_ranges(nil, 500).must_be_nil + Rack::Utils.get_byte_ranges("foobar", 500).must_be_nil + Rack::Utils.get_byte_ranges("furlongs=123-456", 500).must_be_nil + Rack::Utils.get_byte_ranges("bytes=", 500).must_be_nil + Rack::Utils.get_byte_ranges("bytes=-", 500).must_be_nil + Rack::Utils.get_byte_ranges("bytes=123,456", 500).must_be_nil + # A range of non-positive length is syntactically invalid and ignored: + Rack::Utils.get_byte_ranges("bytes=456-123", 500).must_be_nil + Rack::Utils.get_byte_ranges("bytes=456-455", 500).must_be_nil end - it "be able to delete the given key case-insensitively" do - h = Rack::Utils::HeaderHash.new("foo" => "bar") - h.delete("FOO") - h["foo"].must_be_nil - h["FOO"].must_be_nil + it "parse simple byte ranges" do + Rack::Utils.get_byte_ranges("bytes=123-456", 500).must_equal [(123..456)] + Rack::Utils.get_byte_ranges("bytes=123-", 500).must_equal [(123..499)] + Rack::Utils.get_byte_ranges("bytes=-100", 500).must_equal [(400..499)] + Rack::Utils.get_byte_ranges("bytes=0-0", 500).must_equal [(0..0)] + Rack::Utils.get_byte_ranges("bytes=499-499", 500).must_equal [(499..499)] end - it "return the deleted value when #delete is called on an existing key" do - h = Rack::Utils::HeaderHash.new("foo" => "bar") - h.delete("Foo").must_equal "bar" + it "parse several byte ranges" do + Rack::Utils.get_byte_ranges("bytes=500-600,601-999", 1000).must_equal [(500..600), (601..999)] end - it "return nil when #delete is called on a non-existent key" do - h = Rack::Utils::HeaderHash.new("foo" => "bar") - h.delete("Hello").must_be_nil + it "truncate byte ranges" do + Rack::Utils.get_byte_ranges("bytes=123-999", 500).must_equal [(123..499)] + Rack::Utils.get_byte_ranges("bytes=600-999", 500).must_equal [] + Rack::Utils.get_byte_ranges("bytes=-999", 500).must_equal [(0..499)] end - it "dups given HeaderHash" do - a = Rack::Utils::HeaderHash.new("foo" => "bar") - b = Rack::Utils::HeaderHash.new(a) - b.object_id.wont_equal a.object_id - b.must_equal a + it "ignore unsatisfiable byte ranges" do + Rack::Utils.get_byte_ranges("bytes=500-501", 500).must_equal [] + Rack::Utils.get_byte_ranges("bytes=500-", 500).must_equal [] + Rack::Utils.get_byte_ranges("bytes=999-", 500).must_equal [] + Rack::Utils.get_byte_ranges("bytes=-0", 500).must_equal [] 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| - k.must_equal "foo" - v.must_equal "bar\nbaz" - end + it "handle byte ranges of empty files" do + Rack::Utils.get_byte_ranges("bytes=123-456", 0).must_equal [] + Rack::Utils.get_byte_ranges("bytes=0-", 0).must_equal [] + Rack::Utils.get_byte_ranges("bytes=-100", 0).must_equal [] + Rack::Utils.get_byte_ranges("bytes=0-0", 0).must_equal [] + Rack::Utils.get_byte_ranges("bytes=-0", 0).must_equal [] end +end - it "not create headers out of thin air" do - h = Rack::Utils::HeaderHash.new - h['foo'] - h['foo'].must_be_nil - h.wont_include 'foo' +describe Rack::Utils::HeaderHash do + deprecated ".[] returns Rack::Headers as is if not frozen" do + h1 = Rack::Headers["Content-MD5" => "d5ff4e2a0 ..."] + h2 = Rack::Utils::HeaderHash[h1] + h2.must_be_same_as h1 + h3 = Rack::Utils::HeaderHash[h1.freeze] + h3.wont_be_same_as h1 + h3.must_equal h1 end - it "uses memoized header hash" do - env = {} - headers = Rack::Utils::HeaderHash.new({ 'content-type' => "text/plain", "content-length" => "3" }) - - app = lambda do |env| - [200, headers, []] - end - - app = Rack::ContentLength.new(app) + deprecated ".[] returns instance of Rack::Headers" do + h = Rack::Utils::HeaderHash["Content-MD5" => "d5ff4e2a0 ..."] + h.must_be_kind_of Rack::Headers + h['content-md5'].must_equal "d5ff4e2a0 ..." - response = app.call(env) - assert_same response[1], headers + h = Rack::Utils::HeaderHash[[["Content-MD5","d5ff4e2a0 ..."]]] + h.must_be_kind_of Rack::Headers + h['content-md5'].must_equal "d5ff4e2a0 ..." end - it "duplicates header hash" do - env = {} - headers = Rack::Utils::HeaderHash.new({ 'content-type' => "text/plain", "content-length" => "3" }) - headers.freeze - - app = lambda do |env| - [200, headers, []] - end + deprecated ".new returns instance of Rack::Headers" do + h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...") + h.must_be_kind_of Rack::Headers + h['content-md5'].must_equal "d5ff4e2a0 ..." - app = Rack::ContentLength.new(app) + h = Rack::Utils::HeaderHash.new([["Content-MD5","d5ff4e2a0 ..."]]) + h.must_be_kind_of Rack::Headers + h['content-md5'].must_equal "d5ff4e2a0 ..." + end - response = app.call(env) - refute_same response[1], headers + it ".allocate raises" do + proc { Rack::Utils::HeaderHash.allocate }.must_raise TypeError end end @@ -779,7 +811,7 @@ describe Rack::Utils::Context do 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 @@ -823,4 +855,10 @@ describe Rack::Utils::Context do r5.status.must_equal 200 r4.body.must_equal r5.body end + + it "raises for invalid context" do + proc do + Rack::Utils::Context.new(nil, test_target1) + end.must_raise RuntimeError + end end diff --git a/test/spec_version.rb b/test/spec_version.rb index 68c4b4c72815672df01c11922cbd1817b8d772d2..bdd903d1d7d4b09751f9446f401610e7a502715d 100644 --- a/test/spec_version.rb +++ b/test/spec_version.rb @@ -2,10 +2,14 @@ require_relative 'helper' +separate_testing do + require_relative '../lib/rack/version' +end + describe Rack do describe 'version' do - it 'defaults to a hard-coded api version' do - Rack.version.must_equal "1.3" + it 'is a version string' do + Rack::RELEASE.must_match(/\d+\.\d+\.\d+/) end end end diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb deleted file mode 100644 index a3c324a90e531e62c9cabb947e76430ab558eeba..0000000000000000000000000000000000000000 --- a/test/spec_webrick.rb +++ /dev/null @@ -1,216 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'thread' -require_relative 'testrequest' - -Thread.abort_on_exception = true - -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.mount "/test", Rack::Handler::WEBrick, - Rack::Lint.new(TestRequest.new) - @thread = Thread.new { @server.start } - trap(:INT) { @server.shutdown } - @status_thread = Thread.new do - seconds = 10 - wait_time = 0.1 - until is_running? || seconds <= 0 - seconds -= wait_time - sleep wait_time - end - raise "Server never reached status 'Running'" unless is_running? - end - end - - def is_running? - @server.status == :Running - end - - it "respond" do - GET("/test") - status.must_equal 200 - end - - it "be a WEBrick" do - GET("/test") - status.must_equal 200 - response["SERVER_SOFTWARE"].must_match(/WEBrick/) - response["HTTP_VERSION"].must_equal "HTTP/1.1" - response["SERVER_PROTOCOL"].must_equal "HTTP/1.1" - response["SERVER_PORT"].must_equal "9202" - response["SERVER_NAME"].must_equal "127.0.0.1" - end - - it "have rack headers" do - GET("/test") - 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"] - 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 "/test" - response["PATH_INFO"].must_equal "" - 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 "/test/foo" - response["PATH_INFO"].must_equal "/foo" - response["QUERY_STRING"].must_equal "quux=1" - - GET("/test/foo%25encoding?quux=1") - response["REQUEST_METHOD"].must_equal "GET" - response["SCRIPT_NAME"].must_equal "/test" - response["REQUEST_PATH"].must_equal "/test/foo%25encoding" - response["PATH_INFO"].must_equal "/foo%25encoding" - 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 "/test" - response["PATH_INFO"].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 - - it "correctly set cookies" do - @server.mount "/cookie-test", Rack::Handler::WEBrick, - Rack::Lint.new(lambda { |req| - res = Rack::Response.new - res.set_cookie "one", "1" - res.set_cookie "two", "2" - res.finish - }) - - Net::HTTP.start(@host, @port) { |http| - res = http.get("/cookie-test") - res.code.to_i.must_equal 200 - res.get_fields("set-cookie").must_equal ["one=1", "two=2"] - } - end - - it "provide a .run" do - queue = Queue.new - - t = Thread.new do - Rack::Handler::WEBrick.run(lambda {}, - Host: '127.0.0.1', - Port: 9210, - Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN), - AccessLog: []) { |server| - 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 - - it "return repeated headers" do - @server.mount "/headers", Rack::Handler::WEBrick, - Rack::Lint.new(lambda { |req| - [ - 401, - { "Content-Type" => "text/plain", - "WWW-Authenticate" => "Bar realm=X\nBaz realm=Y" }, - [""] - ] - }) - - Net::HTTP.start(@host, @port) { |http| - res = http.get("/headers") - res.code.to_i.must_equal 401 - res["www-authenticate"].must_equal "Bar realm=X, Baz realm=Y" - } - end - - it "support Rack partial hijack" do - io_lambda = lambda{ |io| - 5.times do - io.write "David\r\n" - end - io.close - } - - @server.mount "/partial", Rack::Handler::WEBrick, - Rack::Lint.new(lambda{ |req| - [ - 200, - [ [ "rack.hijack", io_lambda ] ], - [""] - ] - }) - - Net::HTTP.start(@host, @port){ |http| - res = http.get("/partial") - res.body.must_equal "David\r\nDavid\r\nDavid\r\nDavid\r\nDavid\r\n" - } - end - - it "produce correct HTTP semantics with and without app chunking" do - @server.mount "/chunked", Rack::Handler::WEBrick, - Rack::Lint.new(lambda{ |req| - [ - 200, - { "Transfer-Encoding" => "chunked" }, - ["7\r\nchunked\r\n0\r\n\r\n"] - ] - }) - - Net::HTTP.start(@host, @port){ |http| - res = http.get("/chunked") - res["Transfer-Encoding"].must_equal "chunked" - res["Content-Length"].must_be_nil - res.body.must_equal "chunked" - } - end - - after do - @status_thread.join - @server.shutdown - @thread.join - end -end diff --git a/test/testrequest.rb b/test/testrequest.rb deleted file mode 100644 index b85aae8312dafedbfa6a64b48a84b8cdb8661b06..0000000000000000000000000000000000000000 --- a/test/testrequest.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -require 'yaml' -require_relative 'psych_fix' -require 'net/http' -require 'rack/lint' - -class TestRequest - NOSERIALIZE = [Method, Proc, Rack::Lint::InputWrapper] - - def call(env) - status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200 - 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) } } - body = minienv.to_yaml - size = body.bytesize - [status, { "Content-Type" => "text/yaml", "Content-Length" => size.to_s }, [body]] - end - - module Helpers - attr_reader :status, :response - - ROOT = File.expand_path(File.dirname(__FILE__) + "/..") - ENV["RUBYOPT"] = "-I#{ROOT}/lib -rubygems" - - def root - ROOT - end - - def rackup - "#{ROOT}/bin/rackup" - end - - def GET(path, header = {}) - Net::HTTP.start(@host, @port) { |http| - user = header.delete(:user) - passwd = header.delete(:passwd) - - get = Net::HTTP::Get.new(path, header) - get.basic_auth user, passwd if user && passwd - http.request(get) { |response| - @status = response.code.to_i - begin - @response = YAML.unsafe_load(response.body) - rescue TypeError, ArgumentError - @response = nil - end - } - } - end - - def POST(path, formdata = {}, header = {}) - Net::HTTP.start(@host, @port) { |http| - user = header.delete(:user) - passwd = header.delete(:passwd) - - post = Net::HTTP::Post.new(path, header) - post.form_data = formdata - post.basic_auth user, passwd if user && passwd - http.request(post) { |response| - @status = response.code.to_i - @response = YAML.unsafe_load(response.body) - } - } - end - end -end - -class StreamingRequest - def self.call(env) - [200, { "Content-Type" => "text/plain" }, new] - end - - def each - yield "hello there!\n" - sleep 5 - yield "that is all.\n" - end -end diff --git a/test/unregistered_handler/rack/handler/unregistered.rb b/test/unregistered_handler/rack/handler/unregistered.rb deleted file mode 100644 index e98468cc6669fa05d1106ec6d5e5b09229e3ea74..0000000000000000000000000000000000000000 --- a/test/unregistered_handler/rack/handler/unregistered.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Rack - module Handler - # this class doesn't do anything, we're just seeing if we get it. - class Unregistered - end - end -end diff --git a/test/unregistered_handler/rack/handler/unregistered_long_one.rb b/test/unregistered_handler/rack/handler/unregistered_long_one.rb deleted file mode 100644 index 87c6c25431f90fa5a5128c14fcf178682be1509f..0000000000000000000000000000000000000000 --- a/test/unregistered_handler/rack/handler/unregistered_long_one.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Rack - module Handler - # this class doesn't do anything, we're just seeing if we get it. - class UnregisteredLongOne - end - end -end