The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .editorconfig
├── .github
    ├── dependabot.yml
    └── workflows
    │   ├── depsreview.yaml
    │   ├── documentation.yaml
    │   ├── test-external.yaml
    │   └── test.yaml
├── .gitignore
├── .mailmap
├── .rdoc_options
├── .rubocop.yml
├── .yardopts
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── SECURITY.md
├── SPEC.rdoc
├── UPGRADE-GUIDE.md
├── config
    └── external.yaml
├── contrib
    ├── LICENSE.md
    ├── logo-lossless.webp
    ├── logo.webp
    └── rdoc.css
├── docs
    └── index.html
├── lib
    ├── rack.rb
    └── rack
    │   ├── auth
    │       ├── abstract
    │       │   ├── handler.rb
    │       │   └── request.rb
    │       └── basic.rb
    │   ├── bad_request.rb
    │   ├── body_proxy.rb
    │   ├── builder.rb
    │   ├── cascade.rb
    │   ├── common_logger.rb
    │   ├── conditional_get.rb
    │   ├── config.rb
    │   ├── constants.rb
    │   ├── content_length.rb
    │   ├── content_type.rb
    │   ├── deflater.rb
    │   ├── directory.rb
    │   ├── etag.rb
    │   ├── events.rb
    │   ├── files.rb
    │   ├── head.rb
    │   ├── headers.rb
    │   ├── lint.rb
    │   ├── lock.rb
    │   ├── media_type.rb
    │   ├── method_override.rb
    │   ├── mime.rb
    │   ├── mock.rb
    │   ├── mock_request.rb
    │   ├── mock_response.rb
    │   ├── multipart.rb
    │   ├── multipart
    │       ├── generator.rb
    │       ├── parser.rb
    │       └── uploaded_file.rb
    │   ├── null_logger.rb
    │   ├── query_parser.rb
    │   ├── recursive.rb
    │   ├── reloader.rb
    │   ├── request.rb
    │   ├── response.rb
    │   ├── rewindable_input.rb
    │   ├── runtime.rb
    │   ├── sendfile.rb
    │   ├── show_exceptions.rb
    │   ├── show_status.rb
    │   ├── static.rb
    │   ├── tempfile_reaper.rb
    │   ├── urlmap.rb
    │   ├── utils.rb
    │   └── version.rb
├── rack.gemspec
└── test
    ├── .bacon
    ├── builder
        ├── an_underscore_app.rb
        ├── bom.ru
        ├── comment.ru
        ├── end.ru
        ├── frozen.ru
        ├── line.ru
        └── options.ru
    ├── cgi
        ├── assets
        │   ├── folder
        │   │   └── test.js
        │   ├── fonts
        │   │   └── font.eot
        │   ├── images
        │   │   ├── favicon.ico
        │   │   └── image.png
        │   ├── index.html
        │   ├── javascripts
        │   │   └── app.js
        │   └── stylesheets
        │   │   └── app.css
        ├── rackup_stub.rb
        ├── sample_rackup.ru
        ├── test
        ├── test+directory
        │   └── test+file
        ├── test.gz
        └── test.ru
    ├── gemloader.rb
    ├── helper.rb
    ├── multipart
        ├── bad_robots
        ├── binary
        ├── content_type_and_no_disposition
        ├── content_type_and_no_filename
        ├── content_type_and_unknown_charset
        ├── empty
        ├── end_boundary_first
        ├── fail_16384_nofile
        ├── file1.txt
        ├── filename_and_modification_param
        ├── filename_and_no_name
        ├── filename_multi
        ├── filename_with_encoded_words
        ├── filename_with_escaped_quotes
        ├── filename_with_escaped_quotes_and_modification_param
        ├── filename_with_null_byte
        ├── filename_with_percent_escaped_quotes
        ├── filename_with_plus
        ├── filename_with_single_quote
        ├── filename_with_unescaped_percentages
        ├── filename_with_unescaped_percentages2
        ├── filename_with_unescaped_percentages3
        ├── filename_with_unescaped_quotes
        ├── ie
        ├── invalid_character
        ├── mixed_files
        ├── multiple_encodings
        ├── nested
        ├── none
        ├── preceding_boundary
        ├── quoted
        ├── rack-logo.png
        ├── robust_field_separation
        ├── semicolon
        ├── space case.txt
        ├── text
        ├── three_files_three_fields
        ├── unity3d_wwwform
        └── webkit
    ├── psych_fix.rb
    ├── rackup
        ├── .gitignore
        └── config.ru
    ├── spec_auth_basic.rb
    ├── spec_body_proxy.rb
    ├── spec_builder.rb
    ├── spec_cascade.rb
    ├── spec_common_logger.rb
    ├── spec_conditional_get.rb
    ├── spec_config.rb
    ├── spec_content_length.rb
    ├── spec_content_type.rb
    ├── spec_deflater.rb
    ├── spec_directory.rb
    ├── spec_etag.rb
    ├── spec_events.rb
    ├── spec_files.rb
    ├── spec_head.rb
    ├── spec_headers.rb
    ├── spec_lint.rb
    ├── spec_lock.rb
    ├── spec_media_type.rb
    ├── spec_method_override.rb
    ├── spec_mime.rb
    ├── spec_mock_request.rb
    ├── spec_mock_response.rb
    ├── spec_multipart.rb
    ├── spec_null_logger.rb
    ├── spec_query_parser.rb
    ├── spec_recursive.rb
    ├── spec_request.rb
    ├── spec_response.rb
    ├── spec_rewindable_input.rb
    ├── spec_runtime.rb
    ├── spec_sendfile.rb
    ├── spec_show_exceptions.rb
    ├── spec_show_status.rb
    ├── spec_static.rb
    ├── spec_tempfile_reaper.rb
    ├── spec_urlmap.rb
    ├── spec_utils.rb
    ├── spec_version.rb
    └── static
        ├── another
            └── index.html
        ├── foo.html
        └── index.html


/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | 
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | 


--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |   - package-ecosystem: "github-actions"
4 |     directory: "/"
5 |     schedule:
6 |       interval: "weekly"
7 | 


--------------------------------------------------------------------------------
/.github/workflows/depsreview.yaml:
--------------------------------------------------------------------------------
 1 | name: 'Dependency Review'
 2 | on: [pull_request]
 3 | 
 4 | permissions:
 5 |   contents: read
 6 | 
 7 | jobs:
 8 |   dependency-review:
 9 |     runs-on: ubuntu-latest
10 |     steps:
11 |       - name: 'Checkout Repository'
12 |         uses: actions/checkout@v4
13 |       - name: 'Dependency Review'
14 |         uses: actions/dependency-review-action@v4
15 | 


--------------------------------------------------------------------------------
/.github/workflows/documentation.yaml:
--------------------------------------------------------------------------------
 1 | name: Documentation
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 |       - documentation-*
 8 | 
 9 | permissions:
10 |   contents: read
11 |   pages: write
12 |   id-token: write
13 | 
14 | concurrency:
15 |   group: "pages"
16 |   cancel-in-progress: true
17 | 
18 | jobs:
19 |   generate:
20 |     runs-on: ubuntu-latest
21 |     
22 |     steps:
23 |     - uses: actions/checkout@v4
24 | 
25 |     - uses: ruby/setup-ruby@v1
26 |       with:
27 |         ruby-version: "3.4"
28 |         bundler-cache: true
29 |     
30 |     - name: Generate main documentation
31 |       timeout-minutes: 5
32 |       run: bundle exec rdoc --op docs/main --main README.md --template-stylesheets contrib/rdoc.css
33 | 
34 |     - name: "Generate 3.1 documentation"
35 |       timeout-minutes: 5
36 |       run: 'gem install rack --version "< 3.2" && gem unpack rack -v "< 3.2" && cd rack-3.1* && bundle exec rdoc --op ../docs/3.1 --main README.md'
37 | 
38 |     - name: "Generate 3.0 documentation"
39 |       timeout-minutes: 5
40 |       run: 'gem install rack --version "< 3.1" && gem unpack rack -v "< 3.1" && cd rack-3.0* && bundle exec rdoc --op ../docs/3.0 --main README.md'
41 | 
42 |     - name: "Generate 2.2 documentation"
43 |       timeout-minutes: 5
44 |       run: 'gem install rack --version "< 2.3" && gem unpack rack -v "< 2.3" && cd rack-2.2* && bundle exec rdoc --op ../docs/2.2 --main README.rdoc'
45 |     
46 |     - name: Copy contrib
47 |       timeout-minutes: 5
48 |       run: cp -r contrib docs/main
49 |     
50 |     - name: Upload documentation artifact
51 |       uses: actions/upload-pages-artifact@v3
52 |       with:
53 |         path: docs
54 |   
55 |   deploy:
56 |     runs-on: ubuntu-latest
57 |     
58 |     environment:
59 |       name: github-pages
60 |       url: ${{steps.deployment.outputs.page_url}}
61 |     
62 |     needs: generate
63 |     steps:
64 |       - name: Deploy to GitHub Pages
65 |         id: deployment
66 |         uses: actions/deploy-pages@v4
67 | 


--------------------------------------------------------------------------------
/.github/workflows/test-external.yaml:
--------------------------------------------------------------------------------
 1 | name: Test External
 2 | 
 3 | on: [push, pull_request]
 4 | 
 5 | permissions:
 6 |   contents: read
 7 | 
 8 | jobs:
 9 |   test:
10 |     strategy:
11 |       fail-fast: false
12 |       matrix:
13 |         os: [ubuntu-latest]
14 |         ruby: ['3.2', '3.3', '3.4']
15 | 
16 |     runs-on: ${{matrix.os}}
17 |     env:
18 |       CI: external
19 | 
20 |     steps:
21 |     - uses: actions/checkout@v4
22 | 
23 |     - uses: ruby/setup-ruby-pkgs@v1
24 |       with:
25 |         ruby-version: ${{matrix.ruby}}
26 |         bundler-cache: true
27 |         apt-get: pandoc
28 |         brew: pandoc
29 | 
30 |     - name: Change permissions
31 |       run: chmod -R o-w /opt/hostedtoolcache/Ruby
32 | 
33 |     - run: bundle exec bake test:external
34 | 


--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
 1 | name: Test
 2 | 
 3 | on: [push, pull_request]
 4 | 
 5 | permissions:
 6 |   contents: read
 7 | 
 8 | jobs:
 9 |   test:
10 |     strategy:
11 |       fail-fast: false
12 |       matrix:
13 |         os:
14 |           - ubuntu-latest
15 |         ruby:
16 |           - '2.4'
17 |           - '2.5'
18 |           - '2.6'
19 |           - '2.7'
20 |           - '3.0'
21 |           - '3.1'
22 |           - '3.2'
23 |           - '3.3'
24 |           - '3.4'
25 |           - ruby-head
26 |           - jruby-head
27 |           - truffleruby-head
28 |         include:
29 |           - os: macos-latest
30 |             ruby: '3.1'
31 |     runs-on: ${{matrix.os}}
32 |     env:
33 |       CI: spec
34 | 
35 |     steps:
36 |     - uses: actions/checkout@v4
37 | 
38 |     - uses: ruby/setup-ruby@v1
39 |       with:
40 |         ruby-version: ${{matrix.ruby}}
41 |         bundler-cache: true
42 |       continue-on-error: ${{ startsWith(matrix.ruby, '2.4') || startsWith(matrix.ruby, '2.5') }}
43 | 
44 |     - run: bundle exec rake
45 |       continue-on-error: ${{ startsWith(matrix.ruby, '2.4') || startsWith(matrix.ruby, '2.5') }}
46 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | RDOX
 2 | ChangeLog
 3 | *.gem
 4 | lighttpd.errors
 5 | *.rbc
 6 | stage
 7 | *.tar.gz
 8 | Gemfile.lock
 9 | .rbx
10 | doc
11 | /.bundle
12 | /.yardoc
13 | /coverage
14 | /external
15 | 


--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
 1 | Leah Neukirchen <leah@vuxu.org> <chneukirchen@gmail.com>
 2 | Mickaël Riga <mig@mypeplum.com>
 3 | James Tucker <jftucker@gmail.com>
 4 | Ravil Bayramgalin <brainopia@evilmartians.com>
 5 | Pavel Rosicky <pavel.rosicky@easy.cz>
 6 | Yuichiro Kaneko <spiketeika@gmail.com>
 7 | Yoshiyuki Hirano <yhirano@me.com>
 8 | Dima Fatko <fatkodima123@gmail.com>
 9 | Yudai Suzuki <3280467rec@gmail.com>
10 | Marc-André Cournoyer <macournoyer@gmail.com>
11 | Megan Batty <megan@stormbrew.ca> <graham-git@stormbrew.ca>
12 | Megan Batty <megan@stormbrew.ca> <graham@graham-battys-macbook.local>
13 | Richard Schneeman <richard.schneeman@gmail.com> <richard.schneeman+foo@gmail.com>
14 | Richard Schneeman <richard.schneeman@gmail.com>
15 | Wyatt Pan <wppurking@gmail.com>
16 | Kazuya Hotta <khotta116@gmail.com>
17 | Julik Tarkhanov <me@julik.nl>
18 | 


--------------------------------------------------------------------------------
/.rdoc_options:
--------------------------------------------------------------------------------
1 | ---
2 | autolink_excluded_words:
3 | - Rack
4 | 


--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
 1 | require:
 2 |   - rubocop-packaging
 3 | 
 4 | AllCops:
 5 |   TargetRubyVersion: 2.4
 6 |   DisabledByDefault: true
 7 |   Exclude:
 8 |     - '**/vendor/**/*'
 9 | 
10 | Style/FrozenStringLiteralComment:
11 |   Enabled: true
12 |   EnforcedStyle: always
13 |   Exclude:
14 |     - 'test/builder/bom.ru'
15 | 
16 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
17 | Style/HashSyntax:
18 |   Enabled: true
19 | 
20 | Style/MethodDefParentheses:
21 |   Enabled: true
22 | 
23 | Layout/EmptyLineAfterMagicComment:
24 |   Enabled: true
25 | 
26 | Layout/LeadingCommentSpace:
27 |   Enabled: true
28 |   Exclude:
29 |     - 'test/builder/options.ru'
30 | 
31 | Layout/SpaceAfterColon:
32 |   Enabled: true
33 | 
34 | Layout/SpaceAfterComma:
35 |   Enabled: true
36 | 
37 | Layout/SpaceAroundEqualsInParameterDefault:
38 |   Enabled: true
39 | 
40 | Layout/SpaceAroundKeyword:
41 |   Enabled: true
42 | 
43 | Layout/SpaceAroundOperators:
44 |   Enabled: true
45 | 
46 | Layout/SpaceBeforeComma:
47 |   Enabled: true
48 | 
49 | Layout/SpaceBeforeFirstArg:
50 |   Enabled: true
51 | 
52 | # Use `{ a: 1 }` not `{a:1}`.
53 | Layout/SpaceInsideHashLiteralBraces:
54 |   Enabled: true
55 | 
56 | Layout/IndentationStyle:
57 |   Enabled: true
58 | 
59 | Layout/TrailingWhitespace:
60 |   Enabled: true
61 | 
62 | Lint/DeprecatedOpenSSLConstant:
63 |   Enabled: true
64 | 


--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | -
2 | SPEC.rdoc
3 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
  1 | # Contributing to Rack
  2 | 
  3 | Rack is work of [hundreds of
  4 | contributors](https://github.com/rack/rack/graphs/contributors). You're
  5 | encouraged to submit [pull requests](https://github.com/rack/rack/pulls) and
  6 | [propose features and discuss issues](https://github.com/rack/rack/issues).
  7 | 
  8 | ## Backports
  9 | 
 10 | Only security patches are ideal for backporting to non-main release versions. If
 11 | you're not sure if your bug fix is backportable, you should open a discussion to
 12 | discuss it first.
 13 | 
 14 | The [Security Policy] documents which release versions will receive security
 15 | backports.
 16 | 
 17 | ## Fork the Project
 18 | 
 19 | Fork the [project on GitHub](https://github.com/rack/rack) and check out your
 20 | copy.
 21 | 
 22 | ```
 23 | git clone https://github.com/(your-github-username)/rack.git
 24 | cd rack
 25 | git remote add upstream https://github.com/rack/rack.git
 26 | ```
 27 | 
 28 | ## Create a Topic Branch
 29 | 
 30 | Make sure your fork is up-to-date and create a topic branch for your feature or
 31 | bug fix.
 32 | 
 33 | ```
 34 | git checkout main
 35 | git pull upstream main
 36 | git checkout -b my-feature-branch
 37 | ```
 38 | 
 39 | ## Running All Tests
 40 | 
 41 | Install all dependencies.
 42 | 
 43 | ```
 44 | bundle install
 45 | ```
 46 | 
 47 | Run all tests.
 48 | 
 49 | ```
 50 | rake test
 51 | ```
 52 | 
 53 | ## Write Tests
 54 | 
 55 | Try to write a test that reproduces the problem you're trying to fix or
 56 | describes a feature that you want to build.
 57 | 
 58 | We definitely appreciate pull requests that highlight or reproduce a problem,
 59 | even without a fix.
 60 | 
 61 | ## Write Code
 62 | 
 63 | Implement your feature or bug fix.
 64 | 
 65 | Make sure that all tests pass:
 66 | 
 67 | ```
 68 | bundle exec rake test
 69 | ```
 70 | 
 71 | ## Write Documentation
 72 | 
 73 | Document any external behavior in the [README](README.md).
 74 | 
 75 | ## Update Changelog
 76 | 
 77 | Add a line to [CHANGELOG](CHANGELOG.md).
 78 | 
 79 | ## Commit Changes
 80 | 
 81 | Make sure git knows your name and email address:
 82 | 
 83 | ```
 84 | git config --global user.name "Your Name"
 85 | git config --global user.email "contributor@example.com"
 86 | ```
 87 | 
 88 | Writing good commit logs is important. A commit log should describe what changed
 89 | and why.
 90 | 
 91 | ```
 92 | git add ...
 93 | git commit
 94 | ```
 95 | 
 96 | ## Push
 97 | 
 98 | ```
 99 | git push origin my-feature-branch
100 | ```
101 | 
102 | ## Make a Pull Request
103 | 
104 | Go to your fork of rack on GitHub and select your feature branch. Click the
105 | 'Pull Request' button and fill out the form. Pull requests are usually
106 | reviewed within a few days.
107 | 
108 | ## Rebase
109 | 
110 | If you've been working on a change for a while, rebase with upstream/main.
111 | 
112 | ```
113 | git fetch upstream
114 | git rebase upstream/main
115 | git push origin my-feature-branch -f
116 | ```
117 | 
118 | ## Make Required Changes
119 | 
120 | Amend your previous commit and force push the changes.
121 | 
122 | ```
123 | git commit --amend
124 | git push origin my-feature-branch -f
125 | ```
126 | 
127 | ## Check on Your Pull Request
128 | 
129 | Go back to your pull request after a few minutes and see whether it passed
130 | tests with GitHub Actions. Everything should look green, otherwise fix issues and
131 | amend your commit as described above.
132 | 
133 | ## Be Patient
134 | 
135 | It's likely that your change will not be merged and that the nitpicky
136 | maintainers will ask you to do more, or fix seemingly benign problems. Hang in
137 | there!
138 | 
139 | ## Thank You
140 | 
141 | Please do know that we really appreciate and value your time and work. We love
142 | you, really.
143 | 
144 | [Security Policy]: SECURITY.md
145 | 


--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | source 'https://rubygems.org'
 4 | 
 5 | gemspec
 6 | 
 7 | group :maintenance, optional: true do
 8 |   gem "rubocop", require: false
 9 |   gem "rubocop-packaging", require: false
10 | end
11 | 
12 | group :doc do
13 |   gem "rdoc"
14 | end
15 | 
16 | group :test do
17 |   gem "logger"
18 |   gem "webrick"
19 | 
20 |   unless ENV['CI'] == 'spec'
21 |     gem 'bake-test-external'
22 |   end
23 | end
24 | 


--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (C) 2007-2021 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to
 7 | deal in the Software without restriction, including without limitation the
 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 9 | sell copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | 


--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require "bundler/gem_tasks"
  4 | require "rake/testtask"
  5 | 
  6 | desc "Run all the tests"
  7 | task default: :test
  8 | 
  9 | desc "Install gem dependencies"
 10 | task :deps do
 11 |   require 'rubygems'
 12 |   spec = Gem::Specification.load('rack.gemspec')
 13 |   spec.dependencies.each do |dep|
 14 |     reqs = dep.requirements_list
 15 |     reqs = (["-v"] * reqs.size).zip(reqs).flatten
 16 |     # Use system over sh, because we want to ignore errors!
 17 |     system Gem.ruby, "-S", "gem", "install", '--conservative', dep.name, *reqs
 18 |   end
 19 | end
 20 | 
 21 | desc "Make an archive as .tar.gz"
 22 | task dist: %w[chmod changelog spec rdoc] do
 23 |   sh "git archive --format=tar --prefix=#{release}/ HEAD^{tree} >#{release}.tar"
 24 |   sh "pax -waf #{release}.tar -s ':^:#{release}/:' SPEC.rdoc ChangeLog doc rack.gemspec"
 25 |   sh "gzip -f -9 #{release}.tar"
 26 | end
 27 | 
 28 | desc "Make an official release"
 29 | task :officialrelease do
 30 |   puts "Official build for #{release}..."
 31 |   sh "rm -rf stage"
 32 |   sh "git clone --shared . stage"
 33 |   sh "cd stage && rake officialrelease_really"
 34 |   sh "mv stage/#{release}.tar.gz stage/#{release}.gem ."
 35 | end
 36 | 
 37 | task officialrelease_really: %w[spec dist gem] do
 38 |   sh "shasum #{release}.tar.gz #{release}.gem"
 39 | end
 40 | 
 41 | def release
 42 |   "rack-" + File.read('lib/rack/version.rb')[/RELEASE += +([\"\'])([\d][\w\.]+)\1/, 2]
 43 | end
 44 | 
 45 | desc "Make binaries executable"
 46 | task :chmod do
 47 |   Dir["bin/*"].each { |binary| File.chmod(0755, binary) }
 48 |   Dir["test/cgi/test*"].each { |binary| File.chmod(0755, binary) }
 49 | end
 50 | 
 51 | desc "Generate a ChangeLog"
 52 | task changelog: "ChangeLog"
 53 | 
 54 | file '.git/index'
 55 | file "ChangeLog" => '.git/index' do
 56 |   File.open("ChangeLog", "w") { |out|
 57 |     log = `git log -z`
 58 |     log.force_encoding(Encoding::BINARY)
 59 |     log.split("\0").map { |chunk|
 60 |       author = chunk[/Author: (.*)/, 1].strip
 61 |       date = chunk[/Date: (.*)/, 1].strip
 62 |       desc, detail = 
#39;.strip.split("\n", 2)
 63 |       detail ||= ""
 64 |       detail = detail.gsub(/.*darcs-hash:.*/, '')
 65 |       detail.rstrip!
 66 |       out.puts "#{date}  #{author}"
 67 |       out.puts "  * #{desc.strip}"
 68 |       out.puts detail  unless detail.empty?
 69 |       out.puts
 70 |     }
 71 |   }
 72 | end
 73 | 
 74 | desc "Generate Rack Specification"
 75 | task spec: "SPEC.rdoc"
 76 | 
 77 | file 'lib/rack/lint.rb'
 78 | file "SPEC.rdoc" => 'lib/rack/lint.rb' do
 79 |   line_pattern = /\A\s*## ?(?<text>.*?)(?<wrap>\\)?$/u
 80 | 
 81 |   File.open("SPEC.rdoc", "wb", encoding: "UTF-8") do |file|
 82 |     IO.foreach("lib/rack/lint.rb", encoding: "UTF-8") do |line|
 83 |       if match = line_pattern.match(line)
 84 |         if match[:wrap]
 85 |           file.print match[:text]
 86 |         else
 87 |           file.puts match[:text]
 88 |         end
 89 |       end
 90 |     end
 91 |   end
 92 | end
 93 | 
 94 | Rake::TestTask.new("test:regular") do |t|
 95 |   t.libs << "test"
 96 |   t.test_files = FileList["test/**/*_test.rb", "test/**/spec_*.rb", "test/gemloader.rb"]
 97 |   t.warning = false
 98 |   t.verbose = true
 99 | end
100 | 
101 | desc "Run tests with coverage"
102 | task "test_cov" do
103 |   ENV['COVERAGE'] = '1'
104 |   Rake::Task['test:regular'].invoke
105 | end
106 | 
107 | desc "Run separate tests for each test file, to test directly requiring components"
108 | task "test:separate" do
109 |   fails = []
110 |   FileList["test/**/spec_*.rb"].each do |file|
111 |     puts "#{FileUtils::RUBY} -w #{file}"
112 |     fails << file unless system({'SEPARATE'=>'1'},  FileUtils::RUBY, '-w', file)
113 |   end
114 |   if fails.empty?
115 |     puts 'All test files passed'
116 |   else
117 |     puts "Failures in the following test files:"
118 |     puts fails
119 |     raise "At least one separate test failed"
120 |   end
121 | end
122 | 
123 | desc "Run all the fast + platform agnostic tests"
124 | task test: %w[spec test:regular test:separate]
125 | 
126 | desc "Run all the tests we run on CI"
127 | task ci: :test
128 | 
129 | task gem: :spec do
130 |   sh "gem build rack.gemspec"
131 | end
132 | 
133 | task doc: :rdoc
134 | 
135 | desc "Generate RDoc documentation"
136 | task rdoc: %w[changelog spec] do
137 |   sh(*%w{rdoc --line-numbers --main README.rdoc
138 |               --title 'Rack\ Documentation' --charset utf-8 -U -o doc} +
139 |               %w{README.rdoc KNOWN-ISSUES SPEC.rdoc ChangeLog} +
140 |               `git ls-files lib/\*\*/\*.rb`.strip.split)
141 |   cp "contrib/rdoc.css", "doc/rdoc.css"
142 | end
143 | 


--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
 1 | # Security Policy
 2 | 
 3 | ## Supported versions
 4 | 
 5 | The current release series and the next most recent one (by major-minor version) will receive patches and new versions in case of a security issue.
 6 | 
 7 | ### Unsupported Release Series
 8 | 
 9 | When a release series is no longer supported, it’s your own responsibility to deal with bugs and security issues. If you are not comfortable maintaining your own versions, you should upgrade to a supported version.
10 | 
11 | ## Reporting a security issue
12 | 
13 | Contact the current security coordinator [Aaron Patterson](mailto:tenderlove@ruby-lang.org) directly. If you do not get a response within 7 days, create an issue on the relevant project without any specific details and mention the project maintainers.
14 | 
15 | ## Disclosure Policy
16 | 
17 | 1. Security report received and is assigned a primary handler. This person will coordinate the fix and release process.
18 | 2. Problem is confirmed and, a list of all affected versions is determined. Code is audited to find any potential similar problems.
19 | 3. Fixes are prepared for all releases which are still supported. These fixes are not committed to the public repository but rather held locally pending the announcement.
20 | 4. A suggested embargo date for this vulnerability is chosen and distros@openwall is notified. This notification will include patches for all versions still under support and a contact address for packagers who need advice back-porting patches to older versions.
21 | 5. On the embargo date, the changes are pushed to the public repository and new gems released to rubygems.
22 | 
23 | This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it’s important that we follow the release process above to ensure that the disclosure is handled in a consistent manner.
24 | 


--------------------------------------------------------------------------------
/config/external.yaml:
--------------------------------------------------------------------------------
 1 | protocol-rack:
 2 |   url: https://github.com/socketry/protocol-rack
 3 |   command: bundle exec bake test
 4 | rails:
 5 |   url: https://github.com/rails/rails
 6 |   command: bash -c "cd actionpack && bundle exec rake test"
 7 | roda:
 8 |   url: https://github.com/jeremyevans/roda
 9 |   command: bundle exec rake spec spec_lint
10 |   gemfile: .ci.gemfile
11 | grape:
12 |   url: https://github.com/ruby-grape/grape
13 |   command: bundle exec rspec --exclude-pattern=spec/integration/**/*_spec.rb
14 | sinatra:
15 |   url: https://github.com/sinatra/sinatra
16 |   command: bundle exec rake test
17 |   # This causes some integration tests taht would otherwise fail, to be skipped:
18 |   env:
19 |     rack: head
20 | 


--------------------------------------------------------------------------------
/contrib/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Contributed Materials
2 | 
3 | ## Logo
4 | 
5 | Copyright, 2022, by Malene Laugesen.
6 | 
7 | This work is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/).
8 | 


--------------------------------------------------------------------------------
/contrib/logo-lossless.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/contrib/logo-lossless.webp


--------------------------------------------------------------------------------
/contrib/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/contrib/logo.webp


--------------------------------------------------------------------------------
/contrib/rdoc.css:
--------------------------------------------------------------------------------
1 | h1 img {
2 |   max-width: 100%;
3 | }
4 | 


--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | <head>
 4 |   <meta charset="UTF-8">
 5 |   <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6 |   <title>Rack Documentation</title>
 7 |   <style>
 8 |     body {
 9 |       display: flex;
10 |       flex-direction: column;
11 |       justify-content: center;
12 |       align-items: center;
13 |       min-height: 100vh;
14 |       font-family: sans-serif;
15 |       text-align: center;
16 |     }
17 | 
18 |     img {
19 |       max-width: 50%;
20 |       margin: 1rem;
21 |     }
22 | 
23 |     a {
24 |       text-decoration: none;
25 |       color: #cc0000;
26 |       font-size: 1.25rem;
27 |     }
28 | 
29 |     a:hover {
30 |       text-decoration: underline;
31 |     }
32 |   </style>
33 | </head>
34 | <body>
35 |   <img src="main/contrib/logo.webp" alt="Rack Logo">
36 |   <h1>Rack Documentation</h1>
37 | 
38 |   <h2>Released Versions</h2>
39 | 
40 |   <p><a href="3.1/index.html">3.1</a></p>
41 |   <p><a href="3.0/index.html">3.0</a></p>
42 |   <p><a href="2.2/index.html">2.2</a></p>
43 | 
44 |   <h2>Development Branch</h2>
45 | 
46 |   <p><a href="main/index.html">Main</a></p>
47 | </body>
48 | </html>
49 | 


--------------------------------------------------------------------------------
/lib/rack.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
 4 | #
 5 | # Rack is freely distributable under the terms of an MIT-style license.
 6 | # See MIT-LICENSE or https://opensource.org/licenses/MIT.
 7 | 
 8 | # The Rack main module, serving as a namespace for all core Rack
 9 | # modules and classes.
10 | #
11 | # All modules meant for use in your application are <tt>autoload</tt>ed here,
12 | # so it should be enough just to <tt>require 'rack'</tt> in your code.
13 | 
14 | require_relative 'rack/version'
15 | require_relative 'rack/constants'
16 | 
17 | module Rack
18 |   autoload :BadRequest, "rack/bad_request"
19 |   autoload :BodyProxy, "rack/body_proxy"
20 |   autoload :Builder, "rack/builder"
21 |   autoload :Cascade, "rack/cascade"
22 |   autoload :CommonLogger, "rack/common_logger"
23 |   autoload :ConditionalGet, "rack/conditional_get"
24 |   autoload :Config, "rack/config"
25 |   autoload :ContentLength, "rack/content_length"
26 |   autoload :ContentType, "rack/content_type"
27 |   autoload :Deflater, "rack/deflater"
28 |   autoload :Directory, "rack/directory"
29 |   autoload :ETag, "rack/etag"
30 |   autoload :Events, "rack/events"
31 |   autoload :Files, "rack/files"
32 |   autoload :ForwardRequest, "rack/recursive"
33 |   autoload :Head, "rack/head"
34 |   autoload :Headers, "rack/headers"
35 |   autoload :Lint, "rack/lint"
36 |   autoload :Lock, "rack/lock"
37 |   autoload :MediaType, "rack/media_type"
38 |   autoload :MethodOverride, "rack/method_override"
39 |   autoload :Mime, "rack/mime"
40 |   autoload :MockRequest, "rack/mock_request"
41 |   autoload :MockResponse, "rack/mock_response"
42 |   autoload :Multipart, "rack/multipart"
43 |   autoload :NullLogger, "rack/null_logger"
44 |   autoload :QueryParser, "rack/query_parser"
45 |   autoload :Recursive, "rack/recursive"
46 |   autoload :Reloader, "rack/reloader"
47 |   autoload :Request, "rack/request"
48 |   autoload :Response, "rack/response"
49 |   autoload :RewindableInput, "rack/rewindable_input"
50 |   autoload :Runtime, "rack/runtime"
51 |   autoload :Sendfile, "rack/sendfile"
52 |   autoload :ShowExceptions, "rack/show_exceptions"
53 |   autoload :ShowStatus, "rack/show_status"
54 |   autoload :Static, "rack/static"
55 |   autoload :TempfileReaper, "rack/tempfile_reaper"
56 |   autoload :URLMap, "rack/urlmap"
57 |   autoload :Utils, "rack/utils"
58 | 
59 |   module Auth
60 |     autoload :Basic, "rack/auth/basic"
61 |     autoload :AbstractHandler, "rack/auth/abstract/handler"
62 |     autoload :AbstractRequest, "rack/auth/abstract/request"
63 |   end
64 | end
65 | 


--------------------------------------------------------------------------------
/lib/rack/auth/abstract/handler.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative '../../constants'
 4 | 
 5 | module Rack
 6 |   module Auth
 7 |     # Rack::Auth::AbstractHandler implements common authentication functionality.
 8 |     #
 9 |     # +realm+ should be set for all handlers.
10 | 
11 |     class AbstractHandler
12 | 
13 |       attr_accessor :realm
14 | 
15 |       def initialize(app, realm = nil, &authenticator)
16 |         @app, @realm, @authenticator = app, realm, authenticator
17 |       end
18 | 
19 | 
20 |       private
21 | 
22 |       def unauthorized(www_authenticate = challenge)
23 |         return [ 401,
24 |           { CONTENT_TYPE => 'text/plain',
25 |             CONTENT_LENGTH => '0',
26 |             'www-authenticate' => www_authenticate.to_s },
27 |           []
28 |         ]
29 |       end
30 | 
31 |       def bad_request
32 |         return [ 400,
33 |           { CONTENT_TYPE => 'text/plain',
34 |             CONTENT_LENGTH => '0' },
35 |           []
36 |         ]
37 |       end
38 | 
39 |     end
40 |   end
41 | end
42 | 


--------------------------------------------------------------------------------
/lib/rack/auth/abstract/request.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # XXX: Remove when removing AbstractRequest#request
 4 | require_relative '../../request'
 5 | 
 6 | module Rack
 7 |   module Auth
 8 |     class AbstractRequest
 9 | 
10 |       def initialize(env)
11 |         @env = env
12 |       end
13 | 
14 |       def request
15 |         warn "Rack::Auth::AbstractRequest#request is deprecated and will be removed in a future version of rack.", uplevel: 1
16 |         @request ||= Request.new(@env)
17 |       end
18 | 
19 |       def provided?
20 |         !authorization_key.nil? && valid?
21 |       end
22 | 
23 |       def valid?
24 |         !@env[authorization_key].nil?
25 |       end
26 | 
27 |       def parts
28 |         @parts ||= @env[authorization_key].split(' ', 2)
29 |       end
30 | 
31 |       def scheme
32 |         @scheme ||= parts.first&.downcase
33 |       end
34 | 
35 |       def params
36 |         @params ||= parts.last
37 |       end
38 | 
39 | 
40 |       private
41 | 
42 |       AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
43 | 
44 |       def authorization_key
45 |         @authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) }
46 |       end
47 | 
48 |     end
49 | 
50 |   end
51 | end
52 | 


--------------------------------------------------------------------------------
/lib/rack/auth/basic.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'abstract/handler'
 4 | require_relative 'abstract/request'
 5 | 
 6 | module Rack
 7 |   module Auth
 8 |     # Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617.
 9 |     #
10 |     # Initialize with the Rack application that you want protecting,
11 |     # and a block that checks if a username and password pair are valid.
12 | 
13 |     class Basic < AbstractHandler
14 | 
15 |       def call(env)
16 |         auth = Basic::Request.new(env)
17 | 
18 |         return unauthorized unless auth.provided?
19 | 
20 |         return bad_request unless auth.basic?
21 | 
22 |         if valid?(auth)
23 |           env['REMOTE_USER'] = auth.username
24 | 
25 |           return @app.call(env)
26 |         end
27 | 
28 |         unauthorized
29 |       end
30 | 
31 | 
32 |       private
33 | 
34 |       def challenge
35 |         'Basic realm="%s"' % realm
36 |       end
37 | 
38 |       def valid?(auth)
39 |         @authenticator.call(*auth.credentials)
40 |       end
41 | 
42 |       class Request < Auth::AbstractRequest
43 |         def basic?
44 |           "basic" == scheme && credentials.length == 2
45 |         end
46 | 
47 |         def credentials
48 |           @credentials ||= params.unpack1('m').split(':', 2)
49 |         end
50 | 
51 |         def username
52 |           credentials.first
53 |         end
54 |       end
55 | 
56 |     end
57 |   end
58 | end
59 | 


--------------------------------------------------------------------------------
/lib/rack/bad_request.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | module Rack
4 |   # Represents a 400 Bad Request error when input data fails to meet the
5 |   # requirements.
6 |   module BadRequest
7 |   end
8 | end
9 | 


--------------------------------------------------------------------------------
/lib/rack/body_proxy.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Rack
 4 |   # Proxy for response bodies allowing calling a block when
 5 |   # the response body is closed (after the response has been fully
 6 |   # sent to the client).
 7 |   class BodyProxy
 8 |     # Set the response body to wrap, and the block to call when the
 9 |     # response has been fully sent.
10 |     def initialize(body, &block)
11 |       @body = body
12 |       @block = block
13 |       @closed = false
14 |     end
15 | 
16 |     # Return whether the wrapped body responds to the method.
17 |     def respond_to_missing?(method_name, include_all = false)
18 |       case method_name
19 |       when :to_str
20 |         false
21 |       else
22 |         super or @body.respond_to?(method_name, include_all)
23 |       end
24 |     end
25 | 
26 |     # If not already closed, close the wrapped body and
27 |     # then call the block the proxy was initialized with.
28 |     def close
29 |       return if @closed
30 |       @closed = true
31 |       begin
32 |         @body.close if @body.respond_to?(:close)
33 |       ensure
34 |         @block.call
35 |       end
36 |     end
37 | 
38 |     # Whether the proxy is closed.  The proxy starts as not closed,
39 |     # and becomes closed on the first call to close.
40 |     def closed?
41 |       @closed
42 |     end
43 | 
44 |     # Delegate missing methods to the wrapped body.
45 |     def method_missing(method_name, *args, &block)
46 |       case method_name
47 |       when :to_str
48 |         super
49 |       when :to_ary
50 |         begin
51 |           @body.__send__(method_name, *args, &block)
52 |         ensure
53 |           close
54 |         end
55 |       else
56 |         @body.__send__(method_name, *args, &block)
57 |       end
58 |     end
59 |     # :nocov:
60 |     ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
61 |     # :nocov:
62 |   end
63 | end
64 | 


--------------------------------------------------------------------------------
/lib/rack/cascade.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | 
 5 | module Rack
 6 |   # Rack::Cascade tries a request on several apps, and returns the
 7 |   # first response that is not 404 or 405 (or in a list of configured
 8 |   # status codes).  If all applications tried return one of the configured
 9 |   # status codes, return the last response.
10 | 
11 |   class Cascade
12 |     # An array of applications to try in order.
13 |     attr_reader :apps
14 | 
15 |     # Set the apps to send requests to, and what statuses result in
16 |     # cascading.  Arguments:
17 |     #
18 |     # apps: An enumerable of rack applications.
19 |     # cascade_for: The statuses to use cascading for.  If a response is received
20 |     #              from an app, the next app is tried.
21 |     def initialize(apps, cascade_for = [404, 405])
22 |       @apps = []
23 |       apps.each { |app| add app }
24 | 
25 |       @cascade_for = {}
26 |       [*cascade_for].each { |status| @cascade_for[status] = true }
27 |     end
28 | 
29 |     # Call each app in order.  If the responses uses a status that requires
30 |     # cascading, try the next app.  If all responses require cascading,
31 |     # return the response from the last app.
32 |     def call(env)
33 |       return [404, { CONTENT_TYPE => "text/plain" }, []] if @apps.empty?
34 |       result = nil
35 |       last_body = nil
36 | 
37 |       @apps.each do |app|
38 |         # The SPEC says that the body must be closed after it has been iterated
39 |         # by the server, or if it is replaced by a middleware action. Cascade
40 |         # replaces the body each time a cascade happens. It is assumed that nil
41 |         # does not respond to close, otherwise the previous application body
42 |         # will be closed. The final application body will not be closed, as it
43 |         # will be passed to the server as a result.
44 |         last_body.close if last_body.respond_to? :close
45 | 
46 |         result = app.call(env)
47 |         return result unless @cascade_for.include?(result[0].to_i)
48 |         last_body = result[2]
49 |       end
50 | 
51 |       result
52 |     end
53 | 
54 |     # Append an app to the list of apps to cascade.  This app will
55 |     # be tried last.
56 |     def add(app)
57 |       @apps << app
58 |     end
59 | 
60 |     # Whether the given app is one of the apps to cascade to.
61 |     def include?(app)
62 |       @apps.include?(app)
63 |     end
64 | 
65 |     alias_method :<<, :add
66 |   end
67 | end
68 | 


--------------------------------------------------------------------------------
/lib/rack/common_logger.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'utils'
 5 | require_relative 'body_proxy'
 6 | require_relative 'request'
 7 | 
 8 | module Rack
 9 |   # Rack::CommonLogger forwards every request to the given +app+, and
10 |   # logs a line in the
11 |   # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
12 |   # to the configured logger.
13 |   class CommonLogger
14 |     # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
15 |     #
16 |     #   lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
17 |     #
18 |     #   %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
19 |     #
20 |     # The actual format is slightly different than the above due to the
21 |     # separation of SCRIPT_NAME and PATH_INFO, and because the elapsed
22 |     # time in seconds is included at the end.
23 |     FORMAT = %{%s - %s [%s] "%s %s%s%s %s" %d %s %0.4f }
24 | 
25 |     # +logger+ can be any object that supports the +write+ or +<<+ methods,
26 |     # which includes the standard library Logger.  These methods are called
27 |     # with a single string argument, the log message.
28 |     # If +logger+ is nil, CommonLogger will fall back <tt>env['rack.errors']</tt>.
29 |     def initialize(app, logger = nil)
30 |       @app = app
31 |       @logger = logger
32 |     end
33 | 
34 |     # Log all requests in common_log format after a response has been
35 |     # returned.  Note that if the app raises an exception, the request
36 |     # will not be logged, so if exception handling middleware are used,
37 |     # they should be loaded after this middleware.  Additionally, because
38 |     # the logging happens after the request body has been fully sent, any
39 |     # exceptions raised during the sending of the response body will
40 |     # cause the request not to be logged.
41 |     def call(env)
42 |       began_at = Utils.clock_time
43 |       status, headers, body = response = @app.call(env)
44 | 
45 |       response[2] = BodyProxy.new(body) { log(env, status, headers, began_at) }
46 |       response
47 |     end
48 | 
49 |     private
50 | 
51 |     # Log the request to the configured logger.
52 |     def log(env, status, response_headers, began_at)
53 |       request = Rack::Request.new(env)
54 |       length = extract_content_length(response_headers)
55 | 
56 |       msg = sprintf(FORMAT,
57 |         request.ip || "-",
58 |         request.get_header("REMOTE_USER") || "-",
59 |         Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
60 |         request.request_method,
61 |         request.script_name,
62 |         request.path_info,
63 |         request.query_string.empty? ? "" : "?#{request.query_string}",
64 |         request.get_header(SERVER_PROTOCOL),
65 |         status.to_s[0..3],
66 |         length,
67 |         Utils.clock_time - began_at)
68 | 
69 |       msg.gsub!(/[^[:print:]]/) { |c| sprintf("\\x%x", c.ord) }
70 |       msg[-1] = "\n"
71 | 
72 |       logger = @logger || request.get_header(RACK_ERRORS)
73 |       # Standard library logger doesn't support write but it supports << which actually
74 |       # calls to write on the log device without formatting
75 |       if logger.respond_to?(:write)
76 |         logger.write(msg)
77 |       else
78 |         logger << msg
79 |       end
80 |     end
81 | 
82 |     # Attempt to determine the content length for the response to
83 |     # include it in the logged data.
84 |     def extract_content_length(headers)
85 |       value = headers[CONTENT_LENGTH]
86 |       !value || value.to_s == '0' ? '-' : value
87 |     end
88 |   end
89 | end
90 | 


--------------------------------------------------------------------------------
/lib/rack/conditional_get.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'utils'
 5 | require_relative 'body_proxy'
 6 | 
 7 | module Rack
 8 | 
 9 |   # Middleware that enables conditional GET using if-none-match and
10 |   # if-modified-since. The application should set either or both of the
11 |   # last-modified or etag response headers according to RFC 2616. When
12 |   # either of the conditions is met, the response body is set to be zero
13 |   # length and the response status is set to 304 Not Modified.
14 |   #
15 |   # Applications that defer response body generation until the body's each
16 |   # message is received will avoid response body generation completely when
17 |   # a conditional GET matches.
18 |   #
19 |   # Adapted from Michael Klishin's Merb implementation:
20 |   # https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
21 |   class ConditionalGet
22 |     def initialize(app)
23 |       @app = app
24 |     end
25 | 
26 |     # Return empty 304 response if the response has not been
27 |     # modified since the last request.
28 |     def call(env)
29 |       case env[REQUEST_METHOD]
30 |       when "GET", "HEAD"
31 |         status, headers, body = response = @app.call(env)
32 | 
33 |         if status == 200 && fresh?(env, headers)
34 |           response[0] = 304
35 |           headers.delete(CONTENT_TYPE)
36 |           headers.delete(CONTENT_LENGTH)
37 |           response[2] = Rack::BodyProxy.new([]) do
38 |             body.close if body.respond_to?(:close)
39 |           end
40 |         end
41 |         response
42 |       else
43 |         @app.call(env)
44 |       end
45 |     end
46 | 
47 |   private
48 | 
49 |     # Return whether the response has not been modified since the
50 |     # last request.
51 |     def fresh?(env, headers)
52 |       # if-none-match has priority over if-modified-since per RFC 7232
53 |       if none_match = env['HTTP_IF_NONE_MATCH']
54 |         etag_matches?(none_match, headers)
55 |       elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since))
56 |         modified_since?(modified_since, headers)
57 |       end
58 |     end
59 | 
60 |     # Whether the etag response header matches the if-none-match request header.
61 |     # If so, the request has not been modified.
62 |     def etag_matches?(none_match, headers)
63 |       headers[ETAG] == none_match
64 |     end
65 | 
66 |     # Whether the last-modified response header matches the if-modified-since
67 |     # request header.  If so, the request has not been modified.
68 |     def modified_since?(modified_since, headers)
69 |       last_modified = to_rfc2822(headers['last-modified']) and
70 |         modified_since >= last_modified
71 |     end
72 | 
73 |     # Return a Time object for the given string (which should be in RFC2822
74 |     # format), or nil if the string cannot be parsed.
75 |     def to_rfc2822(since)
76 |       # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
77 |       # anything shorter is invalid, this avoids exceptions for common cases
78 |       # most common being the empty string
79 |       if since && since.length >= 16
80 |         # NOTE: there is no trivial way to write this in a non exception way
81 |         #   _rfc2822 returns a hash but is not that usable
82 |         Time.rfc2822(since) rescue nil
83 |       end
84 |     end
85 |   end
86 | end
87 | 


--------------------------------------------------------------------------------
/lib/rack/config.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Rack
 4 |   # Rack::Config modifies the environment using the block given during
 5 |   # initialization.
 6 |   #
 7 |   # Example:
 8 |   #     use Rack::Config do |env|
 9 |   #       env['my-key'] = 'some-value'
10 |   #     end
11 |   class Config
12 |     def initialize(app, &block)
13 |       @app = app
14 |       @block = block
15 |     end
16 | 
17 |     def call(env)
18 |       @block.call(env)
19 |       @app.call(env)
20 |     end
21 |   end
22 | end
23 | 


--------------------------------------------------------------------------------
/lib/rack/constants.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Rack
 4 |   # Request env keys
 5 |   HTTP_HOST         = 'HTTP_HOST'
 6 |   HTTP_PORT         = 'HTTP_PORT'
 7 |   HTTPS             = 'HTTPS'
 8 |   PATH_INFO         = 'PATH_INFO'
 9 |   REQUEST_METHOD    = 'REQUEST_METHOD'
10 |   REQUEST_PATH      = 'REQUEST_PATH'
11 |   SCRIPT_NAME       = 'SCRIPT_NAME'
12 |   QUERY_STRING      = 'QUERY_STRING'
13 |   SERVER_PROTOCOL   = 'SERVER_PROTOCOL'
14 |   SERVER_NAME       = 'SERVER_NAME'
15 |   SERVER_PORT       = 'SERVER_PORT'
16 |   HTTP_COOKIE       = 'HTTP_COOKIE'
17 | 
18 |   # Response Header Keys
19 |   CACHE_CONTROL     = 'cache-control'
20 |   CONTENT_LENGTH    = 'content-length'
21 |   CONTENT_TYPE      = 'content-type'
22 |   ETAG              = 'etag'
23 |   EXPIRES           = 'expires'
24 |   SET_COOKIE        = 'set-cookie'
25 |   TRANSFER_ENCODING = 'transfer-encoding'
26 | 
27 |   # HTTP method verbs
28 |   GET     = 'GET'
29 |   POST    = 'POST'
30 |   PUT     = 'PUT'
31 |   PATCH   = 'PATCH'
32 |   DELETE  = 'DELETE'
33 |   HEAD    = 'HEAD'
34 |   OPTIONS = 'OPTIONS'
35 |   CONNECT = 'CONNECT'
36 |   LINK    = 'LINK'
37 |   UNLINK  = 'UNLINK'
38 |   TRACE   = 'TRACE'
39 | 
40 |   # Rack environment variables
41 |   RACK_VERSION                        = 'rack.version'
42 |   RACK_TEMPFILES                      = 'rack.tempfiles'
43 |   RACK_EARLY_HINTS                    = 'rack.early_hints'
44 |   RACK_ERRORS                         = 'rack.errors'
45 |   RACK_LOGGER                         = 'rack.logger'
46 |   RACK_INPUT                          = 'rack.input'
47 |   RACK_SESSION                        = 'rack.session'
48 |   RACK_SESSION_OPTIONS                = 'rack.session.options'
49 |   RACK_SHOWSTATUS_DETAIL              = 'rack.showstatus.detail'
50 |   RACK_URL_SCHEME                     = 'rack.url_scheme'
51 |   RACK_HIJACK                         = 'rack.hijack'
52 |   RACK_IS_HIJACK                      = 'rack.hijack?'
53 |   RACK_RECURSIVE_INCLUDE              = 'rack.recursive.include'
54 |   RACK_MULTIPART_BUFFER_SIZE          = 'rack.multipart.buffer_size'
55 |   RACK_MULTIPART_TEMPFILE_FACTORY     = 'rack.multipart.tempfile_factory'
56 |   RACK_RESPONSE_FINISHED              = 'rack.response_finished'
57 |   RACK_PROTOCOL                       = 'rack.protocol'
58 |   RACK_REQUEST_FORM_INPUT             = 'rack.request.form_input'
59 |   RACK_REQUEST_FORM_HASH              = 'rack.request.form_hash'
60 |   RACK_REQUEST_FORM_PAIRS             = 'rack.request.form_pairs'
61 |   RACK_REQUEST_FORM_VARS              = 'rack.request.form_vars'
62 |   RACK_REQUEST_FORM_ERROR             = 'rack.request.form_error'
63 |   RACK_REQUEST_COOKIE_HASH            = 'rack.request.cookie_hash'
64 |   RACK_REQUEST_COOKIE_STRING          = 'rack.request.cookie_string'
65 |   RACK_REQUEST_QUERY_HASH             = 'rack.request.query_hash'
66 |   RACK_REQUEST_QUERY_STRING           = 'rack.request.query_string'
67 |   RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
68 | end
69 | 


--------------------------------------------------------------------------------
/lib/rack/content_length.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'utils'
 5 | 
 6 | module Rack
 7 | 
 8 |   # Sets the content-length header on responses that do not specify
 9 |   # a content-length or transfer-encoding header.  Note that this
10 |   # does not fix responses that have an invalid content-length
11 |   # header specified.
12 |   class ContentLength
13 |     include Rack::Utils
14 | 
15 |     def initialize(app)
16 |       @app = app
17 |     end
18 | 
19 |     def call(env)
20 |       status, headers, body = response = @app.call(env)
21 | 
22 |       if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
23 |          !headers[CONTENT_LENGTH] &&
24 |          !headers[TRANSFER_ENCODING] &&
25 |          body.respond_to?(:to_ary)
26 | 
27 |         response[2] = body = body.to_ary
28 |         headers[CONTENT_LENGTH] = body.sum(&:bytesize).to_s
29 |       end
30 | 
31 |       response
32 |     end
33 |   end
34 | end
35 | 


--------------------------------------------------------------------------------
/lib/rack/content_type.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'utils'
 5 | 
 6 | module Rack
 7 | 
 8 |   # Sets the content-type header on responses which don't have one.
 9 |   #
10 |   # Builder Usage:
11 |   #   use Rack::ContentType, "text/plain"
12 |   #
13 |   # When no content type argument is provided, "text/html" is the
14 |   # default.
15 |   class ContentType
16 |     include Rack::Utils
17 | 
18 |     def initialize(app, content_type = "text/html")
19 |       @app = app
20 |       @content_type = content_type
21 |     end
22 | 
23 |     def call(env)
24 |       status, headers, _ = response = @app.call(env)
25 | 
26 |       unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i)
27 |         headers[CONTENT_TYPE] ||= @content_type
28 |       end
29 | 
30 |       response
31 |     end
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/lib/rack/deflater.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require "zlib"
  4 | require "time"  # for Time.httpdate
  5 | 
  6 | require_relative 'constants'
  7 | require_relative 'utils'
  8 | require_relative 'request'
  9 | require_relative 'body_proxy'
 10 | 
 11 | module Rack
 12 |   # This middleware enables content encoding of http responses,
 13 |   # usually for purposes of compression.
 14 |   #
 15 |   # Currently supported encodings:
 16 |   #
 17 |   # * gzip
 18 |   # * identity (no transformation)
 19 |   #
 20 |   # This middleware automatically detects when encoding is supported
 21 |   # and allowed. For example no encoding is made when a cache
 22 |   # directive of 'no-transform' is present, when the response status
 23 |   # code is one that doesn't allow an entity body, or when the body
 24 |   # is empty.
 25 |   #
 26 |   # Note that despite the name, Deflater does not support the +deflate+
 27 |   # encoding.
 28 |   class Deflater
 29 |     # Creates Rack::Deflater middleware. Options:
 30 |     #
 31 |     # :if :: a lambda enabling / disabling deflation based on returned boolean value
 32 |     #        (e.g <tt>use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }</tt>).
 33 |     #        However, be aware that calling `body.each` inside the block will break cases where `body.each` is not idempotent,
 34 |     #        such as when it is an +IO+ instance.
 35 |     # :include :: a list of content types that should be compressed. By default, all content types are compressed.
 36 |     # :sync :: determines if the stream is going to be flushed after every chunk.  Flushing after every chunk reduces
 37 |     #          latency for time-sensitive streaming applications, but hurts compression and throughput.
 38 |     #          Defaults to +true+.
 39 |     def initialize(app, options = {})
 40 |       @app = app
 41 |       @condition = options[:if]
 42 |       @compressible_types = options[:include]
 43 |       @sync = options.fetch(:sync, true)
 44 |     end
 45 | 
 46 |     def call(env)
 47 |       status, headers, body = response = @app.call(env)
 48 | 
 49 |       unless should_deflate?(env, status, headers, body)
 50 |         return response
 51 |       end
 52 | 
 53 |       request = Request.new(env)
 54 | 
 55 |       encoding = Utils.select_best_encoding(%w(gzip identity),
 56 |                                             request.accept_encoding)
 57 | 
 58 |       # Set the Vary HTTP header.
 59 |       vary = headers["vary"].to_s.split(",").map(&:strip)
 60 |       unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
 61 |         headers["vary"] = vary.push("Accept-Encoding").join(",")
 62 |       end
 63 | 
 64 |       case encoding
 65 |       when "gzip"
 66 |         headers['content-encoding'] = "gzip"
 67 |         headers.delete(CONTENT_LENGTH)
 68 |         mtime = headers["last-modified"]
 69 |         mtime = Time.httpdate(mtime).to_i if mtime
 70 |         response[2] = GzipStream.new(body, mtime, @sync)
 71 |         response
 72 |       when "identity"
 73 |         response
 74 |       else # when nil
 75 |         # Only possible encoding values here are 'gzip', 'identity', and nil
 76 |         message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
 77 |         bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
 78 |         [406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
 79 |       end
 80 |     end
 81 | 
 82 |     # Body class used for gzip encoded responses.
 83 |     class GzipStream
 84 | 
 85 |       BUFFER_LENGTH = 128 * 1_024
 86 | 
 87 |       # Initialize the gzip stream.  Arguments:
 88 |       # body :: Response body to compress with gzip
 89 |       # mtime :: The modification time of the body, used to set the
 90 |       #          modification time in the gzip header.
 91 |       # sync :: Whether to flush each gzip chunk as soon as it is ready.
 92 |       def initialize(body, mtime, sync)
 93 |         @body = body
 94 |         @mtime = mtime
 95 |         @sync = sync
 96 |       end
 97 | 
 98 |       # Yield gzip compressed strings to the given block.
 99 |       def each(&block)
100 |         @writer = block
101 |         gzip = ::Zlib::GzipWriter.new(self)
102 |         gzip.mtime = @mtime if @mtime
103 |         # @body.each is equivalent to @body.gets (slow)
104 |         if @body.is_a? ::File # XXX: Should probably be ::IO
105 |           while part = @body.read(BUFFER_LENGTH)
106 |             gzip.write(part)
107 |             gzip.flush if @sync
108 |           end
109 |         else
110 |           @body.each { |part|
111 |             # Skip empty strings, as they would result in no output,
112 |             # and flushing empty parts would raise Zlib::BufError.
113 |             next if part.empty?
114 |             gzip.write(part)
115 |             gzip.flush if @sync
116 |           }
117 |         end
118 |       ensure
119 |         gzip.finish
120 |       end
121 | 
122 |       # Call the block passed to #each with the gzipped data.
123 |       def write(data)
124 |         @writer.call(data)
125 |       end
126 | 
127 |       # Close the original body if possible.
128 |       def close
129 |         @body.close if @body.respond_to?(:close)
130 |       end
131 |     end
132 | 
133 |     private
134 | 
135 |     # Whether the body should be compressed.
136 |     def should_deflate?(env, status, headers, body)
137 |       # Skip compressing empty entity body responses and responses with
138 |       # no-transform set.
139 |       if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
140 |           /\bno-transform\b/.match?(headers[CACHE_CONTROL].to_s) ||
141 |           headers['content-encoding']&.!~(/\bidentity\b/)
142 |         return false
143 |       end
144 | 
145 |       # Skip if @compressible_types are given and does not include request's content type
146 |       return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
147 | 
148 |       # Skip if @condition lambda is given and evaluates to false
149 |       return false if @condition && !@condition.call(env, status, headers, body)
150 | 
151 |       # No point in compressing empty body, also handles usage with
152 |       # Rack::Sendfile.
153 |       return false if headers[CONTENT_LENGTH] == '0'
154 | 
155 |       true
156 |     end
157 |   end
158 | end
159 | 


--------------------------------------------------------------------------------
/lib/rack/etag.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'digest/sha2'
 4 | 
 5 | require_relative 'constants'
 6 | require_relative 'utils'
 7 | 
 8 | module Rack
 9 |   # Automatically sets the etag header on all String bodies.
10 |   #
11 |   # The etag header is skipped if etag or last-modified headers are sent or if
12 |   # a sendfile body (body.responds_to :to_path) is given (since such cases
13 |   # should be handled by apache/nginx).
14 |   #
15 |   # On initialization, you can pass two parameters: a cache-control directive
16 |   # used when etag is absent and a directive when it is present. The first
17 |   # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
18 |   class ETag
19 |     ETAG_STRING = Rack::ETAG
20 |     DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
21 | 
22 |     def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
23 |       @app = app
24 |       @cache_control = cache_control
25 |       @no_cache_control = no_cache_control
26 |     end
27 | 
28 |     def call(env)
29 |       status, headers, body = response = @app.call(env)
30 | 
31 |       if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
32 |         body = body.to_ary
33 |         digest = digest_body(body)
34 |         headers[ETAG_STRING] = %(W/"#{digest}") if digest
35 | 
36 |         # Body was modified, so we need to re-assign it:
37 |         response[2] = body
38 |       end
39 | 
40 |       unless headers[CACHE_CONTROL]
41 |         if digest
42 |           headers[CACHE_CONTROL] = @cache_control if @cache_control
43 |         else
44 |           headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
45 |         end
46 |       end
47 | 
48 |       response
49 |     end
50 | 
51 |     private
52 | 
53 |       def etag_status?(status)
54 |         status == 200 || status == 201
55 |       end
56 | 
57 |       def skip_caching?(headers)
58 |         headers.key?(ETAG_STRING) || headers.key?('last-modified')
59 |       end
60 | 
61 |       def digest_body(body)
62 |         digest = nil
63 | 
64 |         body.each do |part|
65 |           (digest ||= Digest::SHA256.new) << part unless part.empty?
66 |         end
67 | 
68 |         digest && digest.hexdigest.byteslice(0,32)
69 |       end
70 |   end
71 | end
72 | 


--------------------------------------------------------------------------------
/lib/rack/events.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'body_proxy'
  4 | require_relative 'request'
  5 | require_relative 'response'
  6 | 
  7 | module Rack
  8 |   ### This middleware provides hooks to certain places in the request /
  9 |   # response lifecycle.  This is so that middleware that don't need to filter
 10 |   # the response data can safely leave it alone and not have to send messages
 11 |   # down the traditional "rack stack".
 12 |   #
 13 |   # The events are:
 14 |   #
 15 |   # * on_start(request, response)
 16 |   #
 17 |   #   This event is sent at the start of the request, before the next
 18 |   #   middleware in the chain is called.  This method is called with a request
 19 |   #   object, and a response object.  Right now, the response object is always
 20 |   #   nil, but in the future it may actually be a real response object.
 21 |   #
 22 |   # * on_commit(request, response)
 23 |   #
 24 |   #   The response has been committed.  The application has returned, but the
 25 |   #   response has not been sent to the webserver yet.  This method is always
 26 |   #   called with a request object and the response object.  The response
 27 |   #   object is constructed from the rack triple that the application returned.
 28 |   #   Changes may still be made to the response object at this point.
 29 |   #
 30 |   # * on_send(request, response)
 31 |   #
 32 |   #   The webserver has started iterating over the response body and presumably
 33 |   #   has started sending data over the wire. This method is always called with
 34 |   #   a request object and the response object.  The response object is
 35 |   #   constructed from the rack triple that the application returned.  Changes
 36 |   #   SHOULD NOT be made to the response object as the webserver has already
 37 |   #   started sending data.  Any mutations will likely result in an exception.
 38 |   #
 39 |   # * on_finish(request, response)
 40 |   #
 41 |   #   The webserver has closed the response, and all data has been written to
 42 |   #   the response socket.  The request and response object should both be
 43 |   #   read-only at this point.  The body MAY NOT be available on the response
 44 |   #   object as it may have been flushed to the socket.
 45 |   #
 46 |   # * on_error(request, response, error)
 47 |   #
 48 |   #   An exception has occurred in the application or an `on_commit` event.
 49 |   #   This method will get the request, the response (if available) and the
 50 |   #   exception that was raised.
 51 |   #
 52 |   # ## Order
 53 |   #
 54 |   # `on_start` is called on the handlers in the order that they were passed to
 55 |   # the constructor.  `on_commit`, on_send`, `on_finish`, and `on_error` are
 56 |   # called in the reverse order.  `on_finish` handlers are called inside an
 57 |   # `ensure` block, so they are guaranteed to be called even if something
 58 |   # raises an exception.  If something raises an exception in a `on_finish`
 59 |   # method, then nothing is guaranteed.
 60 | 
 61 |   class Events
 62 |     module Abstract
 63 |       def on_start(req, res)
 64 |       end
 65 | 
 66 |       def on_commit(req, res)
 67 |       end
 68 | 
 69 |       def on_send(req, res)
 70 |       end
 71 | 
 72 |       def on_finish(req, res)
 73 |       end
 74 | 
 75 |       def on_error(req, res, e)
 76 |       end
 77 |     end
 78 | 
 79 |     class EventedBodyProxy < Rack::BodyProxy # :nodoc:
 80 |       attr_reader :request, :response
 81 | 
 82 |       def initialize(body, request, response, handlers, &block)
 83 |         super(body, &block)
 84 |         @request  = request
 85 |         @response = response
 86 |         @handlers = handlers
 87 |       end
 88 | 
 89 |       def each
 90 |         @handlers.reverse_each { |handler| handler.on_send request, response }
 91 |         super
 92 |       end
 93 |     end
 94 | 
 95 |     class BufferedResponse < Rack::Response::Raw # :nodoc:
 96 |       attr_reader :body
 97 | 
 98 |       def initialize(status, headers, body)
 99 |         super(status, headers)
100 |         @body = body
101 |       end
102 | 
103 |       def to_a; [status, headers, body]; end
104 |     end
105 | 
106 |     def initialize(app, handlers)
107 |       @app      = app
108 |       @handlers = handlers
109 |     end
110 | 
111 |     def call(env)
112 |       request = make_request env
113 |       on_start request, nil
114 | 
115 |       begin
116 |         status, headers, body = @app.call request.env
117 |         response = make_response status, headers, body
118 |         on_commit request, response
119 |       rescue StandardError => e
120 |         on_error request, response, e
121 |         on_finish request, response
122 |         raise
123 |       end
124 | 
125 |       body = EventedBodyProxy.new(body, request, response, @handlers) do
126 |         on_finish request, response
127 |       end
128 |       [response.status, response.headers, body]
129 |     end
130 | 
131 |     private
132 | 
133 |     def on_error(request, response, e)
134 |       @handlers.reverse_each { |handler| handler.on_error request, response, e }
135 |     end
136 | 
137 |     def on_commit(request, response)
138 |       @handlers.reverse_each { |handler| handler.on_commit request, response }
139 |     end
140 | 
141 |     def on_start(request, response)
142 |       @handlers.each { |handler| handler.on_start request, nil }
143 |     end
144 | 
145 |     def on_finish(request, response)
146 |       @handlers.reverse_each { |handler| handler.on_finish request, response }
147 |     end
148 | 
149 |     def make_request(env)
150 |       Rack::Request.new env
151 |     end
152 | 
153 |     def make_response(status, headers, body)
154 |       BufferedResponse.new status, headers, body
155 |     end
156 |   end
157 | end
158 | 


--------------------------------------------------------------------------------
/lib/rack/head.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'body_proxy'
 5 | 
 6 | module Rack
 7 |   # Rack::Head returns an empty body for all HEAD requests. It leaves
 8 |   # all other requests unchanged.
 9 |   class Head
10 |     def initialize(app)
11 |       @app = app
12 |     end
13 | 
14 |     def call(env)
15 |       _, _, body = response = @app.call(env)
16 | 
17 |       if env[REQUEST_METHOD] == HEAD
18 |         response[2] = Rack::BodyProxy.new([]) do
19 |           body.close if body.respond_to? :close
20 |         end
21 |       end
22 | 
23 |       response
24 |     end
25 |   end
26 | end
27 | 


--------------------------------------------------------------------------------
/lib/rack/headers.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | module Rack
  4 |   # Rack::Headers is a Hash subclass that downcases all keys.  It's designed
  5 |   # to be used by rack applications that don't implement the Rack 3 SPEC
  6 |   # (by using non-lowercase response header keys), automatically handling
  7 |   # the downcasing of keys.
  8 |   class Headers < Hash
  9 |     KNOWN_HEADERS = {}
 10 |     %w(
 11 |       Accept-CH
 12 |       Accept-Patch
 13 |       Accept-Ranges
 14 |       Access-Control-Allow-Credentials
 15 |       Access-Control-Allow-Headers
 16 |       Access-Control-Allow-Methods
 17 |       Access-Control-Allow-Origin
 18 |       Access-Control-Expose-Headers
 19 |       Access-Control-Max-Age
 20 |       Age
 21 |       Allow
 22 |       Alt-Svc
 23 |       Cache-Control
 24 |       Connection
 25 |       Content-Disposition
 26 |       Content-Encoding
 27 |       Content-Language
 28 |       Content-Length
 29 |       Content-Location
 30 |       Content-MD5
 31 |       Content-Range
 32 |       Content-Security-Policy
 33 |       Content-Security-Policy-Report-Only
 34 |       Content-Type
 35 |       Date
 36 |       Delta-Base
 37 |       ETag
 38 |       Expect-CT
 39 |       Expires
 40 |       Feature-Policy
 41 |       IM
 42 |       Last-Modified
 43 |       Link
 44 |       Location
 45 |       NEL
 46 |       P3P
 47 |       Permissions-Policy
 48 |       Pragma
 49 |       Preference-Applied
 50 |       Proxy-Authenticate
 51 |       Public-Key-Pins
 52 |       Referrer-Policy
 53 |       Refresh
 54 |       Report-To
 55 |       Retry-After
 56 |       Server
 57 |       Set-Cookie
 58 |       Status
 59 |       Strict-Transport-Security
 60 |       Timing-Allow-Origin
 61 |       Tk
 62 |       Trailer
 63 |       Transfer-Encoding
 64 |       Upgrade
 65 |       Vary
 66 |       Via
 67 |       WWW-Authenticate
 68 |       Warning
 69 |       X-Cascade
 70 |       X-Content-Duration
 71 |       X-Content-Security-Policy
 72 |       X-Content-Type-Options
 73 |       X-Correlation-ID
 74 |       X-Correlation-Id
 75 |       X-Download-Options
 76 |       X-Frame-Options
 77 |       X-Permitted-Cross-Domain-Policies
 78 |       X-Powered-By
 79 |       X-Redirect-By
 80 |       X-Request-ID
 81 |       X-Request-Id
 82 |       X-Runtime
 83 |       X-UA-Compatible
 84 |       X-WebKit-CS
 85 |       X-XSS-Protection
 86 |     ).each do |str|
 87 |       downcased = str.downcase.freeze
 88 |       KNOWN_HEADERS[str] = KNOWN_HEADERS[downcased] = downcased
 89 |     end
 90 | 
 91 |     def self.[](*items)
 92 |       if items.length % 2 != 0
 93 |         if items.length == 1 && items.first.is_a?(Hash)
 94 |           new.merge!(items.first)
 95 |         else
 96 |           raise ArgumentError, "odd number of arguments for Rack::Headers"
 97 |         end
 98 |       else
 99 |         hash = new
100 |         loop do
101 |           break if items.length == 0
102 |           key = items.shift
103 |           value = items.shift
104 |           hash[key] = value
105 |         end
106 |         hash
107 |       end
108 |     end
109 | 
110 |     def [](key)
111 |       super(downcase_key(key))
112 |     end
113 | 
114 |     def []=(key, value)
115 |       super(KNOWN_HEADERS[key] || key.downcase.freeze, value)
116 |     end
117 |     alias store []=
118 | 
119 |     def assoc(key)
120 |       super(downcase_key(key))
121 |     end
122 | 
123 |     def compare_by_identity
124 |       raise TypeError, "Rack::Headers cannot compare by identity, use regular Hash"
125 |     end
126 | 
127 |     def delete(key)
128 |       super(downcase_key(key))
129 |     end
130 | 
131 |     def dig(key, *a)
132 |       super(downcase_key(key), *a)
133 |     end
134 | 
135 |     def fetch(key, *default, &block)
136 |       key = downcase_key(key)
137 |       super
138 |     end
139 | 
140 |     def fetch_values(*a)
141 |       super(*a.map!{|key| downcase_key(key)})
142 |     end
143 | 
144 |     def has_key?(key)
145 |       super(downcase_key(key))
146 |     end
147 |     alias include? has_key?
148 |     alias key? has_key?
149 |     alias member? has_key?
150 | 
151 |     def invert
152 |       hash = self.class.new
153 |       each{|key, value| hash[value] = key}
154 |       hash
155 |     end
156 | 
157 |     def merge(hash, &block)
158 |       dup.merge!(hash, &block)
159 |     end
160 | 
161 |     def reject(&block)
162 |       hash = dup
163 |       hash.reject!(&block)
164 |       hash
165 |     end
166 | 
167 |     def replace(hash)
168 |       clear
169 |       update(hash)
170 |     end
171 | 
172 |     def select(&block)
173 |       hash = dup
174 |       hash.select!(&block)
175 |       hash
176 |     end
177 | 
178 |     def to_proc
179 |       lambda{|x| self[x]}
180 |     end
181 | 
182 |     def transform_values(&block)
183 |       dup.transform_values!(&block)
184 |     end
185 | 
186 |     def update(hash, &block)
187 |       hash.each do |key, value|
188 |         self[key] = if block_given? && include?(key)
189 |           block.call(key, self[key], value)
190 |         else
191 |           value
192 |         end
193 |       end
194 |       self
195 |     end
196 |     alias merge! update
197 | 
198 |     def values_at(*keys)
199 |       keys.map{|key| self[key]}
200 |     end
201 | 
202 |     # :nocov:
203 |     if RUBY_VERSION >= '2.5'
204 |     # :nocov:
205 |       def slice(*a)
206 |         h = self.class.new
207 |         a.each{|k| h[k] = self[k] if has_key?(k)}
208 |         h
209 |       end
210 | 
211 |       def transform_keys(&block)
212 |         dup.transform_keys!(&block)
213 |       end
214 | 
215 |       def transform_keys!
216 |         hash = self.class.new
217 |         each do |k, v|
218 |           hash[yield k] = v
219 |         end
220 |         replace(hash)
221 |       end
222 |     end
223 | 
224 |     # :nocov:
225 |     if RUBY_VERSION >= '3.0'
226 |     # :nocov:
227 |       def except(*a)
228 |         super(*a.map!{|key| downcase_key(key)})
229 |       end
230 |     end
231 | 
232 |     private
233 | 
234 |     def downcase_key(key)
235 |       key.is_a?(String) ? KNOWN_HEADERS[key] || key.downcase : key
236 |     end
237 |   end
238 | end
239 | 


--------------------------------------------------------------------------------
/lib/rack/lock.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'body_proxy'
 4 | 
 5 | module Rack
 6 |   # Rack::Lock locks every request inside a mutex, so that every request
 7 |   # will effectively be executed synchronously.
 8 |   class Lock
 9 |     def initialize(app, mutex = Mutex.new)
10 |       @app, @mutex = app, mutex
11 |     end
12 | 
13 |     def call(env)
14 |       @mutex.lock
15 |       begin
16 |         response = @app.call(env)
17 |         returned = response << BodyProxy.new(response.pop) { unlock }
18 |       ensure
19 |         unlock unless returned
20 |       end
21 |     end
22 | 
23 |     private
24 | 
25 |     def unlock
26 |       @mutex.unlock
27 |     end
28 |   end
29 | end
30 | 


--------------------------------------------------------------------------------
/lib/rack/media_type.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Rack
 4 |   # Rack::MediaType parse media type and parameters out of content_type string
 5 | 
 6 |   class MediaType
 7 |     SPLIT_PATTERN = /[;,]/
 8 | 
 9 |     class << self
10 |       # The media type (type/subtype) portion of the CONTENT_TYPE header
11 |       # without any media type parameters. e.g., when CONTENT_TYPE is
12 |       # "text/plain;charset=utf-8", the media-type is "text/plain".
13 |       #
14 |       # For more information on the use of media types in HTTP, see:
15 |       # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
16 |       def type(content_type)
17 |         return nil unless content_type && !content_type.empty?
18 |         type = content_type.split(SPLIT_PATTERN, 2).first
19 |         type.rstrip!
20 |         type.downcase!
21 |         type
22 |       end
23 | 
24 |       # The media type parameters provided in CONTENT_TYPE as a Hash, or
25 |       # an empty Hash if no CONTENT_TYPE or media-type parameters were
26 |       # provided.  e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
27 |       # this method responds with the following Hash:
28 |       #   { 'charset' => 'utf-8' }
29 |       #
30 |       # This will pass back parameters with empty strings in the hash if they
31 |       # lack a value (e.g., "text/plain;charset=" will return { 'charset' => '' },
32 |       # and "text/plain;charset" will return { 'charset' => '' }, similarly to 
33 |       # the query params parser (barring the latter case, which returns nil instead)).
34 |       def params(content_type)
35 |         return {} if content_type.nil? || content_type.empty?
36 | 
37 |         content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
38 |           s.strip!
39 |           k, v = s.split('=', 2)
40 |           k.downcase!
41 |           hsh[k] = strip_doublequotes(v)
42 |         end
43 |       end
44 | 
45 |       private
46 | 
47 |       def strip_doublequotes(str)
48 |         (str && str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str || ''
49 |       end
50 |     end
51 |   end
52 | end
53 | 


--------------------------------------------------------------------------------
/lib/rack/method_override.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'request'
 5 | require_relative 'utils'
 6 | 
 7 | module Rack
 8 |   class MethodOverride
 9 |     HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
10 | 
11 |     METHOD_OVERRIDE_PARAM_KEY = "_method"
12 |     HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
13 |     ALLOWED_METHODS = %w[POST]
14 | 
15 |     def initialize(app)
16 |       @app = app
17 |     end
18 | 
19 |     def call(env)
20 |       if allowed_methods.include?(env[REQUEST_METHOD])
21 |         method = method_override(env)
22 |         if HTTP_METHODS.include?(method)
23 |           env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
24 |           env[REQUEST_METHOD] = method
25 |         end
26 |       end
27 | 
28 |       @app.call(env)
29 |     end
30 | 
31 |     def method_override(env)
32 |       req = Request.new(env)
33 |       method = method_override_param(req) ||
34 |         env[HTTP_METHOD_OVERRIDE_HEADER]
35 |       begin
36 |         method.to_s.upcase
37 |       rescue ArgumentError
38 |         env[RACK_ERRORS].puts "Invalid string for method"
39 |       end
40 |     end
41 | 
42 |     private
43 | 
44 |     def allowed_methods
45 |       ALLOWED_METHODS
46 |     end
47 | 
48 |     def method_override_param(req)
49 |       req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
50 |     rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
51 |       req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
52 |     rescue EOFError
53 |       req.get_header(RACK_ERRORS).puts "Bad request content body"
54 |     end
55 |   end
56 | end
57 | 


--------------------------------------------------------------------------------
/lib/rack/mock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | require_relative 'mock_request'
4 | 


--------------------------------------------------------------------------------
/lib/rack/mock_request.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require 'uri'
  4 | require 'stringio'
  5 | 
  6 | require_relative 'constants'
  7 | require_relative 'mock_response'
  8 | 
  9 | module Rack
 10 |   # Rack::MockRequest helps testing your Rack application without
 11 |   # actually using HTTP.
 12 |   #
 13 |   # After performing a request on a URL with get/post/put/patch/delete, it
 14 |   # returns a MockResponse with useful helper methods for effective
 15 |   # testing.
 16 |   #
 17 |   # You can pass a hash with additional configuration to the
 18 |   # get/post/put/patch/delete.
 19 |   # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
 20 |   # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
 21 |   # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
 22 | 
 23 |   class MockRequest
 24 |     class FatalWarning < RuntimeError
 25 |     end
 26 | 
 27 |     class FatalWarner
 28 |       def puts(warning)
 29 |         raise FatalWarning, warning
 30 |       end
 31 | 
 32 |       def write(warning)
 33 |         raise FatalWarning, warning
 34 |       end
 35 | 
 36 |       def flush
 37 |       end
 38 | 
 39 |       def string
 40 |         ""
 41 |       end
 42 |     end
 43 | 
 44 |     def initialize(app)
 45 |       @app = app
 46 |     end
 47 | 
 48 |     # Make a GET request and return a MockResponse. See #request.
 49 |     def get(uri, opts = {})     request(GET, uri, opts)     end
 50 |     # Make a POST request and return a MockResponse. See #request.
 51 |     def post(uri, opts = {})    request(POST, uri, opts)    end
 52 |     # Make a PUT request and return a MockResponse. See #request.
 53 |     def put(uri, opts = {})     request(PUT, uri, opts)     end
 54 |     # Make a PATCH request and return a MockResponse. See #request.
 55 |     def patch(uri, opts = {})   request(PATCH, uri, opts)   end
 56 |     # Make a DELETE request and return a MockResponse. See #request.
 57 |     def delete(uri, opts = {})  request(DELETE, uri, opts)  end
 58 |     # Make a HEAD request and return a MockResponse. See #request.
 59 |     def head(uri, opts = {})    request(HEAD, uri, opts)    end
 60 |     # Make an OPTIONS request and return a MockResponse. See #request.
 61 |     def options(uri, opts = {}) request(OPTIONS, uri, opts) end
 62 | 
 63 |     # Make a request using the given request method for the given
 64 |     # uri to the rack application and return a MockResponse.
 65 |     # Options given are passed to MockRequest.env_for.
 66 |     def request(method = GET, uri = "", opts = {})
 67 |       env = self.class.env_for(uri, opts.merge(method: method))
 68 | 
 69 |       if opts[:lint]
 70 |         app = Rack::Lint.new(@app)
 71 |       else
 72 |         app = @app
 73 |       end
 74 | 
 75 |       errors = env[RACK_ERRORS]
 76 |       status, headers, body = app.call(env)
 77 |       MockResponse.new(status, headers, body, errors)
 78 |     ensure
 79 |       body.close if body.respond_to?(:close)
 80 |     end
 81 | 
 82 |     # For historical reasons, we're pinning to RFC 2396.
 83 |     # URI::Parser = URI::RFC2396_Parser
 84 |     def self.parse_uri_rfc2396(uri)
 85 |       @parser ||= URI::Parser.new
 86 |       @parser.parse(uri)
 87 |     end
 88 | 
 89 |     # Return the Rack environment used for a request to +uri+.
 90 |     # All options that are strings are added to the returned environment.
 91 |     # Options:
 92 |     # :fatal :: Whether to raise an exception if request outputs to rack.errors
 93 |     # :input :: The rack.input to set
 94 |     # :http_version :: The SERVER_PROTOCOL to set
 95 |     # :method :: The HTTP request method to use
 96 |     # :params :: The params to use
 97 |     # :script_name :: The SCRIPT_NAME to set
 98 |     def self.env_for(uri = "", opts = {})
 99 |       uri = parse_uri_rfc2396(uri)
100 |       uri.path = "/#{uri.path}" unless uri.path[0] == ?/
101 | 
102 |       env = {}
103 | 
104 |       env[REQUEST_METHOD]  = (opts[:method] ? opts[:method].to_s.upcase : GET).b
105 |       env[SERVER_NAME]     = (uri.host || "example.org").b
106 |       env[SERVER_PORT]     = (uri.port ? uri.port.to_s : "80").b
107 |       env[SERVER_PROTOCOL] = opts[:http_version] || 'HTTP/1.1'
108 |       env[QUERY_STRING]    = (uri.query.to_s).b
109 |       env[PATH_INFO]       = (uri.path).b
110 |       env[RACK_URL_SCHEME] = (uri.scheme || "http").b
111 |       env[HTTPS]           = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
112 | 
113 |       env[SCRIPT_NAME] = opts[:script_name] || ""
114 | 
115 |       if opts[:fatal]
116 |         env[RACK_ERRORS] = FatalWarner.new
117 |       else
118 |         env[RACK_ERRORS] = StringIO.new
119 |       end
120 | 
121 |       if params = opts[:params]
122 |         if env[REQUEST_METHOD] == GET
123 |           params = Utils.parse_nested_query(params) if params.is_a?(String)
124 |           params.update(Utils.parse_nested_query(env[QUERY_STRING]))
125 |           env[QUERY_STRING] = Utils.build_nested_query(params)
126 |         elsif !opts.has_key?(:input)
127 |           opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
128 |           if params.is_a?(Hash)
129 |             if data = Rack::Multipart.build_multipart(params)
130 |               opts[:input] = data
131 |               opts["CONTENT_LENGTH"] ||= data.length.to_s
132 |               opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
133 |             else
134 |               opts[:input] = Utils.build_nested_query(params)
135 |             end
136 |           else
137 |             opts[:input] = params
138 |           end
139 |         end
140 |       end
141 | 
142 |       rack_input = opts[:input]
143 |       if String === rack_input
144 |         rack_input = StringIO.new(rack_input)
145 |       end
146 | 
147 |       if rack_input
148 |         rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
149 |         env[RACK_INPUT] = rack_input
150 | 
151 |         env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
152 |       end
153 | 
154 |       opts.each { |field, value|
155 |         env[field] = value if String === field
156 |       }
157 | 
158 |       env
159 |     end
160 |   end
161 | end
162 | 


--------------------------------------------------------------------------------
/lib/rack/mock_response.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require 'time'
  4 | 
  5 | require_relative 'response'
  6 | 
  7 | module Rack
  8 |   # Rack::MockResponse provides useful helpers for testing your apps.
  9 |   # Usually, you don't create the MockResponse on your own, but use
 10 |   # MockRequest.
 11 | 
 12 |   class MockResponse < Rack::Response
 13 |     class Cookie
 14 |       attr_reader :name, :value, :path, :domain, :expires, :secure
 15 | 
 16 |       def initialize(args)
 17 |         @name = args["name"]
 18 |         @value = args["value"]
 19 |         @path = args["path"]
 20 |         @domain = args["domain"]
 21 |         @expires = args["expires"]
 22 |         @secure = args["secure"]
 23 |       end
 24 | 
 25 |       def method_missing(method_name, *args, &block)
 26 |         @value.send(method_name, *args, &block)
 27 |       end
 28 |       # :nocov:
 29 |       ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
 30 |       # :nocov:
 31 | 
 32 |       def respond_to_missing?(method_name, include_all = false)
 33 |         @value.respond_to?(method_name, include_all) || super
 34 |       end
 35 |     end
 36 | 
 37 |     class << self
 38 |       alias [] new
 39 |     end
 40 | 
 41 |     # Headers
 42 |     attr_reader :original_headers, :cookies
 43 | 
 44 |     # Errors
 45 |     attr_accessor :errors
 46 | 
 47 |     def initialize(status, headers, body, errors = nil)
 48 |       @original_headers = headers
 49 | 
 50 |       if errors
 51 |         @errors = errors.string if errors.respond_to?(:string)
 52 |       else
 53 |         @errors = ""
 54 |       end
 55 | 
 56 |       super(body, status, headers)
 57 | 
 58 |       @cookies = parse_cookies_from_header
 59 |       buffered_body!
 60 |     end
 61 | 
 62 |     def =~(other)
 63 |       body =~ other
 64 |     end
 65 | 
 66 |     def match(other)
 67 |       body.match other
 68 |     end
 69 | 
 70 |     def body
 71 |       return @buffered_body if defined?(@buffered_body)
 72 | 
 73 |       # FIXME: apparently users of MockResponse expect the return value of
 74 |       # MockResponse#body to be a string.  However, the real response object
 75 |       # returns the body as a list.
 76 |       #
 77 |       # See spec_showstatus.rb:
 78 |       #
 79 |       #   should "not replace existing messages" do
 80 |       #     ...
 81 |       #     res.body.should == "foo!"
 82 |       #   end
 83 |       buffer = @buffered_body = String.new
 84 | 
 85 |       @body.each do |chunk|
 86 |         buffer << chunk
 87 |       end
 88 | 
 89 |       return buffer
 90 |     end
 91 | 
 92 |     def empty?
 93 |       [201, 204, 304].include? status
 94 |     end
 95 | 
 96 |     def cookie(name)
 97 |       cookies.fetch(name, nil)
 98 |     end
 99 | 
100 |     private
101 | 
102 |     def parse_cookies_from_header
103 |       cookies = Hash.new
104 |       set_cookie_header = headers['set-cookie']
105 |       if set_cookie_header && !set_cookie_header.empty?
106 |         Array(set_cookie_header).each do |cookie|
107 |           cookie_name, cookie_filling = cookie.split('=', 2)
108 |           cookie_attributes = identify_cookie_attributes cookie_filling
109 |           parsed_cookie = Cookie.new(
110 |             'name' => cookie_name.strip,
111 |             'value' => cookie_attributes.fetch('value'),
112 |             'path' => cookie_attributes.fetch('path', nil),
113 |             'domain' => cookie_attributes.fetch('domain', nil),
114 |             'expires' => cookie_attributes.fetch('expires', nil),
115 |             'secure' => cookie_attributes.fetch('secure', false)
116 |           )
117 |           cookies.store(cookie_name, parsed_cookie)
118 |         end
119 |       end
120 |       cookies
121 |     end
122 | 
123 |     def identify_cookie_attributes(cookie_filling)
124 |       cookie_bits = cookie_filling.split(';')
125 |       cookie_attributes = Hash.new
126 |       cookie_attributes.store('value', Array(cookie_bits[0].strip))
127 |       cookie_bits.drop(1).each do |bit|
128 |         if bit.include? '='
129 |           cookie_attribute, attribute_value = bit.split('=', 2)
130 |           cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
131 |         end
132 |         if bit.include? 'secure'
133 |           cookie_attributes.store('secure', true)
134 |         end
135 |       end
136 | 
137 |       if cookie_attributes.key? 'max-age'
138 |         cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
139 |       elsif cookie_attributes.key? 'expires'
140 |         cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
141 |       end
142 | 
143 |       cookie_attributes
144 |     end
145 | 
146 |   end
147 | end
148 | 


--------------------------------------------------------------------------------
/lib/rack/multipart.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'utils'
 5 | 
 6 | require_relative 'multipart/parser'
 7 | require_relative 'multipart/generator'
 8 | 
 9 | require_relative 'bad_request'
10 | 
11 | module Rack
12 |   # A multipart form data parser, adapted from IOWA.
13 |   #
14 |   # Usually, Rack::Request#POST takes care of calling this.
15 |   module Multipart
16 |     MULTIPART_BOUNDARY = "AaB03x"
17 | 
18 |     class MissingInputError < StandardError
19 |       include BadRequest
20 |     end
21 | 
22 |     # Accumulator for multipart form data, conforming to the QueryParser API.
23 |     # In future, the Parser could return the pair list directly, but that would
24 |     # change its API.
25 |     class ParamList # :nodoc:
26 |       def self.make_params
27 |         new
28 |       end
29 | 
30 |       def self.normalize_params(params, key, value)
31 |         params << [key, value]
32 |       end
33 | 
34 |       def initialize
35 |         @pairs = []
36 |       end
37 | 
38 |       def <<(pair)
39 |         @pairs << pair
40 |       end
41 | 
42 |       def to_params_hash
43 |         @pairs
44 |       end
45 |     end
46 | 
47 |     class << self
48 |       def parse_multipart(env, params = Rack::Utils.default_query_parser)
49 |         unless io = env[RACK_INPUT]
50 |           raise MissingInputError, "Missing input stream!"
51 |         end
52 | 
53 |         if content_length = env['CONTENT_LENGTH']
54 |           content_length = content_length.to_i
55 |         end
56 | 
57 |         content_type = env['CONTENT_TYPE']
58 | 
59 |         tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
60 |         bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
61 | 
62 |         info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
63 |         env[RACK_TEMPFILES] = info.tmp_files
64 | 
65 |         return info.params
66 |       end
67 | 
68 |       def extract_multipart(request, params = Rack::Utils.default_query_parser)
69 |         parse_multipart(request.env)
70 |       end
71 | 
72 |       def build_multipart(params, first = true)
73 |         Generator.new(params, first).dump
74 |       end
75 |     end
76 |   end
77 | end
78 | 


--------------------------------------------------------------------------------
/lib/rack/multipart/generator.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'uploaded_file'
  4 | 
  5 | module Rack
  6 |   module Multipart
  7 |     class Generator
  8 |       def initialize(params, first = true)
  9 |         @params, @first = params, first
 10 | 
 11 |         if @first && !@params.is_a?(Hash)
 12 |           raise ArgumentError, "value must be a Hash"
 13 |         end
 14 |       end
 15 | 
 16 |       def dump
 17 |         return nil if @first && !multipart?
 18 |         return flattened_params unless @first
 19 | 
 20 |         flattened_params.map do |name, file|
 21 |           if file.respond_to?(:original_filename)
 22 |             if file.path
 23 |               ::File.open(file.path, 'rb') do |f|
 24 |                 f.set_encoding(Encoding::BINARY)
 25 |                 content_for_tempfile(f, file, name)
 26 |               end
 27 |             else
 28 |               content_for_tempfile(file, file, name)
 29 |             end
 30 |           else
 31 |             content_for_other(file, name)
 32 |           end
 33 |         end.join << "--#{MULTIPART_BOUNDARY}--\r"
 34 |       end
 35 | 
 36 |       private
 37 |       def multipart?
 38 |         query = lambda { |value|
 39 |           case value
 40 |           when Array
 41 |             value.any?(&query)
 42 |           when Hash
 43 |             value.values.any?(&query)
 44 |           when Rack::Multipart::UploadedFile
 45 |             true
 46 |           end
 47 |         }
 48 | 
 49 |         @params.values.any?(&query)
 50 |       end
 51 | 
 52 |       def flattened_params
 53 |         @flattened_params ||= begin
 54 |           h = Hash.new
 55 |           @params.each do |key, value|
 56 |             k = @first ? key.to_s : "[#{key}]"
 57 | 
 58 |             case value
 59 |             when Array
 60 |               value.map { |v|
 61 |                 Multipart.build_multipart(v, false).each { |subkey, subvalue|
 62 |                   h["#{k}[]#{subkey}"] = subvalue
 63 |                 }
 64 |               }
 65 |             when Hash
 66 |               Multipart.build_multipart(value, false).each { |subkey, subvalue|
 67 |                 h[k + subkey] = subvalue
 68 |               }
 69 |             else
 70 |               h[k] = value
 71 |             end
 72 |           end
 73 |           h
 74 |         end
 75 |       end
 76 | 
 77 |       def content_for_tempfile(io, file, name)
 78 |         length = ::File.stat(file.path).size if file.path
 79 |         filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
 80 | <<-EOF
 81 | --#{MULTIPART_BOUNDARY}\r
 82 | content-disposition: form-data; name="#{name}"#{filename}\r
 83 | content-type: #{file.content_type}\r
 84 | #{"content-length: #{length}\r\n" if length}\r
 85 | #{io.read}\r
 86 | EOF
 87 |       end
 88 | 
 89 |       def content_for_other(file, name)
 90 | <<-EOF
 91 | --#{MULTIPART_BOUNDARY}\r
 92 | content-disposition: form-data; name="#{name}"\r
 93 | \r
 94 | #{file}\r
 95 | EOF
 96 |       end
 97 |     end
 98 |   end
 99 | end
100 | 


--------------------------------------------------------------------------------
/lib/rack/multipart/uploaded_file.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'tempfile'
 4 | require 'fileutils'
 5 | 
 6 | module Rack
 7 |   module Multipart
 8 |     # Despite the misleading name, UploadedFile is designed for use for
 9 |     # preparing multipart file upload bodies, generally for use in tests.
10 |     # It is not designed for and should not be used for handling uploaded
11 |     # files (there is no need for that, since Rack's multipart parser
12 |     # already creates Tempfiles for that). Using this with non-trusted
13 |     # filenames can create a security vulnerability.
14 |     #
15 |     # You should only use this class if you plan on passing the instances
16 |     # to Rack::MockRequest for use in creating multipart request bodies.
17 |     #
18 |     # UploadedFile delegates most methods to the tempfile it contains.
19 |     class UploadedFile
20 |       # The provided name of the file. This generally is the basename of
21 |       # path provided during initialization, but it can contain slashes if they
22 |       # were present in the filename argument when the instance was created.
23 |       attr_reader :original_filename
24 | 
25 |       # The content type of the instance.
26 |       attr_accessor :content_type
27 | 
28 |       # Create a new UploadedFile.  For backwards compatibility, this accepts
29 |       # both positional and keyword versions of the same arguments:
30 |       #
31 |       # filepath/path :: The path to the file
32 |       # ct/content_type :: The content_type of the file
33 |       # bin/binary :: Whether to set binmode on the file before copying data into it.
34 |       #
35 |       # If both positional and keyword arguments are present, the keyword arguments
36 |       # take precedence.
37 |       #
38 |       # The following keyword-only arguments are also accepted:
39 |       #
40 |       # filename :: Override the filename to use for the file.  This is so the
41 |       #             filename for the upload does not need to match the basename of
42 |       #             the file path.  This should not contain slashes, unless you are
43 |       #             trying to test how an application handles invalid filenames in
44 |       #             multipart upload bodies.
45 |       # io :: Use the given IO-like instance as the tempfile, instead of creating
46 |       #       a Tempfile instance.  This is useful for building multipart file
47 |       #       upload bodies without a file being present on the filesystem. If you are
48 |       #       providing this, you should also provide the filename argument.
49 |       def initialize(filepath = nil, ct = "text/plain", bin = false,
50 |                      path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
51 |         if io
52 |           @tempfile = io
53 |           @original_filename = filename
54 |         else
55 |           raise "#{path} file does not exist" unless ::File.exist?(path)
56 |           @original_filename = filename || ::File.basename(path)
57 |           @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
58 |           @tempfile.binmode if binary
59 |           FileUtils.copy_file(path, @tempfile.path)
60 |         end
61 |         @content_type = content_type
62 |       end
63 | 
64 |       # The path of the tempfile for the instance, if the tempfile has a path.
65 |       # nil if the tempfile does not have a path.
66 |       def path
67 |         @tempfile.path if @tempfile.respond_to?(:path)
68 |       end
69 |       alias_method :local_path, :path
70 | 
71 |       # Return true if the tempfile responds to the method.
72 |       def respond_to_missing?(*args)
73 |         @tempfile.respond_to?(*args)
74 |       end
75 | 
76 |       # Delegate method missing calls to the tempfile.
77 |       def method_missing(method_name, *args, &block) #:nodoc:
78 |         @tempfile.__send__(method_name, *args, &block)
79 |       end
80 |     end
81 |   end
82 | end
83 | 


--------------------------------------------------------------------------------
/lib/rack/null_logger.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | 
 5 | module Rack
 6 |   class NullLogger
 7 |     def initialize(app)
 8 |       @app = app
 9 |     end
10 | 
11 |     def call(env)
12 |       env[RACK_LOGGER] = self
13 |       @app.call(env)
14 |     end
15 | 
16 |     def info(progname = nil, &block); end
17 |     def debug(progname = nil, &block); end
18 |     def warn(progname = nil, &block); end
19 |     def error(progname = nil, &block); end
20 |     def fatal(progname = nil, &block); end
21 |     def unknown(progname = nil, &block); end
22 |     def info? ;  end
23 |     def debug? ; end
24 |     def warn? ;  end
25 |     def error? ; end
26 |     def fatal? ; end
27 |     def debug! ; end
28 |     def error! ; end
29 |     def fatal! ; end
30 |     def info! ; end
31 |     def warn! ; end
32 |     def level ; end
33 |     def progname ; end
34 |     def datetime_format ; end
35 |     def formatter ; end
36 |     def sev_threshold ; end
37 |     def level=(level); end
38 |     def progname=(progname); end
39 |     def datetime_format=(datetime_format); end
40 |     def formatter=(formatter); end
41 |     def sev_threshold=(sev_threshold); end
42 |     def close ; end
43 |     def add(severity, message = nil, progname = nil, &block); end
44 |     def log(severity, message = nil, progname = nil, &block); end
45 |     def <<(msg); end
46 |     def reopen(logdev = nil); end
47 |   end
48 | end
49 | 


--------------------------------------------------------------------------------
/lib/rack/recursive.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'uri'
 4 | 
 5 | require_relative 'constants'
 6 | 
 7 | module Rack
 8 |   # Rack::ForwardRequest gets caught by Rack::Recursive and redirects
 9 |   # the current request to the app at +url+.
10 |   #
11 |   #   raise ForwardRequest.new("/not-found")
12 |   #
13 | 
14 |   class ForwardRequest < Exception
15 |     attr_reader :url, :env
16 | 
17 |     def initialize(url, env = {})
18 |       @url = URI(url)
19 |       @env = env
20 | 
21 |       @env[PATH_INFO]       = @url.path
22 |       @env[QUERY_STRING]    = @url.query  if @url.query
23 |       @env[HTTP_HOST]       = @url.host   if @url.host
24 |       @env[HTTP_PORT]       = @url.port   if @url.port
25 |       @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
26 | 
27 |       super "forwarding to #{url}"
28 |     end
29 |   end
30 | 
31 |   # Rack::Recursive allows applications called down the chain to
32 |   # include data from other applications (by using
33 |   # <tt>rack['rack.recursive.include'][...]</tt> or raise a
34 |   # ForwardRequest to redirect internally.
35 | 
36 |   class Recursive
37 |     def initialize(app)
38 |       @app = app
39 |     end
40 | 
41 |     def call(env)
42 |       dup._call(env)
43 |     end
44 | 
45 |     def _call(env)
46 |       @script_name = env[SCRIPT_NAME]
47 |       @app.call(env.merge(RACK_RECURSIVE_INCLUDE => method(:include)))
48 |     rescue ForwardRequest => req
49 |       call(env.merge(req.env))
50 |     end
51 | 
52 |     def include(env, path)
53 |       unless path.index(@script_name) == 0 && (path[@script_name.size] == ?/ ||
54 |                                                path[@script_name.size].nil?)
55 |         raise ArgumentError, "can only include below #{@script_name}, not #{path}"
56 |       end
57 | 
58 |       env = env.merge(PATH_INFO => path,
59 |                       SCRIPT_NAME => @script_name,
60 |                       REQUEST_METHOD => GET,
61 |                       "CONTENT_LENGTH" => "0", "CONTENT_TYPE" => "",
62 |                       RACK_INPUT => StringIO.new(""))
63 |       @app.call(env)
64 |     end
65 |   end
66 | end
67 | 


--------------------------------------------------------------------------------
/lib/rack/reloader.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | # Copyright (C) 2009-2018 Michael Fellinger <m.fellinger@gmail.com>
  4 | # Rack::Reloader is subject to the terms of an MIT-style license.
  5 | # See MIT-LICENSE or https://opensource.org/licenses/MIT.
  6 | 
  7 | require 'pathname'
  8 | 
  9 | module Rack
 10 | 
 11 |   # High performant source reloader
 12 |   #
 13 |   # This class acts as Rack middleware.
 14 |   #
 15 |   # What makes it especially suited for use in a production environment is that
 16 |   # any file will only be checked once and there will only be made one system
 17 |   # call stat(2).
 18 |   #
 19 |   # Please note that this will not reload files in the background, it does so
 20 |   # only when actively called.
 21 |   #
 22 |   # It is performing a check/reload cycle at the start of every request, but
 23 |   # also respects a cool down time, during which nothing will be done.
 24 |   class Reloader
 25 |     def initialize(app, cooldown = 10, backend = Stat)
 26 |       @app = app
 27 |       @cooldown = cooldown
 28 |       @last = (Time.now - cooldown)
 29 |       @cache = {}
 30 |       @mtimes = {}
 31 |       @reload_mutex = Mutex.new
 32 | 
 33 |       extend backend
 34 |     end
 35 | 
 36 |     def call(env)
 37 |       if @cooldown and Time.now > @last + @cooldown
 38 |         if Thread.list.size > 1
 39 |           @reload_mutex.synchronize{ reload! }
 40 |         else
 41 |           reload!
 42 |         end
 43 | 
 44 |         @last = Time.now
 45 |       end
 46 | 
 47 |       @app.call(env)
 48 |     end
 49 | 
 50 |     def reload!(stderr = $stderr)
 51 |       rotation do |file, mtime|
 52 |         previous_mtime = @mtimes[file] ||= mtime
 53 |         safe_load(file, mtime, stderr) if mtime > previous_mtime
 54 |       end
 55 |     end
 56 | 
 57 |     # A safe Kernel::load, issuing the hooks depending on the results
 58 |     def safe_load(file, mtime, stderr = $stderr)
 59 |       load(file)
 60 |       stderr.puts "#{self.class}: reloaded `#{file}'"
 61 |       file
 62 |     rescue LoadError, SyntaxError => ex
 63 |       stderr.puts ex
 64 |     ensure
 65 |       @mtimes[file] = mtime
 66 |     end
 67 | 
 68 |     module Stat
 69 |       def rotation
 70 |         files = [$0, *$LOADED_FEATURES].uniq
 71 |         paths = ['./', *$LOAD_PATH].uniq
 72 | 
 73 |         files.map{|file|
 74 |           next if /\.(so|bundle)$/.match?(file) # cannot reload compiled files
 75 | 
 76 |           found, stat = figure_path(file, paths)
 77 |           next unless found && stat && mtime = stat.mtime
 78 | 
 79 |           @cache[file] = found
 80 | 
 81 |           yield(found, mtime)
 82 |         }.compact
 83 |       end
 84 | 
 85 |       # Takes a relative or absolute +file+ name, a couple possible +paths+ that
 86 |       # the +file+ might reside in. Returns the full path and File::Stat for the
 87 |       # path.
 88 |       def figure_path(file, paths)
 89 |         found = @cache[file]
 90 |         found = file if !found and Pathname.new(file).absolute?
 91 |         found, stat = safe_stat(found)
 92 |         return found, stat if found
 93 | 
 94 |         paths.find do |possible_path|
 95 |           path = ::File.join(possible_path, file)
 96 |           found, stat = safe_stat(path)
 97 |           return ::File.expand_path(found), stat if found
 98 |         end
 99 | 
100 |         return false, false
101 |       end
102 | 
103 |       def safe_stat(file)
104 |         return unless file
105 |         stat = ::File.stat(file)
106 |         return file, stat if stat.file?
107 |       rescue Errno::ENOENT, Errno::ENOTDIR, Errno::ESRCH
108 |         @cache.delete(file) and false
109 |       end
110 |     end
111 |   end
112 | end
113 | 


--------------------------------------------------------------------------------
/lib/rack/rewindable_input.rb:
--------------------------------------------------------------------------------
  1 | # -*- encoding: binary -*-
  2 | # frozen_string_literal: true
  3 | 
  4 | require 'tempfile'
  5 | 
  6 | require_relative 'constants'
  7 | 
  8 | module Rack
  9 |   # Class which can make any IO object rewindable, including non-rewindable ones. It does
 10 |   # this by buffering the data into a tempfile, which is rewindable.
 11 |   #
 12 |   # Don't forget to call #close when you're done. This frees up temporary resources that
 13 |   # RewindableInput uses, though it does *not* close the original IO object.
 14 |   class RewindableInput
 15 |     # Makes rack.input rewindable, for compatibility with applications and middleware
 16 |     # designed for earlier versions of Rack (where rack.input was required to be
 17 |     # rewindable).
 18 |     class Middleware
 19 |       def initialize(app)
 20 |         @app = app
 21 |       end
 22 | 
 23 |       def call(env)
 24 |         if (input = env[RACK_INPUT])
 25 |           env[RACK_INPUT] = RewindableInput.new(input)
 26 |         end
 27 | 
 28 |         @app.call(env)
 29 |       end
 30 |     end
 31 | 
 32 |     def initialize(io)
 33 |       @io = io
 34 |       @rewindable_io = nil
 35 |       @unlinked = false
 36 |     end
 37 | 
 38 |     def gets
 39 |       make_rewindable unless @rewindable_io
 40 |       @rewindable_io.gets
 41 |     end
 42 | 
 43 |     def read(*args)
 44 |       make_rewindable unless @rewindable_io
 45 |       @rewindable_io.read(*args)
 46 |     end
 47 | 
 48 |     def each(&block)
 49 |       make_rewindable unless @rewindable_io
 50 |       @rewindable_io.each(&block)
 51 |     end
 52 | 
 53 |     def rewind
 54 |       make_rewindable unless @rewindable_io
 55 |       @rewindable_io.rewind
 56 |     end
 57 | 
 58 |     def size
 59 |       make_rewindable unless @rewindable_io
 60 |       @rewindable_io.size
 61 |     end
 62 | 
 63 |     # Closes this RewindableInput object without closing the originally
 64 |     # wrapped IO object. Cleans up any temporary resources that this RewindableInput
 65 |     # has created.
 66 |     #
 67 |     # This method may be called multiple times. It does nothing on subsequent calls.
 68 |     def close
 69 |       if @rewindable_io
 70 |         if @unlinked
 71 |           @rewindable_io.close
 72 |         else
 73 |           @rewindable_io.close!
 74 |         end
 75 |         @rewindable_io = nil
 76 |       end
 77 |     end
 78 | 
 79 |     private
 80 | 
 81 |     def make_rewindable
 82 |       # Buffer all data into a tempfile. Since this tempfile is private to this
 83 |       # RewindableInput object, we chmod it so that nobody else can read or write
 84 |       # it. On POSIX filesystems we also unlink the file so that it doesn't
 85 |       # even have a file entry on the filesystem anymore, though we can still
 86 |       # access it because we have the file handle open.
 87 |       @rewindable_io = Tempfile.new('RackRewindableInput')
 88 |       @rewindable_io.chmod(0000)
 89 |       @rewindable_io.set_encoding(Encoding::BINARY)
 90 |       @rewindable_io.binmode
 91 |       # :nocov:
 92 |       if filesystem_has_posix_semantics?
 93 |         raise 'Unlink failed. IO closed.' if @rewindable_io.closed?
 94 |         @unlinked = true
 95 |       end
 96 |       # :nocov:
 97 | 
 98 |       buffer = "".dup
 99 |       while @io.read(1024 * 4, buffer)
100 |         entire_buffer_written_out = false
101 |         while !entire_buffer_written_out
102 |           written = @rewindable_io.write(buffer)
103 |           entire_buffer_written_out = written == buffer.bytesize
104 |           if !entire_buffer_written_out
105 |             buffer.slice!(0 .. written - 1)
106 |           end
107 |         end
108 |       end
109 |       @rewindable_io.rewind
110 |     end
111 | 
112 |     def filesystem_has_posix_semantics?
113 |       RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
114 |     end
115 |   end
116 | end
117 | 


--------------------------------------------------------------------------------
/lib/rack/runtime.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'utils'
 4 | 
 5 | module Rack
 6 |   # Sets an "x-runtime" response header, indicating the response
 7 |   # time of the request, in seconds
 8 |   #
 9 |   # You can put it right before the application to see the processing
10 |   # time, or before all the other middlewares to include time for them,
11 |   # too.
12 |   class Runtime
13 |     FORMAT_STRING = "%0.6f" # :nodoc:
14 |     HEADER_NAME = "x-runtime" # :nodoc:
15 | 
16 |     def initialize(app, name = nil)
17 |       @app = app
18 |       @header_name = HEADER_NAME
19 |       @header_name += "-#{name.to_s.downcase}" if name
20 |     end
21 | 
22 |     def call(env)
23 |       start_time = Utils.clock_time
24 |       _, headers, _ = response = @app.call(env)
25 | 
26 |       request_time = Utils.clock_time - start_time
27 | 
28 |       unless headers.key?(@header_name)
29 |         headers[@header_name] = FORMAT_STRING % request_time
30 |       end
31 | 
32 |       response
33 |     end
34 |   end
35 | end
36 | 


--------------------------------------------------------------------------------
/lib/rack/show_status.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require 'erb'
  4 | 
  5 | require_relative 'constants'
  6 | require_relative 'utils'
  7 | require_relative 'request'
  8 | require_relative 'body_proxy'
  9 | 
 10 | module Rack
 11 |   # Rack::ShowStatus catches all empty responses and replaces them
 12 |   # with a site explaining the error.
 13 |   #
 14 |   # Additional details can be put into <tt>rack.showstatus.detail</tt>
 15 |   # and will be shown as HTML.  If such details exist, the error page
 16 |   # is always rendered, even if the reply was not empty.
 17 | 
 18 |   class ShowStatus
 19 |     def initialize(app)
 20 |       @app = app
 21 |       @template = ERB.new(TEMPLATE)
 22 |     end
 23 | 
 24 |     def call(env)
 25 |       status, headers, body = response = @app.call(env)
 26 |       empty = headers[CONTENT_LENGTH].to_i <= 0
 27 | 
 28 |       # client or server error, or explicit message
 29 |       if (status.to_i >= 400 && empty) || env[RACK_SHOWSTATUS_DETAIL]
 30 |         # This double assignment is to prevent an "unused variable" warning.
 31 |         # Yes, it is dumb, but I don't like Ruby yelling at me.
 32 |         req = req = Rack::Request.new(env)
 33 | 
 34 |         message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
 35 | 
 36 |         # This double assignment is to prevent an "unused variable" warning.
 37 |         # Yes, it is dumb, but I don't like Ruby yelling at me.
 38 |         detail = detail = env[RACK_SHOWSTATUS_DETAIL] || message
 39 | 
 40 |         html = @template.result(binding)
 41 |         size = html.bytesize
 42 | 
 43 |         response[2] = Rack::BodyProxy.new([html]) do
 44 |           body.close if body.respond_to?(:close)
 45 |         end
 46 | 
 47 |         headers[CONTENT_TYPE] = "text/html"
 48 |         headers[CONTENT_LENGTH] = size.to_s
 49 |       end
 50 | 
 51 |       response
 52 |     end
 53 | 
 54 |     def h(obj)                  # :nodoc:
 55 |       case obj
 56 |       when String
 57 |         Utils.escape_html(obj)
 58 |       else
 59 |         Utils.escape_html(obj.inspect)
 60 |       end
 61 |     end
 62 | 
 63 |     # :stopdoc:
 64 | 
 65 | # adapted from Django <www.djangoproject.com>
 66 | # Copyright (c) Django Software Foundation and individual contributors.
 67 | # Used under the modified BSD license:
 68 | # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
 69 | TEMPLATE = <<'HTML'
 70 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 71 | <html lang="en">
 72 | <head>
 73 |   <meta http-equiv="content-type" content="text/html; charset=utf-8" />
 74 |   <title><%=h message %> at <%=h req.script_name + req.path_info %></title>
 75 |   <meta name="robots" content="NONE,NOARCHIVE" />
 76 |   <style type="text/css">
 77 |     html * { padding:0; margin:0; }
 78 |     body * { padding:10px 20px; }
 79 |     body * * { padding:0; }
 80 |     body { font:small sans-serif; background:#eee; }
 81 |     body>div { border-bottom:1px solid #ddd; }
 82 |     h1 { font-weight:normal; margin-bottom:.4em; }
 83 |     h1 span { font-size:60%; color:#666; font-weight:normal; }
 84 |     table { border:none; border-collapse: collapse; width:100%; }
 85 |     td, th { vertical-align:top; padding:2px 3px; }
 86 |     th { width:12em; text-align:right; color:#666; padding-right:.5em; }
 87 |     #info { background:#f6f6f6; }
 88 |     #info ol { margin: 0.5em 4em; }
 89 |     #info ol li { font-family: monospace; }
 90 |     #summary { background: #ffc; }
 91 |     #explanation { background:#eee; border-bottom: 0px none; }
 92 |   </style>
 93 | </head>
 94 | <body>
 95 |   <div id="summary">
 96 |     <h1><%=h message %> <span>(<%= status.to_i %>)</span></h1>
 97 |     <table class="meta">
 98 |       <tr>
 99 |         <th>Request Method:</th>
100 |         <td><%=h req.request_method %></td>
101 |       </tr>
102 |       <tr>
103 |         <th>Request URL:</th>
104 |       <td><%=h req.url %></td>
105 |       </tr>
106 |     </table>
107 |   </div>
108 |   <div id="info">
109 |     <p><%=h detail %></p>
110 |   </div>
111 | 
112 |   <div id="explanation">
113 |     <p>
114 |     You're seeing this error because you use <code>Rack::ShowStatus</code>.
115 |     </p>
116 |   </div>
117 | </body>
118 | </html>
119 | HTML
120 | 
121 |     # :startdoc:
122 |   end
123 | end
124 | 


--------------------------------------------------------------------------------
/lib/rack/tempfile_reaper.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'constants'
 4 | require_relative 'body_proxy'
 5 | 
 6 | module Rack
 7 | 
 8 |   # Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart)
 9 |   # Ideas/strategy based on posts by Eric Wong and Charles Oliver Nutter
10 |   # https://groups.google.com/forum/#!searchin/rack-devel/temp/rack-devel/brK8eh-MByw/sw61oJJCGRMJ
11 |   class TempfileReaper
12 |     def initialize(app)
13 |       @app = app
14 |     end
15 | 
16 |     def call(env)
17 |       env[RACK_TEMPFILES] ||= []
18 | 
19 |       begin
20 |         _, _, body = response = @app.call(env)
21 |       rescue Exception
22 |         env[RACK_TEMPFILES]&.each(&:close!)
23 |         raise
24 |       end
25 | 
26 |       response[2] = BodyProxy.new(body) do
27 |         env[RACK_TEMPFILES]&.each(&:close!)
28 |       end
29 | 
30 |       response
31 |     end
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/lib/rack/urlmap.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require 'set'
  4 | 
  5 | require_relative 'constants'
  6 | 
  7 | module Rack
  8 |   # Rack::URLMap takes a hash mapping urls or paths to apps, and
  9 |   # dispatches accordingly.  Support for HTTP/1.1 host names exists if
 10 |   # the URLs start with <tt>http://</tt> or <tt>https://</tt>.
 11 |   #
 12 |   # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
 13 |   # relevant for dispatch is in the SCRIPT_NAME, and the rest in the
 14 |   # PATH_INFO.  This should be taken care of when you need to
 15 |   # reconstruct the URL in order to create links.
 16 |   #
 17 |   # URLMap dispatches in such a way that the longest paths are tried
 18 |   # first, since they are most specific.
 19 | 
 20 |   class URLMap
 21 |     def initialize(map = {})
 22 |       remap(map)
 23 |     end
 24 | 
 25 |     def remap(map)
 26 |       @known_hosts = Set[]
 27 |       @mapping = map.map { |location, app|
 28 |         if location =~ %r{\Ahttps?://(.*?)(/.*)}
 29 |           host, location = $1, $2
 30 |           @known_hosts << host
 31 |         else
 32 |           host = nil
 33 |         end
 34 | 
 35 |         unless location[0] == ?/
 36 |           raise ArgumentError, "paths need to start with /"
 37 |         end
 38 | 
 39 |         location = location.chomp('/')
 40 |         match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
 41 | 
 42 |         [host, location, match, app]
 43 |       }.sort_by do |(host, location, _, _)|
 44 |         [host ? -host.size : Float::INFINITY, -location.size]
 45 |       end
 46 |     end
 47 | 
 48 |     def call(env)
 49 |       path        = env[PATH_INFO]
 50 |       script_name = env[SCRIPT_NAME]
 51 |       http_host   = env[HTTP_HOST]
 52 |       server_name = env[SERVER_NAME]
 53 |       server_port = env[SERVER_PORT]
 54 | 
 55 |       is_same_server = casecmp?(http_host, server_name) ||
 56 |                        casecmp?(http_host, "#{server_name}:#{server_port}")
 57 | 
 58 |       is_host_known = @known_hosts.include? http_host
 59 | 
 60 |       @mapping.each do |host, location, match, app|
 61 |         unless casecmp?(http_host, host) \
 62 |             || casecmp?(server_name, host) \
 63 |             || (!host && is_same_server) \
 64 |             || (!host && !is_host_known) # If we don't have a matching host, default to the first without a specified host
 65 |           next
 66 |         end
 67 | 
 68 |         next unless m = match.match(path.to_s)
 69 | 
 70 |         rest = m[1]
 71 |         next unless !rest || rest.empty? || rest[0] == ?/
 72 | 
 73 |         env[SCRIPT_NAME] = (script_name + location)
 74 |         env[PATH_INFO] = rest
 75 | 
 76 |         return app.call(env)
 77 |       end
 78 | 
 79 |       [404, { CONTENT_TYPE => "text/plain", "x-cascade" => "pass" }, ["Not Found: #{path}"]]
 80 | 
 81 |     ensure
 82 |       env[PATH_INFO]   = path
 83 |       env[SCRIPT_NAME] = script_name
 84 |     end
 85 | 
 86 |     private
 87 |     def casecmp?(v1, v2)
 88 |       # if both nil, or they're the same string
 89 |       return true if v1 == v2
 90 | 
 91 |       # if either are nil... (but they're not the same)
 92 |       return false if v1.nil?
 93 |       return false if v2.nil?
 94 | 
 95 |       # otherwise check they're not case-insensitive the same
 96 |       v1.casecmp(v2).zero?
 97 |     end
 98 |   end
 99 | end
100 | 


--------------------------------------------------------------------------------
/lib/rack/version.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
 4 | #
 5 | # Rack is freely distributable under the terms of an MIT-style license.
 6 | # See MIT-LICENSE or https://opensource.org/licenses/MIT.
 7 | 
 8 | module Rack
 9 |   VERSION = "3.1.1"
10 | 
11 |   RELEASE = VERSION
12 | 
13 |   # Return the Rack release as a dotted string.
14 |   def self.release
15 |     VERSION
16 |   end
17 | end
18 | 


--------------------------------------------------------------------------------
/rack.gemspec:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'lib/rack/version'
 4 | 
 5 | Gem::Specification.new do |s|
 6 |   s.name = "rack"
 7 |   s.version = Rack::VERSION
 8 |   s.platform = Gem::Platform::RUBY
 9 |   s.summary = "A modular Ruby webserver interface."
10 |   s.license = "MIT"
11 | 
12 |   s.description = <<~EOF
13 |     Rack provides a minimal, modular and adaptable interface for developing
14 |     web applications in Ruby. By wrapping HTTP requests and responses in
15 |     the simplest way possible, it unifies and distills the API for web
16 |     servers, web frameworks, and software in between (the so-called
17 |     middleware) into a single method call.
18 |   EOF
19 | 
20 |   s.files = Dir['lib/**/*'] + %w(MIT-LICENSE README.md SPEC.rdoc)
21 |   s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'CONTRIBUTING.md']
22 | 
23 |   s.author = 'Leah Neukirchen'
24 |   s.email = 'leah@vuxu.org'
25 | 
26 |   s.homepage = 'https://github.com/rack/rack'
27 | 
28 |   s.required_ruby_version = '>= 2.4.0'
29 | 
30 |   s.metadata = {
31 |     "bug_tracker_uri" => "https://github.com/rack/rack/issues",
32 |     "changelog_uri" => "https://github.com/rack/rack/blob/main/CHANGELOG.md",
33 |     "documentation_uri" => "https://rubydoc.info/github/rack/rack",
34 |     "source_code_uri"   => "https://github.com/rack/rack",
35 |     "rubygems_mfa_required" => "true"
36 |   }
37 | 
38 |   s.add_development_dependency 'minitest', "~> 5.0"
39 |   s.add_development_dependency 'minitest-global_expectations'
40 | 
41 |   s.add_development_dependency 'bundler'
42 |   s.add_development_dependency 'rake'
43 | end
44 | 


--------------------------------------------------------------------------------
/test/.bacon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/test/.bacon


--------------------------------------------------------------------------------
/test/builder/an_underscore_app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | class AnUnderscoreApp
4 |   def self.call(env)
5 |     [200, { 'content-type' => 'text/plain' }, ['OK']]
6 |   end
7 | end
8 | 


--------------------------------------------------------------------------------
/test/builder/bom.ru:
--------------------------------------------------------------------------------
1 | run -> (env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
2 | 


--------------------------------------------------------------------------------
/test/builder/comment.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | =begin
4 | 
5 | =end
6 | run lambda { |env| [200, { 'content-type' => 'text/plain' }, ['OK']] }
7 | 


--------------------------------------------------------------------------------
/test/builder/end.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | run lambda { |env| [200, { 'content-type' => 'text/plain' }, ['OK']] }
4 | __END__
5 | Should not be evaluated
6 | Neither should
7 | This
8 | 


--------------------------------------------------------------------------------
/test/builder/frozen.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | run lambda { |env|
4 |   body = 'frozen'
5 |   raise "Not frozen!" unless body.frozen?
6 |   [200, { 'content-type' => 'text/plain' }, [body]]
7 | }
8 | 


--------------------------------------------------------------------------------
/test/builder/line.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | run lambda{ |env| [200, { 'content-type' => 'text/plain' }, [__LINE__.to_s]] }
4 | 


--------------------------------------------------------------------------------
/test/builder/options.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | #\ -d -p 2929 --env test
4 | run lambda { |env| [200, { 'content-type' => 'text/plain' }, ['OK']] }
5 | 


--------------------------------------------------------------------------------
/test/cgi/assets/folder/test.js:
--------------------------------------------------------------------------------
1 | ### TestFile ###
2 | 


--------------------------------------------------------------------------------
/test/cgi/assets/fonts/font.eot:
--------------------------------------------------------------------------------
1 | ### TestFile ###
2 | 


--------------------------------------------------------------------------------
/test/cgi/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/test/cgi/assets/images/favicon.ico


--------------------------------------------------------------------------------
/test/cgi/assets/images/image.png:
--------------------------------------------------------------------------------
1 | ### TestFile ###
2 | 


--------------------------------------------------------------------------------
/test/cgi/assets/index.html:
--------------------------------------------------------------------------------
1 | ### TestFile ###
2 | 


--------------------------------------------------------------------------------
/test/cgi/assets/javascripts/app.js:
--------------------------------------------------------------------------------
1 | ### TestFile ###
2 | 


--------------------------------------------------------------------------------
/test/cgi/assets/stylesheets/app.css:
--------------------------------------------------------------------------------
1 | ### TestFile ###
2 | 


--------------------------------------------------------------------------------
/test/cgi/rackup_stub.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 | 
4 | $:.unshift '../../lib'
5 | require 'rack'
6 | Rack::Server.start
7 | 


--------------------------------------------------------------------------------
/test/cgi/sample_rackup.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | require '../test_request'
4 | 
5 | run Rack::Lint.new(TestRequest.new)
6 | 


--------------------------------------------------------------------------------
/test/cgi/test:
--------------------------------------------------------------------------------
1 | ***** DO NOT MODIFY THIS FILE! *****
2 | If you modify this file, tests will break!!!
3 | The quick brown fox jumps over the ruby dog.
4 | The quick brown fox jumps over the lazy dog.
5 | ***** DO NOT MODIFY THIS FILE! *****
6 | 


--------------------------------------------------------------------------------
/test/cgi/test+directory/test+file:
--------------------------------------------------------------------------------
1 | this file has plusses!
2 | 


--------------------------------------------------------------------------------
/test/cgi/test.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/test/cgi/test.gz


--------------------------------------------------------------------------------
/test/cgi/test.ru:
--------------------------------------------------------------------------------
1 | #!../../bin/rackup
2 | # frozen_string_literal: true
3 | 
4 | require '../test_request'
5 | run Rack::Lint.new(TestRequest.new)
6 | 


--------------------------------------------------------------------------------
/test/gemloader.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'rubygems'
 4 | project = 'rack'
 5 | gemspec = File.expand_path("#{project}.gemspec", Dir.pwd)
 6 | Gem::Specification.load(gemspec).dependencies.each do |dep|
 7 |   begin
 8 |     gem dep.name, *dep.requirement.as_list
 9 |   rescue Gem::LoadError
10 |     warn "Cannot load #{dep.name} #{dep.requirement.to_s}"
11 |   end
12 | end
13 | 


--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | if ENV.delete('COVERAGE')
 4 |   require 'simplecov'
 5 | 
 6 |   SimpleCov.start do
 7 |     enable_coverage :branch
 8 |     add_filter "/test/"
 9 |     add_group('Missing'){|src| src.covered_percent < 100}
10 |     add_group('Covered'){|src| src.covered_percent == 100}
11 |   end
12 | end
13 | 
14 | if ENV['SEPARATE']
15 |   def self.separate_testing
16 |     yield
17 |   end
18 | else
19 |   $:.unshift(File.expand_path('../lib', __dir__))
20 |   require_relative '../lib/rack'
21 | 
22 |   def self.separate_testing
23 |   end
24 | end
25 | 
26 | require 'minitest/global_expectations/autorun'
27 | require 'stringio'
28 | 
29 | class Minitest::Spec
30 |   def self.deprecated(*args, &block)
31 |     it(*args) do
32 |       begin
33 |         verbose, $VERBOSE = $VERBOSE, nil
34 |         instance_exec(&block)
35 |       ensure
36 |         $VERBOSE = verbose
37 |       end
38 |     end
39 |   end
40 |   
41 |   def capture_warnings(target)
42 |     verbose = $VERBOSE
43 |     warnings = Thread::Queue.new
44 |     target.define_singleton_method(:warn) do |*args|
45 |       warnings << args
46 |     end
47 |     begin
48 |       $VERBOSE = true
49 |       yield warnings
50 |     ensure
51 |       $VERBOSE = verbose
52 |       target.singleton_class.send(:remove_method, :warn)
53 |     end
54 |   end
55 | end
56 | 


--------------------------------------------------------------------------------
/test/multipart/binary:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/test/multipart/binary


--------------------------------------------------------------------------------
/test/multipart/content_type_and_no_disposition:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-type: text/plain; charset=US-ASCII
3 | 
4 | contents
5 | --AaB03x--
6 | 


--------------------------------------------------------------------------------
/test/multipart/content_type_and_no_filename:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="text"
3 | content-type: text/plain; charset=US-ASCII
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/content_type_and_unknown_charset:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="text"
3 | content-type: text/plain; charset=foo; bar=baz
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/empty:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="submit-name"
 3 | 
 4 | Larry
 5 | --AaB03x
 6 | content-disposition: form-data; name="files"; filename="file1.txt"
 7 | content-type: text/plain
 8 | 
 9 | 
10 | --AaB03x--
11 | 


--------------------------------------------------------------------------------
/test/multipart/end_boundary_first:
--------------------------------------------------------------------------------
1 | --AaB03x--
2 | 
3 | --AaB03x
4 | Content-Disposition: form-data; name="files"; filename="foo"
5 | Content-Type: application/octet-stream
6 | 
7 | contents
8 | --AaB03x--
9 | 


--------------------------------------------------------------------------------
/test/multipart/file1.txt:
--------------------------------------------------------------------------------
1 | contents


--------------------------------------------------------------------------------
/test/multipart/filename_and_modification_param:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-type: image/jpeg
3 | content-disposition: attachment; name="files"; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500";
4 | Content-Description: a complete map of the human genome
5 | 
6 | contents
7 | --AaB03x--
8 | 


--------------------------------------------------------------------------------
/test/multipart/filename_and_no_name:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; filename="file1.txt"
3 | content-type: text/plain
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_multi:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | Content-Disposition: form-data; name="files"; filename="foo"; filename*=utf-8''bar
3 | Content-Type: application/octet-stream
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_encoded_words:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-type: image/jpeg
3 | content-disposition: attachment; name="files"; filename*=utf-8''%D1%84%D0%B0%D0%B9%D0%BB
4 | Content-Description: a complete map of the human genome
5 | 
6 | contents
7 | --AaB03x--
8 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_escaped_quotes:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="files"; filename="escape \"quotes"
3 | content-type: application/octet-stream
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_escaped_quotes_and_modification_param:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-type: image/jpeg
3 | content-disposition: attachment; name="files"; filename="\"human\" genome.jpeg"; modification-date="Wed, 12 Feb 1997 16:29:51 -0500";
4 | Content-Description: a complete map of the human genome
5 | 
6 | contents
7 | --AaB03x--
8 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_null_byte:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-type: image/jpeg
3 | content-disposition: attachment; name="files"; filename="flowers.exe%00.jpg"
4 | Content-Description: a complete map of the human genome
5 | 
6 | contents
7 | --AaB03x--
8 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_percent_escaped_quotes:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="files"; filename="escape %22quotes"
3 | content-type: application/octet-stream
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_plus:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="files"; filename="foo+bar"
3 | content-type: application/octet-stream
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_single_quote:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-type: image/jpeg
3 | content-disposition: attachment; name="files"; filename="bob's flowers.jpg"
4 | Content-Description: a complete map of the human genome
5 | 
6 | contents
7 | --AaB03x--
8 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_unescaped_percentages:
--------------------------------------------------------------------------------
1 | ------WebKitFormBoundary2NHc7OhsgU68l3Al
2 | content-disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"
3 | content-type: image/jpeg
4 | 
5 | contents
6 | ------WebKitFormBoundary2NHc7OhsgU68l3Al--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_unescaped_percentages2:
--------------------------------------------------------------------------------
1 | ------WebKitFormBoundary2NHc7OhsgU68l3Al
2 | content-disposition: form-data; name="document[attachment]"; filename="100%a"
3 | content-type: image/jpeg
4 | 
5 | contents
6 | ------WebKitFormBoundary2NHc7OhsgU68l3Al--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_unescaped_percentages3:
--------------------------------------------------------------------------------
1 | ------WebKitFormBoundary2NHc7OhsgU68l3Al
2 | content-disposition: form-data; name="document[attachment]"; filename="100%"
3 | content-type: image/jpeg
4 | 
5 | contents
6 | ------WebKitFormBoundary2NHc7OhsgU68l3Al--
7 | 


--------------------------------------------------------------------------------
/test/multipart/filename_with_unescaped_quotes:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="files"; filename="escape "quotes"
3 | content-type: application/octet-stream
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/ie:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="files"; filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"
3 | content-type: text/plain
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/invalid_character:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/test/multipart/invalid_character


--------------------------------------------------------------------------------
/test/multipart/mixed_files:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="foo"
 3 | 
 4 | bar
 5 | --AaB03x
 6 | content-disposition: form-data; name="files"
 7 | content-type: multipart/mixed; boundary=BbC04y
 8 | 
 9 | --BbC04y
10 | content-disposition: attachment; filename="file.txt"
11 | content-type: text/plain
12 | 
13 | contents
14 | --BbC04y
15 | content-disposition: attachment; filename="flowers.jpg"
16 | content-type: image/jpeg
17 | content-transfer-encoding: binary
18 | 
19 | contents
20 | --BbC04y--
21 | --AaB03x--
22 | 


--------------------------------------------------------------------------------
/test/multipart/multiple_encodings:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="us-ascii"
 3 | 
 4 | Alice
 5 | --AaB03x
 6 | content-disposition: form-data; name="iso-2022-jp"
 7 | content-type: text/plain; charset=iso-2022-jp
 8 | 
 9 | $B%"%j%9(B
10 | --AaB03x--
11 | 


--------------------------------------------------------------------------------
/test/multipart/nested:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="foo[submit-name]"
 3 | 
 4 | Larry
 5 | --AaB03x
 6 | content-disposition: form-data; name="foo[files]"; filename="file1.txt"
 7 | content-type: text/plain
 8 | 
 9 | contents
10 | --AaB03x--
11 | 


--------------------------------------------------------------------------------
/test/multipart/none:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="submit-name"
 3 | 
 4 | Larry
 5 | --AaB03x
 6 | content-disposition: form-data; name="files"; filename=""
 7 | 
 8 | 
 9 | --AaB03x--
10 | 


--------------------------------------------------------------------------------
/test/multipart/preceding_boundary:
--------------------------------------------------------------------------------
1 | A--AaB03x
2 | Content-Disposition: form-data; name="files"; filename="foo"
3 | Content-Type: application/octet-stream
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/quoted:
--------------------------------------------------------------------------------
 1 | --AaB:03x
 2 | content-disposition: form-data; name="submit-name"
 3 | 
 4 | Larry
 5 | --AaB:03x
 6 | content-disposition: form-data; name="submit-name-with-content"
 7 | content-type: text/plain
 8 | 
 9 | Berry
10 | --AaB:03x
11 | content-disposition: form-data; name="files"; filename="file1.txt"
12 | content-type: text/plain
13 | 
14 | contents
15 | --AaB:03x--
16 | 


--------------------------------------------------------------------------------
/test/multipart/rack-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rack/rack/45d2f874530e55b2ac77092da63b00ee979d18ba/test/multipart/rack-logo.png


--------------------------------------------------------------------------------
/test/multipart/robust_field_separation:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data;name="text"
3 | content-type: text/plain
4 | 
5 | contents
6 | --AaB03x--
7 | 


--------------------------------------------------------------------------------
/test/multipart/semicolon:
--------------------------------------------------------------------------------
1 | --AaB03x
2 | content-disposition: form-data; name="files"; filename="fi;le1.txt"
3 | content-type: text/plain
4 | 
5 | contents
6 | --AaB03x--


--------------------------------------------------------------------------------
/test/multipart/space case.txt:
--------------------------------------------------------------------------------
1 | contents


--------------------------------------------------------------------------------
/test/multipart/text:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="submit-name"
 3 | 
 4 | Larry
 5 | --AaB03x
 6 | content-disposition: form-data; name="submit-name-with-content"
 7 | content-type: text/plain
 8 | 
 9 | Berry
10 | --AaB03x
11 | content-disposition: form-data; name="files"; filename="file1.txt"
12 | content-type: text/plain
13 | 
14 | contents
15 | --AaB03x--


--------------------------------------------------------------------------------
/test/multipart/three_files_three_fields:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | content-disposition: form-data; name="reply"
 3 | 
 4 | yes
 5 | --AaB03x
 6 | content-disposition: form-data; name="to"
 7 | 
 8 | people
 9 | --AaB03x
10 | content-disposition: form-data; name="from"
11 | 
12 | others
13 | --AaB03x
14 | content-disposition: form-data; name="fileupload1"; filename="file1.jpg"
15 | content-type: image/jpeg
16 | content-transfer-encoding: base64
17 | 
18 | /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
19 | --AaB03x
20 | content-disposition: form-data; name="fileupload2"; filename="file2.jpg"
21 | content-type: image/jpeg
22 | content-transfer-encoding: base64
23 | 
24 | /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
25 | --AaB03x
26 | content-disposition: form-data; name="fileupload3"; filename="file3.jpg"
27 | content-type: image/jpeg
28 | content-transfer-encoding: base64
29 | 
30 | /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
31 | --AaB03x--
32 | 


--------------------------------------------------------------------------------
/test/multipart/unity3d_wwwform:
--------------------------------------------------------------------------------
 1 | --AaB03x
 2 | Content-Type: text/plain; charset="utf-8"
 3 | Content-disposition: form-data; name="user_sid"
 4 | 
 5 | bbf14f82-d2aa-4c07-9fb8-ca6714a7ea97
 6 | --AaB03x
 7 | Content-Type: image/png; charset=UTF-8
 8 | Content-disposition: form-data; name="file";
 9 | filename="b67879ed-bfed-4491-a8cc-f99cca769f94.png"
10 | 
11 | --AaB03x
12 | 


--------------------------------------------------------------------------------
/test/multipart/webkit:
--------------------------------------------------------------------------------
 1 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
 2 | content-disposition: form-data; name="_method"
 3 | 
 4 | put
 5 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
 6 | content-disposition: form-data; name="profile[blog]"
 7 | 
 8 | 
 9 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
10 | content-disposition: form-data; name="profile[public_email]"
11 | 
12 | 
13 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
14 | content-disposition: form-data; name="profile[interests]"
15 | 
16 | 
17 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
18 | content-disposition: form-data; name="profile[bio]"
19 | 
20 | hello
21 | 
22 | "quote"
23 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
24 | content-disposition: form-data; name="media"; filename=""
25 | Content-Type: application/octet-stream
26 | 
27 | 
28 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
29 | content-disposition: form-data; name="commit"
30 | 
31 | Save
32 | ------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
33 | 


--------------------------------------------------------------------------------
/test/psych_fix.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # Work correctly with older versions of Psych, having
 4 | # unsafe_load call load (in older versions, load operates
 5 | # as unsafe_load in current version).
 6 | unless YAML.respond_to?(:unsafe_load)
 7 |   def YAML.unsafe_load(body)
 8 |     load(body)
 9 |   end
10 | end
11 | 


--------------------------------------------------------------------------------
/test/rackup/.gitignore:
--------------------------------------------------------------------------------
1 | log_output
2 | 


--------------------------------------------------------------------------------
/test/rackup/config.ru:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative "../test_request"
 4 | 
 5 | $stderr = File.open("#{File.dirname(__FILE__)}/log_output", "w")
 6 | 
 7 | class EnvMiddleware
 8 |   def initialize(app)
 9 |     @app = app
10 |   end
11 | 
12 |   def call(env)
13 |     # provides a way to test that lint is present
14 |     if env["PATH_INFO"] == "/broken_lint"
15 |       return [200, {}, ["Broken Lint"]]
16 |     # provides a way to kill the process without knowing the pid
17 |     elsif env["PATH_INFO"] == "/die"
18 |       exit!
19 |     end
20 | 
21 |     env["test.$DEBUG"]      = $DEBUG
22 |     env["test.$EVAL"]       = BUKKIT if defined?(BUKKIT)
23 |     env["test.$VERBOSE"]    = $VERBOSE
24 |     env["test.$LOAD_PATH"]  = $LOAD_PATH
25 |     env["test.stderr"]      = File.expand_path($stderr.path)
26 |     env["test.Ping"]        = defined?(Ping)
27 |     env["test.pid"]         = Process.pid
28 |     @app.call(env)
29 |   end
30 | end
31 | 
32 | use EnvMiddleware
33 | run TestRequest.new
34 | 


--------------------------------------------------------------------------------
/test/spec_auth_basic.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/auth/basic'
  7 |   require_relative '../lib/rack/mock_request'
  8 |   require_relative '../lib/rack/lint'
  9 | end
 10 | 
 11 | describe Rack::Auth::Basic do
 12 |   def realm
 13 |     'WallysWorld'
 14 |   end
 15 | 
 16 |   def unprotected_app
 17 |     Rack::Lint.new lambda { |env|
 18 |       [ 200, { 'content-type' => 'text/plain' }, ["Hi #{env['REMOTE_USER']}"] ]
 19 |     }
 20 |   end
 21 | 
 22 |   def protected_app
 23 |     app = Rack::Auth::Basic.new(unprotected_app) { |username, password| 'Boss' == username }
 24 |     app.realm = realm
 25 |     app
 26 |   end
 27 | 
 28 |   before do
 29 |     @request = Rack::MockRequest.new(protected_app)
 30 |   end
 31 | 
 32 |   def request_with_basic_auth(username, password, &block)
 33 |     request 'HTTP_AUTHORIZATION' => 'Basic ' + ["#{username}:#{password}"].pack("m*"), &block
 34 |   end
 35 | 
 36 |   def request(headers = {})
 37 |     yield @request.get('/', headers)
 38 |   end
 39 | 
 40 |   def assert_basic_auth_challenge(response)
 41 |     response.must_be :client_error?
 42 |     response.status.must_equal 401
 43 |     response.must_include 'www-authenticate'
 44 |     response.headers['www-authenticate'].must_match(/Basic realm="#{Regexp.escape(realm)}"/)
 45 |     response.body.must_be :empty?
 46 |   end
 47 | 
 48 |   it 'challenge correctly when no credentials are specified' do
 49 |     request do |response|
 50 |       assert_basic_auth_challenge response
 51 |     end
 52 |   end
 53 | 
 54 |   it 'rechallenge if incorrect credentials are specified' do
 55 |     request_with_basic_auth 'joe', 'password' do |response|
 56 |       assert_basic_auth_challenge response
 57 |     end
 58 |   end
 59 | 
 60 |   it 'return application output if correct credentials are specified' do
 61 |     request_with_basic_auth 'Boss', 'password' do |response|
 62 |       response.status.must_equal 200
 63 |       response.body.to_s.must_equal 'Hi Boss'
 64 |     end
 65 |   end
 66 | 
 67 |   it 'return 400 Bad Request if different auth scheme used' do
 68 |     request 'HTTP_AUTHORIZATION' => 'Digest params' do |response|
 69 |       response.must_be :client_error?
 70 |       response.status.must_equal 400
 71 |       response.wont_include 'www-authenticate'
 72 |     end
 73 |   end
 74 | 
 75 |   it 'return 400 Bad Request for a malformed authorization header' do
 76 |     request 'HTTP_AUTHORIZATION' => '' do |response|
 77 |       response.must_be :client_error?
 78 |       response.status.must_equal 400
 79 |       response.wont_include 'www-authenticate'
 80 |     end
 81 |   end
 82 | 
 83 |   it 'return 401 Bad Request for a nil authorization header' do
 84 |     request 'HTTP_AUTHORIZATION' => nil do |response|
 85 |       response.must_be :client_error?
 86 |       response.status.must_equal 401
 87 |     end
 88 |   end
 89 | 
 90 |   it 'return 400 Bad Request for a authorization header with only username' do
 91 |     auth = 'Basic ' + ['foo'].pack("m*")
 92 |     request 'HTTP_AUTHORIZATION' => auth do |response|
 93 |       response.must_be :client_error?
 94 |       response.status.must_equal 400
 95 |       response.wont_include 'www-authenticate'
 96 |     end
 97 |   end
 98 | 
 99 |   it 'takes realm as optional constructor arg' do
100 |     app = Rack::Auth::Basic.new(unprotected_app, realm) { true }
101 |     realm.must_equal app.realm
102 |   end
103 | 
104 |   deprecated "supports #request for a Rack::Request object" do
105 |     Rack::Auth::Basic::Request.new({}).request.must_be_kind_of Rack::Request
106 |   end
107 | end
108 | 


--------------------------------------------------------------------------------
/test/spec_body_proxy.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/body_proxy'
  7 | end
  8 | 
  9 | describe Rack::BodyProxy do
 10 |   it 'call each on the wrapped body' do
 11 |     called = false
 12 |     proxy  = Rack::BodyProxy.new(['foo']) { }
 13 |     proxy.each do |str|
 14 |       called = true
 15 |       str.must_equal 'foo'
 16 |     end
 17 |     called.must_equal true
 18 |   end
 19 | 
 20 |   it 'call close on the wrapped body' do
 21 |     body  = StringIO.new
 22 |     proxy = Rack::BodyProxy.new(body) { }
 23 |     proxy.close
 24 |     body.must_be :closed?
 25 |   end
 26 | 
 27 |   it 'only call close on the wrapped body if it responds to close' do
 28 |     body  = []
 29 |     proxy = Rack::BodyProxy.new(body) { }
 30 |     proxy.close.must_be_nil
 31 |   end
 32 | 
 33 |   it 'call the passed block on close' do
 34 |     called = false
 35 |     proxy  = Rack::BodyProxy.new([]) { called = true }
 36 |     called.must_equal false
 37 |     proxy.close
 38 |     called.must_equal true
 39 |   end
 40 | 
 41 |   it 'call the passed block on close even if there is an exception' do
 42 |     object = Object.new
 43 |     def object.close() raise "No!" end
 44 |     called = false
 45 | 
 46 |     begin
 47 |       proxy = Rack::BodyProxy.new(object) { called = true }
 48 |       called.must_equal false
 49 |       proxy.close
 50 |     rescue RuntimeError => e
 51 |     end
 52 | 
 53 |     raise "Expected exception to have been raised" unless e
 54 |     called.must_equal true
 55 |   end
 56 | 
 57 |   it 'allow multiple arguments in respond_to?' do
 58 |     body  = []
 59 |     proxy = Rack::BodyProxy.new(body) { }
 60 |     proxy.respond_to?(:foo, false).must_equal false
 61 |   end
 62 | 
 63 |   it 'allows #method to work with delegated methods' do
 64 |     body  = Object.new
 65 |     def body.banana; :pear end
 66 |     proxy = Rack::BodyProxy.new(body) { }
 67 |     proxy.method(:banana).call.must_equal :pear
 68 |   end
 69 | 
 70 |   it 'allows calling delegated methods with keywords' do
 71 |     body  = Object.new
 72 |     def body.banana(foo: nil); foo end
 73 |     proxy = Rack::BodyProxy.new(body) { }
 74 |     proxy.banana(foo: 1).must_equal 1
 75 |   end
 76 | 
 77 |   it 'respond to :to_ary if body does responds to it, and have to_ary call close' do
 78 |     proxy_closed = false
 79 |     proxy = Rack::BodyProxy.new([]) { proxy_closed = true }
 80 |     proxy.respond_to?(:to_ary).must_equal true
 81 |     proxy_closed.must_equal false
 82 |     proxy.to_ary.must_equal []
 83 |     proxy_closed.must_equal true
 84 |   end
 85 | 
 86 |   it 'not respond to :to_ary if body does not respond to it' do
 87 |     proxy = Rack::BodyProxy.new([].map) { }
 88 |     proxy.respond_to?(:to_ary).must_equal false
 89 |     proc do
 90 |       proxy.to_ary
 91 |     end.must_raise NoMethodError
 92 |   end
 93 | 
 94 |   it 'not respond to :to_str' do
 95 |     proxy = Rack::BodyProxy.new("string body") { }
 96 |     proxy.respond_to?(:to_str).must_equal false
 97 |     proc do
 98 |       proxy.to_str
 99 |     end.must_raise NoMethodError
100 |   end
101 | 
102 |   it 'not respond to :to_path if body does not respond to it' do
103 |     proxy = Rack::BodyProxy.new("string body") { }
104 |     proxy.respond_to?(:to_path).must_equal false
105 |     proc do
106 |       proxy.to_path
107 |     end.must_raise NoMethodError
108 |   end
109 | 
110 |   it 'not close more than one time' do
111 |     count = 0
112 |     proxy = Rack::BodyProxy.new([]) { count += 1; raise "Block invoked more than 1 time!" if count > 1 }
113 |     2.times { proxy.close }
114 |     count.must_equal 1
115 |   end
116 | 
117 |   it 'be closed when the callback is triggered' do
118 |     closed = false
119 |     proxy = Rack::BodyProxy.new([]) { closed = proxy.closed? }
120 |     proxy.close
121 |     closed.must_equal true
122 |   end
123 | end
124 | 


--------------------------------------------------------------------------------
/test/spec_cascade.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/cascade'
  7 |   require_relative '../lib/rack/lint'
  8 |   require_relative '../lib/rack/mock_request'
  9 |   require_relative '../lib/rack/urlmap'
 10 |   require_relative '../lib/rack/files'
 11 | end
 12 | 
 13 | describe Rack::Cascade do
 14 |   def cascade(*args)
 15 |     Rack::Lint.new Rack::Cascade.new(*args)
 16 |   end
 17 | 
 18 |   docroot = File.expand_path(File.dirname(__FILE__))
 19 |   app1 = Rack::Files.new(docroot)
 20 | 
 21 |   app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" })
 22 | 
 23 |   app3 = Rack::URLMap.new("/foo" => lambda { |env|
 24 |                             [200, { "content-type" => "text/plain" }, [""]]})
 25 | 
 26 |   it "dispatch onward on 404 and 405 by default" do
 27 |     cascade = cascade([app1, app2, app3])
 28 |     Rack::MockRequest.new(cascade).get("/cgi/test").must_be :ok?
 29 |     Rack::MockRequest.new(cascade).get("/foo").must_be :ok?
 30 |     Rack::MockRequest.new(cascade).get("/toobad").must_be :not_found?
 31 |     Rack::MockRequest.new(cascade).get("/cgi/../..").must_be :client_error?
 32 | 
 33 |     # Put is not allowed by Rack::Files so it'll 405.
 34 |     Rack::MockRequest.new(cascade).put("/foo").must_be :ok?
 35 |   end
 36 | 
 37 |   it "dispatch onward on whatever is passed" do
 38 |     cascade = cascade([app1, app2, app3], [404, 403])
 39 |     Rack::MockRequest.new(cascade).get("/cgi/../bla").must_be :not_found?
 40 |   end
 41 | 
 42 |   it "include? returns whether app is included" do
 43 |     cascade = Rack::Cascade.new([app1, app2])
 44 |     cascade.include?(app1).must_equal true
 45 |     cascade.include?(app2).must_equal true
 46 |     cascade.include?(app3).must_equal false
 47 |   end
 48 | 
 49 |   it "return 404 if empty" do
 50 |     Rack::MockRequest.new(cascade([])).get('/').must_be :not_found?
 51 |   end
 52 | 
 53 |   it "uses new response object if empty" do
 54 |     app = Rack::Cascade.new([])
 55 |     res = app.call('/')
 56 |     s, h, body = res
 57 |     s.must_equal 404
 58 |     h['content-type'].must_equal 'text/plain'
 59 |     body.must_be_empty
 60 | 
 61 |     res[0] = 200
 62 |     h['content-type'] = 'text/html'
 63 |     body << "a"
 64 | 
 65 |     res = app.call('/')
 66 |     s, h, body = res
 67 |     s.must_equal 404
 68 |     h['content-type'].must_equal 'text/plain'
 69 |     body.must_be_empty
 70 |   end
 71 | 
 72 |   it "returns final response if all responses are cascaded" do
 73 |    app = Rack::Cascade.new([])
 74 |    app << lambda { |env| [405, {}, []] }
 75 |    app.call({})[0].must_equal 405
 76 |   end
 77 | 
 78 |   it "append new app" do
 79 |     cascade = Rack::Cascade.new([], [404, 403])
 80 |     Rack::MockRequest.new(cascade).get('/').must_be :not_found?
 81 |     cascade << app2
 82 |     Rack::MockRequest.new(cascade).get('/cgi/test').must_be :not_found?
 83 |     Rack::MockRequest.new(cascade).get('/cgi/../bla').must_be :not_found?
 84 |     cascade << app1
 85 |     Rack::MockRequest.new(cascade).get('/cgi/test').must_be :ok?
 86 |     Rack::MockRequest.new(cascade).get('/cgi/../..').must_be :client_error?
 87 |     Rack::MockRequest.new(cascade).get('/foo').must_be :not_found?
 88 |     cascade << app3
 89 |     Rack::MockRequest.new(cascade).get('/foo').must_be :ok?
 90 |   end
 91 | 
 92 |   it "close the body on cascade" do
 93 |     body = StringIO.new
 94 |     closer = lambda { |env| [404, {}, body] }
 95 |     cascade = Rack::Cascade.new([closer, app3], [404])
 96 |     Rack::MockRequest.new(cascade).get("/foo").must_be :ok?
 97 |     body.must_be :closed?
 98 |   end
 99 | end
100 | 


--------------------------------------------------------------------------------
/test/spec_common_logger.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | require 'logger'
  5 | 
  6 | separate_testing do
  7 |   require_relative '../lib/rack/common_logger'
  8 |   require_relative '../lib/rack/lint'
  9 |   require_relative '../lib/rack/mock_request'
 10 | end
 11 | 
 12 | describe Rack::CommonLogger do
 13 |   obj = 'foobar'
 14 |   length = obj.size
 15 | 
 16 |   app = Rack::Lint.new lambda { |env|
 17 |     [200,
 18 |      { "content-type" => "text/html", "content-length" => length.to_s },
 19 |      [obj]]}
 20 |   app_without_length = Rack::Lint.new lambda { |env|
 21 |     [200,
 22 |      { "content-type" => "text/html" },
 23 |      []]}
 24 |   app_with_zero_length = Rack::Lint.new lambda { |env|
 25 |     [200,
 26 |      { "content-type" => "text/html", "content-length" => "0" },
 27 |      []]}
 28 |   app_without_lint = lambda { |env|
 29 |     [200,
 30 |      { "content-type" => "text/html", "content-length" => length.to_s },
 31 |      [obj]]}
 32 | 
 33 |   it "log to rack.errors by default" do
 34 |     res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/")
 35 | 
 36 |     res.errors.wont_be :empty?
 37 |     res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /)
 38 |   end
 39 | 
 40 |   it "log to anything with +write+" do
 41 |     log = StringIO.new
 42 |     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
 43 | 
 44 |     log.string.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /)
 45 |   end
 46 | 
 47 |   it "work with standard library logger" do
 48 |     logdev = StringIO.new
 49 |     log = Logger.new(logdev)
 50 |     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
 51 | 
 52 |     logdev.string.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /)
 53 |   end
 54 | 
 55 |   it "log - content length if header is missing" do
 56 |     res = Rack::MockRequest.new(Rack::CommonLogger.new(app_without_length)).get("/")
 57 | 
 58 |     res.errors.wont_be :empty?
 59 |     res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 - /)
 60 |   end
 61 | 
 62 |   it "log - content length if header is zero" do
 63 |     res = Rack::MockRequest.new(Rack::CommonLogger.new(app_with_zero_length)).get("/")
 64 | 
 65 |     res.errors.wont_be :empty?
 66 |     res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 - /)
 67 |   end
 68 | 
 69 |   it "log - records host from X-Forwarded-For header" do
 70 |     res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/", 'HTTP_X_FORWARDED_FOR' => '203.0.113.0')
 71 | 
 72 |     res.errors.wont_be :empty?
 73 |     res.errors.must_match(/203\.0\.113\.0 - /)
 74 |   end
 75 | 
 76 |   it "log - records host from RFC 7239 forwarded for header" do
 77 |     res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/", 'HTTP_FORWARDED' => 'for=203.0.113.0')
 78 | 
 79 |     res.errors.wont_be :empty?
 80 |     res.errors.must_match(/203\.0\.113\.0 - /)
 81 |   end
 82 | 
 83 |   def with_mock_time(t = 0)
 84 |     mc = class << Time; self; end
 85 |     mc.send :alias_method, :old_now, :now
 86 |     mc.send :define_method, :now do
 87 |       at(t)
 88 |     end
 89 |     yield
 90 |   ensure
 91 |     mc.send :undef_method, :now
 92 |     mc.send :alias_method, :now, :old_now
 93 |   end
 94 | 
 95 |   it "log in common log format" do
 96 |     log = StringIO.new
 97 |     with_mock_time do
 98 |       Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/", 'QUERY_STRING' => 'foo=bar')
 99 |     end
100 | 
101 |     md = /- - - \[([^\]]+)\] "(\w+) \/\?foo=bar HTTP\/1\.1" (\d{3}) \d+ ([\d\.]+)/.match(log.string)
102 |     md.wont_equal nil
103 |     time, method, status, duration = *md.captures
104 |     time.must_equal Time.at(0).strftime("%d/%b/%Y:%H:%M:%S %z")
105 |     method.must_equal "GET"
106 |     status.must_equal "200"
107 |     (0..1).must_include duration.to_f
108 |   end
109 | 
110 |   it "escapes non printable characters including newline" do
111 |     logdev = StringIO.new
112 |     log = Logger.new(logdev)
113 |     Rack::MockRequest.new(Rack::CommonLogger.new(app_without_lint, log)).request("GET\x1f", "/hello")
114 | 
115 |     logdev.string.must_match(/GET\\x1f \/hello HTTP\/1\.1/)
116 | 
117 |     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/", 'REMOTE_USER' => "foo\nbar", "QUERY_STRING" => "bar\nbaz")
118 |     logdev.string[-1].must_equal "\n"
119 |     logdev.string.must_include("foo\\xabar")
120 |     logdev.string.must_include("bar\\xabaz")
121 |   end
122 | 
123 |   it "log path with PATH_INFO" do
124 |     logdev = StringIO.new
125 |     log = Logger.new(logdev)
126 |     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/hello")
127 | 
128 |     logdev.string.must_match(/"GET \/hello HTTP\/1\.1" 200 #{length} /)
129 |   end
130 | 
131 |   it "log path with SCRIPT_NAME" do
132 |     logdev = StringIO.new
133 |     log = Logger.new(logdev)
134 |     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/path", script_name: "/script")
135 | 
136 |     logdev.string.must_match(/"GET \/script\/path HTTP\/1\.1" 200 #{length} /)
137 |   end
138 | 
139 |   it "log path with SERVER_PROTOCOL" do
140 |     logdev = StringIO.new
141 |     log = Logger.new(logdev)
142 |     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/path", http_version: "HTTP/1.0")
143 | 
144 |     logdev.string.must_match(/"GET \/path HTTP\/1\.0" 200 #{length} /)
145 |   end
146 | 
147 |   def length
148 |     123
149 |   end
150 | 
151 |   def self.obj
152 |     "hello world"
153 |   end
154 | end
155 | 


--------------------------------------------------------------------------------
/test/spec_conditional_get.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | require 'time'
  5 | 
  6 | separate_testing do
  7 |   require_relative '../lib/rack/conditional_get'
  8 |   require_relative '../lib/rack/lint'
  9 |   require_relative '../lib/rack/mock_request'
 10 | end
 11 | 
 12 | describe Rack::ConditionalGet do
 13 |   def conditional_get(app)
 14 |     Rack::Lint.new Rack::ConditionalGet.new(app)
 15 |   end
 16 | 
 17 |   it "set a 304 status and truncate body when if-modified-since hits" do
 18 |     timestamp = Time.now.httpdate
 19 |     app = conditional_get(lambda { |env|
 20 |       [200, { 'last-modified' => timestamp }, ['TEST']] })
 21 | 
 22 |     response = Rack::MockRequest.new(app).
 23 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp)
 24 | 
 25 |     response.status.must_equal 304
 26 |     response.body.must_be :empty?
 27 |   end
 28 | 
 29 |   it "set a 304 status and truncate body when if-modified-since hits and is higher than current time" do
 30 |     app = conditional_get(lambda { |env|
 31 |       [200, { 'last-modified' => (Time.now - 3600).httpdate }, ['TEST']] })
 32 | 
 33 |     response = Rack::MockRequest.new(app).
 34 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
 35 | 
 36 |     response.status.must_equal 304
 37 |     response.body.must_be :empty?
 38 |   end
 39 | 
 40 |   it "closes bodies" do
 41 |     body = Object.new
 42 |     def body.each; yield 'TEST' end
 43 |     closed = false
 44 |     body.define_singleton_method(:close){closed = true}
 45 |     app = conditional_get(lambda { |env|
 46 |       [200, { 'last-modified' => (Time.now - 3600).httpdate }, body] })
 47 | 
 48 |     response = Rack::MockRequest.new(app).
 49 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
 50 | 
 51 |     response.status.must_equal 304
 52 |     response.body.must_be :empty?
 53 |     closed.must_equal true
 54 |   end
 55 | 
 56 |   it "set a 304 status and truncate body when if-none-match hits" do
 57 |     app = conditional_get(lambda { |env|
 58 |       [200, { 'etag' => '1234' }, ['TEST']] })
 59 | 
 60 |     response = Rack::MockRequest.new(app).
 61 |       get("/", 'HTTP_IF_NONE_MATCH' => '1234')
 62 | 
 63 |     response.status.must_equal 304
 64 |     response.body.must_be :empty?
 65 |   end
 66 | 
 67 |   it "set a 304 status and truncate body when if-none-match hits but if-modified-since is after last-modified" do
 68 |     app = conditional_get(lambda { |env|
 69 |       [200, { 'last-modified' => (Time.now + 3600).httpdate, 'etag' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
 70 | 
 71 |     response = Rack::MockRequest.new(app).
 72 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate, 'HTTP_IF_NONE_MATCH' => '1234')
 73 | 
 74 |     response.status.must_equal 304
 75 |     response.body.must_be :empty?
 76 |   end
 77 | 
 78 |   it "not set a 304 status if last-modified is too short" do
 79 |     app = conditional_get(lambda { |env|
 80 |       [200, { 'last-modified' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
 81 | 
 82 |     response = Rack::MockRequest.new(app).
 83 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
 84 | 
 85 |     response.status.must_equal 200
 86 |     response.body.must_equal 'TEST'
 87 |   end
 88 | 
 89 |   it "not set a 304 status if if-modified-since hits but etag does not" do
 90 |     timestamp = Time.now.httpdate
 91 |     app = conditional_get(lambda { |env|
 92 |       [200, { 'last-modified' => timestamp, 'etag' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
 93 | 
 94 |     response = Rack::MockRequest.new(app).
 95 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '4321')
 96 | 
 97 |     response.status.must_equal 200
 98 |     response.body.must_equal 'TEST'
 99 |   end
100 | 
101 |   it "set a 304 status and truncate body when both if-none-match and if-modified-since hits" do
102 |     timestamp = Time.now.httpdate
103 |     app = conditional_get(lambda { |env|
104 |       [200, { 'last-modified' => timestamp, 'etag' => '1234' }, ['TEST']] })
105 | 
106 |     response = Rack::MockRequest.new(app).
107 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '1234')
108 | 
109 |     response.status.must_equal 304
110 |     response.body.must_be :empty?
111 |   end
112 | 
113 |   it "not affect non-GET/HEAD requests" do
114 |     app = conditional_get(lambda { |env|
115 |       [200, { 'etag' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
116 | 
117 |     response = Rack::MockRequest.new(app).
118 |       post("/", 'HTTP_IF_NONE_MATCH' => '1234')
119 | 
120 |     response.status.must_equal 200
121 |     response.body.must_equal 'TEST'
122 |   end
123 | 
124 |   it "not affect non-200 requests" do
125 |     app = conditional_get(lambda { |env|
126 |       [302, { 'etag' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
127 | 
128 |     response = Rack::MockRequest.new(app).
129 |       get("/", 'HTTP_IF_NONE_MATCH' => '1234')
130 | 
131 |     response.status.must_equal 302
132 |     response.body.must_equal 'TEST'
133 |   end
134 | 
135 |   it "not affect requests with malformed HTTP_IF_NONE_MATCH" do
136 |     bad_timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
137 |     app = conditional_get(lambda { |env|
138 |       [200, { 'last-modified' => (Time.now - 3600).httpdate, 'content-type' => 'text/plain' }, ['TEST']] })
139 | 
140 |     response = Rack::MockRequest.new(app).
141 |       get("/", 'HTTP_IF_MODIFIED_SINCE' => bad_timestamp)
142 | 
143 |     response.status.must_equal 200
144 |     response.body.must_equal 'TEST'
145 |   end
146 | 
147 | end
148 | 


--------------------------------------------------------------------------------
/test/spec_config.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/config'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/builder'
 9 |   require_relative '../lib/rack/mock_request'
10 | end
11 | 
12 | describe Rack::Config do
13 |   it "accept a block that modifies the environment" do
14 |     app = Rack::Builder.new do
15 |       use Rack::Lint
16 |       use Rack::Config do |env|
17 |         env['greeting'] = 'hello'
18 |       end
19 |       run lambda { |env|
20 |         [200, { 'content-type' => 'text/plain' }, [env['greeting'] || '']]
21 |       }
22 |     end
23 | 
24 |     response = Rack::MockRequest.new(app).get('/')
25 |     response.body.must_equal 'hello'
26 |   end
27 | end
28 | 


--------------------------------------------------------------------------------
/test/spec_content_length.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/content_length'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/mock_request'
 9 | end
10 | 
11 | describe Rack::ContentLength do
12 |   def content_length(app)
13 |     Rack::Lint.new Rack::ContentLength.new(app)
14 |   end
15 | 
16 |   def request
17 |     Rack::MockRequest.env_for
18 |   end
19 | 
20 |   it "set content-length on Array bodies if none is set" do
21 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
22 |     response = content_length(app).call(request)
23 |     response[1]['content-length'].must_equal '13'
24 |   end
25 | 
26 |   it "not set content-length on variable length bodies" do
27 |     body = lambda { "Hello World!" }
28 |     def body.each ; yield call ; end
29 | 
30 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
31 |     response = content_length(app).call(request)
32 |     response[1]['content-length'].must_be_nil
33 |   end
34 | 
35 |   it "not change content-length if it is already set" do
36 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', 'content-length' => '1' }, "Hello, World!"] }
37 |     response = content_length(app).call(request)
38 |     response[1]['content-length'].must_equal '1'
39 |   end
40 | 
41 |   it "not set content-length on 304 responses" do
42 |     app = lambda { |env| [304, {}, []] }
43 |     response = content_length(app).call(request)
44 |     response[1]['content-length'].must_be_nil
45 |   end
46 | 
47 |   it "not set content-length when transfer-encoding is chunked" do
48 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', 'transfer-encoding' => 'chunked' }, []] }
49 |     response = content_length(app).call(request)
50 |     response[1]['content-length'].must_be_nil
51 |   end
52 | 
53 |   # Using "Connection: close" for this is fairly contended. It might be useful
54 |   # to have some other way to signal this.
55 |   #
56 |   # should "not force a content-length when Connection:close" do
57 |   #   app = lambda { |env| [200, {'Connection' => 'close'}, []] }
58 |   #   response = content_length(app).call({})
59 |   #   response[1]['content-length'].must_be_nil
60 |   # end
61 | 
62 |   it "close bodies that need to be closed" do
63 |     body = Struct.new(:body) do
64 |       attr_reader :closed
65 |       def each; body.each {|b| yield b}; close; end
66 |       def close; @closed = true; end
67 |       def to_ary; enum_for.to_a; end
68 |     end.new(%w[one two three])
69 | 
70 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
71 |     content_length(app).call(request)
72 |     body.closed.must_equal true
73 |   end
74 | 
75 |   it "support single-execute bodies" do
76 |     body = Struct.new(:body) do
77 |       def each
78 |         yield body.shift until body.empty?
79 |       end
80 |       def to_ary; enum_for.to_a; end
81 |     end.new(%w[one two three])
82 | 
83 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
84 |     response = content_length(app).call(request)
85 |     expected = %w[one two three]
86 |     response[1]['content-length'].must_equal expected.join.size.to_s
87 |     response[2].to_enum.to_a.must_equal expected
88 |   end
89 | end
90 | 


--------------------------------------------------------------------------------
/test/spec_content_type.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/content_type'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/mock_request'
 9 | end
10 | 
11 | describe Rack::ContentType do
12 |   def content_type(app, *args)
13 |     Rack::Lint.new Rack::ContentType.new(app, *args)
14 |   end
15 | 
16 |   def request
17 |     Rack::MockRequest.env_for
18 |   end
19 | 
20 |   it "set content-type to default text/html if none is set" do
21 |     app = lambda { |env| [200, {}, "Hello, World!"] }
22 |     headers = content_type(app).call(request)[1]
23 |     headers['content-type'].must_equal 'text/html'
24 |   end
25 | 
26 |   it "set content-type to chosen default if none is set" do
27 |     app = lambda { |env| [200, {}, "Hello, World!"] }
28 |     headers =
29 |       content_type(app, 'application/octet-stream').call(request)[1]
30 |     headers['content-type'].must_equal 'application/octet-stream'
31 |   end
32 | 
33 |   it "not change content-type if it is already set" do
34 |     app = lambda { |env| [200, { 'content-type' => 'foo/bar' }, "Hello, World!"] }
35 |     headers = content_type(app).call(request)[1]
36 |     headers['content-type'].must_equal 'foo/bar'
37 |   end
38 | 
39 |   [100, 204, 304].each do |code|
40 |     it "not set content-type on #{code} responses" do
41 |       app = lambda { |env| [code, {}, []] }
42 |       response = content_type(app, "text/html").call(request)
43 |       response[1]['content-type'].must_be_nil
44 |     end
45 |   end
46 | end
47 | 


--------------------------------------------------------------------------------
/test/spec_etag.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | require 'time'
  5 | 
  6 | separate_testing do
  7 |   require_relative '../lib/rack/etag'
  8 |   require_relative '../lib/rack/lint'
  9 |   require_relative '../lib/rack/mock_request'
 10 | end
 11 | 
 12 | describe Rack::ETag do
 13 |   def etag(app, *args)
 14 |     Rack::Lint.new Rack::ETag.new(app, *args)
 15 |   end
 16 | 
 17 |   def request
 18 |     Rack::MockRequest.env_for
 19 |   end
 20 | 
 21 |   def sendfile_body
 22 |     File.new(File::NULL)
 23 |   end
 24 | 
 25 |   it "set etag if none is set if status is 200" do
 26 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
 27 |     response = etag(app).call(request)
 28 |     response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
 29 |   end
 30 | 
 31 |   it "returns a valid response body when using a linted app" do
 32 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
 33 |     response = etag(Rack::Lint.new(app)).call(request)
 34 |     response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
 35 | 
 36 |     response[2].each do |chunk|
 37 |       chunk.must_equal "Hello, World!"
 38 |     end
 39 |   end
 40 | 
 41 |   it "set etag if none is set if status is 201" do
 42 |     app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
 43 |     response = etag(app).call(request)
 44 |     response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
 45 |   end
 46 | 
 47 |   it "set cache-control to 'max-age=0, private, must-revalidate' (default) if none is set" do
 48 |     app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
 49 |     response = etag(app).call(request)
 50 |     response[1]['cache-control'].must_equal 'max-age=0, private, must-revalidate'
 51 |   end
 52 | 
 53 |   it "set cache-control to chosen one if none is set" do
 54 |     app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
 55 |     response = etag(app, nil, 'public').call(request)
 56 |     response[1]['cache-control'].must_equal 'public'
 57 |   end
 58 | 
 59 |   it "set a given cache-control even if digest could not be calculated" do
 60 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, []] }
 61 |     response = etag(app, 'no-cache').call(request)
 62 |     response[1]['cache-control'].must_equal 'no-cache'
 63 |   end
 64 | 
 65 |   it "not set cache-control if it is already set" do
 66 |     app = lambda { |env| [201, { 'content-type' => 'text/plain', 'cache-control' => 'public' }, ["Hello, World!"]] }
 67 |     response = etag(app).call(request)
 68 |     response[1]['cache-control'].must_equal 'public'
 69 |   end
 70 | 
 71 |   it "not set cache-control if directive isn't present" do
 72 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
 73 |     response = etag(app, nil, nil).call(request)
 74 |     response[1]['cache-control'].must_be_nil
 75 |   end
 76 | 
 77 |   it "not change etag if it is already set" do
 78 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', 'etag' => '"abc"' }, ["Hello, World!"]] }
 79 |     response = etag(app).call(request)
 80 |     response[1]['etag'].must_equal "\"abc\""
 81 |   end
 82 | 
 83 |   it "not set etag if body is empty" do
 84 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', 'last-modified' => Time.now.httpdate }, []] }
 85 |     response = etag(app).call(request)
 86 |     response[1]['etag'].must_be_nil
 87 |   end
 88 | 
 89 |   it "set handle empty body parts" do
 90 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello", "", ", World!"]] }
 91 |     response = etag(app).call(request)
 92 |     response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
 93 |   end
 94 | 
 95 |   it "not set etag if last-modified is set" do
 96 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', 'last-modified' => Time.now.httpdate }, ["Hello, World!"]] }
 97 |     response = etag(app).call(request)
 98 |     response[1]['etag'].must_be_nil
 99 |   end
100 | 
101 |   it "not set etag if a sendfile_body is given" do
102 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, sendfile_body] }
103 |     response = etag(app).call(request)
104 |     response[1]['etag'].must_be_nil
105 |   end
106 | 
107 |   it "not set etag if a status is not 200 or 201" do
108 |     app = lambda { |env| [401, { 'content-type' => 'text/plain' }, ['Access denied.']] }
109 |     response = etag(app).call(request)
110 |     response[1]['etag'].must_be_nil
111 |   end
112 | 
113 |   it "set etag even if no-cache is given" do
114 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', 'cache-control' => 'no-cache, must-revalidate' }, ['Hello, World!']] }
115 |     response = etag(app).call(request)
116 |     response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
117 |   end
118 | 
119 |   it "close the original body" do
120 |     body = StringIO.new
121 |     app = lambda { |env| [200, {}, body] }
122 |     response = etag(app).call(request)
123 |     body.wont_be :closed?
124 |     response[2].close
125 |     body.must_be :closed?
126 |   end
127 | end
128 | 


--------------------------------------------------------------------------------
/test/spec_events.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/events'
  7 | end
  8 | 
  9 | module Rack
 10 |   class TestEvents < Minitest::Test
 11 |     class EventMiddleware
 12 |       attr_reader :events
 13 | 
 14 |       def initialize(events)
 15 |         @events = events
 16 |       end
 17 | 
 18 |       def on_start(req, res)
 19 |         events << [self, __method__]
 20 |       end
 21 | 
 22 |       def on_commit(req, res)
 23 |         events << [self, __method__]
 24 |       end
 25 | 
 26 |       def on_send(req, res)
 27 |         events << [self, __method__]
 28 |       end
 29 | 
 30 |       def on_finish(req, res)
 31 |         events << [self, __method__]
 32 |       end
 33 | 
 34 |       def on_error(req, res, e)
 35 |         events << [self, __method__]
 36 |       end
 37 |     end
 38 | 
 39 |     def test_events_fire
 40 |       events = []
 41 |       ret = [200, {}, []]
 42 |       app = lambda { |env| events << [app, :call]; ret }
 43 |       se = EventMiddleware.new events
 44 |       e = Events.new app, [se]
 45 |       triple = e.call({})
 46 |       response_body = []
 47 |       triple[2].each { |x| response_body << x }
 48 |       triple[2].close
 49 |       triple[2] = response_body
 50 |       assert_equal ret, triple
 51 |       assert_equal [[se, :on_start],
 52 |                     [app, :call],
 53 |                     [se, :on_commit],
 54 |                     [se, :on_send],
 55 |                     [se, :on_finish],
 56 |       ], events
 57 |     end
 58 | 
 59 |     def test_send_and_finish_are_not_run_until_body_is_sent
 60 |       events = []
 61 |       ret = [200, {}, []]
 62 |       app = lambda { |env| events << [app, :call]; ret }
 63 |       se = EventMiddleware.new events
 64 |       e = Events.new app, [se]
 65 |       e.call({})
 66 |       assert_equal [[se, :on_start],
 67 |                     [app, :call],
 68 |                     [se, :on_commit],
 69 |       ], events
 70 |     end
 71 | 
 72 |     def test_send_is_called_on_each
 73 |       events = []
 74 |       ret = [200, {}, []]
 75 |       app = lambda { |env| events << [app, :call]; ret }
 76 |       se = EventMiddleware.new events
 77 |       e = Events.new app, [se]
 78 |       triple = e.call({})
 79 |       triple[2].each { |x| }
 80 |       assert_equal [[se, :on_start],
 81 |                     [app, :call],
 82 |                     [se, :on_commit],
 83 |                     [se, :on_send],
 84 |       ], events
 85 |     end
 86 | 
 87 |     def test_finish_is_called_on_close
 88 |       events = []
 89 |       ret = [200, {}, []]
 90 |       app = lambda { |env| events << [app, :call]; ret }
 91 |       se = EventMiddleware.new events
 92 |       e = Events.new app, [se]
 93 |       triple = e.call({})
 94 |       triple[2].each { |x| }
 95 |       triple[2].close
 96 |       assert_equal [[se, :on_start],
 97 |                     [app, :call],
 98 |                     [se, :on_commit],
 99 |                     [se, :on_send],
100 |                     [se, :on_finish],
101 |       ], events
102 |     end
103 | 
104 |     def test_finish_is_called_in_reverse_order
105 |       events = []
106 |       ret = [200, {}, []]
107 |       app = lambda { |env| events << [app, :call]; ret }
108 |       se1 = EventMiddleware.new events
109 |       se2 = EventMiddleware.new events
110 |       se3 = EventMiddleware.new events
111 | 
112 |       e = Events.new app, [se1, se2, se3]
113 |       triple = e.call({})
114 |       triple[2].each { |x| }
115 |       triple[2].close
116 | 
117 |       groups = events.group_by { |x| x.last }
118 |       assert_equal groups[:on_start].map(&:first), groups[:on_finish].map(&:first).reverse
119 |       assert_equal groups[:on_commit].map(&:first), groups[:on_finish].map(&:first)
120 |       assert_equal groups[:on_send].map(&:first), groups[:on_finish].map(&:first)
121 |     end
122 | 
123 |     def test_finish_is_called_if_there_is_an_exception
124 |       events = []
125 |       app = lambda { |env| raise }
126 |       se = EventMiddleware.new events
127 |       e = Events.new app, [se]
128 |       assert_raises(RuntimeError) do
129 |         e.call({})
130 |       end
131 |       assert_equal [[se, :on_start],
132 |                     [se, :on_error],
133 |                     [se, :on_finish],
134 |       ], events
135 |     end
136 |   end
137 | end
138 | 


--------------------------------------------------------------------------------
/test/spec_head.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/head'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/mock_request'
 9 | end
10 | 
11 | describe Rack::Head do
12 | 
13 |   def test_response(headers = {})
14 |     body = StringIO.new "foo"
15 |     app = lambda do |env|
16 |       [200, { "content-type" => "test/plain", "content-length" => "3" }, body]
17 |     end
18 |     request = Rack::MockRequest.env_for("/", headers)
19 |     response = Rack::Lint.new(Rack::Head.new(app)).call(request)
20 | 
21 |     return response, body
22 |   end
23 | 
24 |   it "pass GET, POST, PUT, DELETE, OPTIONS, TRACE requests" do
25 |     %w[GET POST PUT DELETE OPTIONS TRACE].each do |type|
26 |       resp, _ = test_response("REQUEST_METHOD" => type)
27 | 
28 |       resp[0].must_equal 200
29 |       resp[1].must_equal "content-type" => "test/plain", "content-length" => "3"
30 |       resp[2].to_enum.to_a.must_equal ["foo"]
31 |     end
32 |   end
33 | 
34 |   it "remove body from HEAD requests" do
35 |     resp, _ = test_response("REQUEST_METHOD" => "HEAD")
36 | 
37 |     resp[0].must_equal 200
38 |     resp[1].must_equal "content-type" => "test/plain", "content-length" => "3"
39 |     resp[2].to_enum.to_a.must_equal []
40 |   end
41 | 
42 |   it "close the body when it is removed" do
43 |     resp, body = test_response("REQUEST_METHOD" => "HEAD")
44 |     resp[0].must_equal 200
45 |     resp[1].must_equal "content-type" => "test/plain", "content-length" => "3"
46 |     resp[2].to_enum.to_a.must_equal []
47 |     body.wont_be :closed?
48 |     resp[2].close
49 |     body.must_be :closed?
50 |   end
51 | end
52 | 


--------------------------------------------------------------------------------
/test/spec_lock.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/lock'
  7 |   require_relative '../lib/rack/mock_request'
  8 |   require_relative '../lib/rack/lint'
  9 | end
 10 | 
 11 | class Lock
 12 |   attr_reader :synchronized
 13 | 
 14 |   def initialize
 15 |     @synchronized = false
 16 |   end
 17 | 
 18 |   def lock
 19 |     @synchronized = true
 20 |   end
 21 | 
 22 |   def unlock
 23 |     @synchronized = false
 24 |   end
 25 | end
 26 | 
 27 | module LockHelpers
 28 |   def lock_app(app, lock = Lock.new)
 29 |     app = if lock
 30 |       Rack::Lock.new app, lock
 31 |     else
 32 |       Rack::Lock.new app
 33 |     end
 34 |     Rack::Lint.new app
 35 |   end
 36 | end
 37 | 
 38 | describe Rack::Lock do
 39 |   include LockHelpers
 40 | 
 41 |   describe 'Proxy' do
 42 |     include LockHelpers
 43 | 
 44 |     it 'delegate each' do
 45 |       env      = Rack::MockRequest.env_for("/")
 46 |       response = Class.new {
 47 |         attr_accessor :close_called
 48 |         def initialize; @close_called = false; end
 49 |         def each; %w{ hi mom }.each { |x| yield x }; end
 50 |       }.new
 51 | 
 52 |       app = lock_app(lambda { |inner_env| [200, { "content-type" => "text/plain" }, response] })
 53 |       response = app.call(env)[2]
 54 |       list = []
 55 |       response.each { |x| list << x }
 56 |       list.must_equal %w{ hi mom }
 57 |     end
 58 | 
 59 |     it 'delegate to_path' do
 60 |       lock = Lock.new
 61 |       env  = Rack::MockRequest.env_for("/")
 62 | 
 63 |       res = ['Hello World']
 64 |       def res.to_path ; "/tmp/hello.txt" ; end
 65 | 
 66 |       app = Rack::Lock.new(lambda { |inner_env| [200, { "content-type" => "text/plain" }, res] }, lock)
 67 |       body = app.call(env)[2]
 68 | 
 69 |       body.must_respond_to :to_path
 70 |       body.to_path.must_equal "/tmp/hello.txt"
 71 |     end
 72 | 
 73 |     it 'not delegate to_path if body does not implement it' do
 74 |       env = Rack::MockRequest.env_for("/")
 75 | 
 76 |       res = ['Hello World']
 77 | 
 78 |       app = lock_app(lambda { |inner_env| [200, { "content-type" => "text/plain" }, res] })
 79 |       body = app.call(env)[2]
 80 | 
 81 |       body.wont_respond_to :to_path
 82 |     end
 83 |   end
 84 | 
 85 |   it 'call super on close' do
 86 |     env      = Rack::MockRequest.env_for("/")
 87 |     response = Class.new do
 88 |       attr_accessor :close_called
 89 |       def initialize; @close_called = false; end
 90 |       def close; @close_called = true; end
 91 |     end.new
 92 | 
 93 |     app = lock_app(lambda { |inner_env| [200, { "content-type" => "text/plain" }, response] })
 94 |     app.call(env)
 95 |     response.close_called.must_equal false
 96 |     response.close
 97 |     response.close_called.must_equal true
 98 |   end
 99 | 
100 |   it "not unlock until body is closed" do
101 |     lock     = Lock.new
102 |     env      = Rack::MockRequest.env_for("/")
103 |     response = Object.new
104 |     app      = lock_app(lambda { |inner_env| [200, { "content-type" => "text/plain" }, response] }, lock)
105 |     lock.synchronized.must_equal false
106 |     response = app.call(env)[2]
107 |     lock.synchronized.must_equal true
108 |     response.close
109 |     lock.synchronized.must_equal false
110 |   end
111 | 
112 |   it "return value from app" do
113 |     env  = Rack::MockRequest.env_for("/")
114 |     body = [200, { "content-type" => "text/plain" }, %w{ hi mom }]
115 |     app  = lock_app(lambda { |inner_env| body })
116 | 
117 |     res = app.call(env)
118 |     res[0].must_equal body[0]
119 |     res[1].must_equal body[1]
120 |     res[2].to_enum.to_a.must_equal ["hi", "mom"]
121 |   end
122 | 
123 |   it "call synchronize on lock" do
124 |     lock = Lock.new
125 |     env = Rack::MockRequest.env_for("/")
126 |     app = lock_app(lambda { |inner_env| [200, { "content-type" => "text/plain" }, %w{ a b c }] }, lock)
127 |     lock.synchronized.must_equal false
128 |     app.call(env)
129 |     lock.synchronized.must_equal true
130 |   end
131 | 
132 |   it "unlock if the app raises" do
133 |     lock = Lock.new
134 |     env = Rack::MockRequest.env_for("/")
135 |     app = lock_app(lambda { raise Exception }, lock)
136 |     lambda { app.call(env) }.must_raise Exception
137 |     lock.synchronized.must_equal false
138 |   end
139 | 
140 |   it "unlock if the app throws" do
141 |     lock = Lock.new
142 |     env = Rack::MockRequest.env_for("/")
143 |     app = lock_app(lambda {|_| throw :bacon }, lock)
144 |     lambda { app.call(env) }.must_throw :bacon
145 |     lock.synchronized.must_equal false
146 |   end
147 | 
148 |   it 'not unlock if an error is raised before the mutex is locked' do
149 |     lock = Class.new do
150 |       def initialize() @unlocked = false end
151 |       def unlocked?() @unlocked end
152 |       def lock() raise Exception end
153 |       def unlock() @unlocked = true end
154 |     end.new
155 |     env = Rack::MockRequest.env_for("/")
156 |     app = lock_app(proc { [200, { "content-type" => "text/plain" }, []] }, lock)
157 |     lambda { app.call(env) }.must_raise Exception
158 |     lock.unlocked?.must_equal false
159 |   end
160 | 
161 |   it "unlock if an exception occurs before returning" do
162 |     lock = Lock.new
163 |     env  = Rack::MockRequest.env_for("/")
164 |     app  = lock_app(proc { [].freeze }, lock)
165 |     lambda { app.call(env) }.must_raise Exception
166 |     lock.synchronized.must_equal false
167 |   end
168 | 
169 |   it "not replace the environment" do
170 |     env  = Rack::MockRequest.env_for("/")
171 |     app  = lock_app(lambda { |inner_env| [200, { "content-type" => "text/plain" }, [inner_env.object_id.to_s]] })
172 | 
173 |     _, _, body = app.call(env)
174 | 
175 |     body.to_enum.to_a.must_equal [env.object_id.to_s]
176 |   end
177 | end
178 | 


--------------------------------------------------------------------------------
/test/spec_media_type.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/media_type'
 7 | end
 8 | 
 9 | describe Rack::MediaType do
10 |   before { @empty_hash = {} }
11 | 
12 |   describe 'when content_type nil' do
13 |     before { @content_type = nil }
14 | 
15 |     it '#type is nil' do
16 |       Rack::MediaType.type(@content_type).must_be_nil
17 |     end
18 | 
19 |     it '#params is empty' do
20 |       Rack::MediaType.params(@content_type).must_equal @empty_hash
21 |     end
22 |   end
23 | 
24 |   describe 'when content_type is empty string' do
25 |     before { @content_type = '' }
26 | 
27 |     it '#type is nil' do
28 |       Rack::MediaType.type(@content_type).must_be_nil
29 |     end
30 | 
31 |     it '#params is empty' do
32 |       Rack::MediaType.params(@content_type).must_equal @empty_hash
33 |     end
34 |   end
35 | 
36 |   describe 'when content_type contains only media_type' do
37 |     before { @content_type = 'application/text' }
38 | 
39 |     it '#type is application/text' do
40 |       Rack::MediaType.type(@content_type).must_equal 'application/text'
41 |     end
42 | 
43 |     it '#params is empty' do
44 |       Rack::MediaType.params(@content_type).must_equal @empty_hash
45 |     end
46 |   end
47 | 
48 |   describe 'when content_type contains media_type and params' do
49 |     before { @content_type = 'application/text;CHARSET="utf-8"' }
50 | 
51 |     it '#type is application/text' do
52 |       Rack::MediaType.type(@content_type).must_equal 'application/text'
53 |     end
54 | 
55 |     it '#params has key "charset" with value "utf-8"' do
56 |       Rack::MediaType.params(@content_type)['charset'].must_equal 'utf-8'
57 |     end
58 |   end
59 | 
60 |   describe 'when content_type contains media_type and incomplete params' do 
61 |     before { @content_type = 'application/text;CHARSET' }
62 | 
63 |     it '#type is application/text' do
64 |       Rack::MediaType.type(@content_type).must_equal 'application/text'
65 |     end
66 | 
67 |     it '#params has key "charset" with value ""' do
68 |       Rack::MediaType.params(@content_type)['charset'].must_equal ''
69 |     end
70 |   end
71 | 
72 |   describe 'when content_type contains media_type and empty params' do 
73 |     before { @content_type = 'application/text;CHARSET=' }
74 | 
75 |     it '#type is application/text' do
76 |       Rack::MediaType.type(@content_type).must_equal 'application/text'
77 |     end
78 | 
79 |     it '#params has key "charset" with value of empty string' do
80 |       Rack::MediaType.params(@content_type)['charset'].must_equal ''
81 |     end
82 |   end
83 | end
84 | 


--------------------------------------------------------------------------------
/test/spec_method_override.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/method_override'
  7 |   require_relative '../lib/rack/lint'
  8 |   require_relative '../lib/rack/mock_request'
  9 | end
 10 | 
 11 | describe Rack::MethodOverride do
 12 |   def app
 13 |     Rack::Lint.new(Rack::MethodOverride.new(lambda {|e|
 14 |       [200, { "content-type" => "text/plain" }, []]
 15 |     }))
 16 |   end
 17 | 
 18 |   it "not affect GET requests" do
 19 |     env = Rack::MockRequest.env_for("/?_method=delete", method: "GET")
 20 |     app.call env
 21 | 
 22 |     env["REQUEST_METHOD"].must_equal "GET"
 23 |   end
 24 | 
 25 |   it "sets rack.errors for invalid UTF8 _method values" do
 26 |     errors = StringIO.new
 27 |     env = Rack::MockRequest.env_for("/",
 28 |       :method => "POST",
 29 |       :input => "_method=\xBF".b,
 30 |       Rack::RACK_ERRORS => errors)
 31 | 
 32 |     app.call env
 33 | 
 34 |     errors.rewind
 35 |     errors.read.must_equal "Invalid string for method\n"
 36 |     env["REQUEST_METHOD"].must_equal "POST"
 37 |   end
 38 | 
 39 |   it "modify REQUEST_METHOD for POST requests when _method parameter is set" do
 40 |     env = Rack::MockRequest.env_for("/", method: "POST", input: "_method=put")
 41 |     app.call env
 42 | 
 43 |     env["REQUEST_METHOD"].must_equal "PUT"
 44 |   end
 45 | 
 46 |   it "modify REQUEST_METHOD for POST requests when X-HTTP-Method-Override is set" do
 47 |     env = Rack::MockRequest.env_for("/",
 48 |             :method => "POST",
 49 |             "HTTP_X_HTTP_METHOD_OVERRIDE" => "PATCH"
 50 |           )
 51 |     app.call env
 52 | 
 53 |     env["REQUEST_METHOD"].must_equal "PATCH"
 54 |   end
 55 | 
 56 |   it "not modify REQUEST_METHOD if the method is unknown" do
 57 |     env = Rack::MockRequest.env_for("/", method: "POST", input: "_method=foo")
 58 |     app.call env
 59 | 
 60 |     env["REQUEST_METHOD"].must_equal "POST"
 61 |   end
 62 | 
 63 |   it "not modify REQUEST_METHOD when _method is nil" do
 64 |     env = Rack::MockRequest.env_for("/", method: "POST", input: "foo=bar")
 65 |     app.call env
 66 | 
 67 |     env["REQUEST_METHOD"].must_equal "POST"
 68 |   end
 69 | 
 70 |   it "store the original REQUEST_METHOD prior to overriding" do
 71 |     env = Rack::MockRequest.env_for("/",
 72 |             method: "POST",
 73 |             input: "_method=options")
 74 |     app.call env
 75 | 
 76 |     env["rack.methodoverride.original_method"].must_equal "POST"
 77 |   end
 78 | 
 79 |   it "not modify REQUEST_METHOD when given invalid multipart form data" do
 80 |     input = <<EOF
 81 | --AaB03x\r
 82 | content-disposition: form-data; name="huge"; filename="huge"\r
 83 | EOF
 84 |     env = Rack::MockRequest.env_for("/",
 85 |                       "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
 86 |                       "CONTENT_LENGTH" => input.size.to_s,
 87 |                       :method => "POST", :input => input)
 88 |     app.call env
 89 | 
 90 |     env["REQUEST_METHOD"].must_equal "POST"
 91 |   end
 92 | 
 93 |   it "writes error to RACK_ERRORS when given invalid multipart form data" do
 94 |     input = <<EOF
 95 | --AaB03x\r
 96 | content-disposition: form-data; name="huge"; filename="huge"\r
 97 | EOF
 98 |     env = Rack::MockRequest.env_for("/",
 99 |                       "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
100 |                       "CONTENT_LENGTH" => input.size.to_s,
101 |                       Rack::RACK_ERRORS => StringIO.new,
102 |                       :method => "POST", :input => input)
103 |     Rack::MethodOverride.new(proc { [200, { "content-type" => "text/plain" }, []] }).call env
104 | 
105 |     env[Rack::RACK_ERRORS].rewind
106 |     env[Rack::RACK_ERRORS].read.must_include 'Bad request content body'
107 |   end
108 | 
109 |   it "not modify REQUEST_METHOD for POST requests when the params are unparseable because too deep" do
110 |     env = Rack::MockRequest.env_for("/", method: "POST", input: ("[a]" * 36) + "=1")
111 |     app.call env
112 | 
113 |     env["REQUEST_METHOD"].must_equal "POST"
114 |   end
115 | 
116 |   it "not modify REQUEST_METHOD for POST requests when the params are unparseable" do
117 |     env = Rack::MockRequest.env_for("/", method: "POST", input: "(%bad-params%)")
118 |     app.call env
119 | 
120 |     env["REQUEST_METHOD"].must_equal "POST"
121 |   end
122 | 
123 |   it "not set form input when the content type is JSON" do
124 |     env = Rack::MockRequest.env_for("/",
125 |       "CONTENT_TYPE" => "application/json",
126 |       method: "POST",
127 |       input: '{"_method":"options"}')
128 |     app.call env
129 | 
130 |     env["REQUEST_METHOD"].must_equal "POST"
131 |     env["rack.request.form_input"].must_be_nil
132 |   end
133 | end
134 | 


--------------------------------------------------------------------------------
/test/spec_mime.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/mime'
 7 | end
 8 | 
 9 | describe Rack::Mime do
10 | 
11 |   it "should return the fallback mime-type for files with no extension" do
12 |     fallback = 'image/jpg'
13 |     Rack::Mime.mime_type(File.extname('no_ext'), fallback).must_equal fallback
14 |   end
15 | 
16 |   it "should always return 'application/octet-stream' for unknown file extensions" do
17 |     unknown_ext = File.extname('unknown_ext.abcdefg')
18 |     Rack::Mime.mime_type(unknown_ext).must_equal 'application/octet-stream'
19 |   end
20 | 
21 |   it "should return the mime-type for a given extension" do
22 |     # sanity check. it would be infeasible test every single mime-type.
23 |     Rack::Mime.mime_type(File.extname('image.jpg')).must_equal 'image/jpeg'
24 |   end
25 | 
26 |   it "should support null fallbacks" do
27 |     Rack::Mime.mime_type('.nothing', nil).must_be_nil
28 |   end
29 | 
30 |   it "should match exact mimes" do
31 |     Rack::Mime.match?('text/html', 'text/html').must_equal true
32 |     Rack::Mime.match?('text/html', 'text/meme').must_equal false
33 |     Rack::Mime.match?('text', 'text').must_equal true
34 |     Rack::Mime.match?('text', 'binary').must_equal false
35 |   end
36 | 
37 |   it "should match class wildcard mimes" do
38 |     Rack::Mime.match?('text/html', 'text/*').must_equal true
39 |     Rack::Mime.match?('text/plain', 'text/*').must_equal true
40 |     Rack::Mime.match?('application/json', 'text/*').must_equal false
41 |     Rack::Mime.match?('text/html', 'text').must_equal true
42 |   end
43 | 
44 |   it "should match full wildcards" do
45 |     Rack::Mime.match?('text/html', '*').must_equal true
46 |     Rack::Mime.match?('text/plain', '*').must_equal true
47 |     Rack::Mime.match?('text/html', '*/*').must_equal true
48 |     Rack::Mime.match?('text/plain', '*/*').must_equal true
49 |   end
50 | 
51 |   it "should match type wildcard mimes" do
52 |     Rack::Mime.match?('text/html', '*/html').must_equal true
53 |     Rack::Mime.match?('text/plain', '*/plain').must_equal true
54 |   end
55 | 
56 | end
57 | 


--------------------------------------------------------------------------------
/test/spec_null_logger.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/null_logger'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/mock_request'
 9 | end
10 | 
11 | describe Rack::NullLogger do
12 |   it "act as a noop logger" do
13 |     app = lambda { |env|
14 |       env['rack.logger'].warn "b00m"
15 |       [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]]
16 |     }
17 | 
18 |     logger = Rack::Lint.new(Rack::NullLogger.new(app))
19 | 
20 |     res = logger.call(Rack::MockRequest.env_for)
21 |     res[0..1].must_equal [
22 |       200, { 'content-type' => 'text/plain' }
23 |     ]
24 |     res[2].to_enum.to_a.must_equal ["Hello, World!"]
25 |   end
26 | end
27 | 


--------------------------------------------------------------------------------
/test/spec_query_parser.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/query_parser'
 7 | end
 8 | 
 9 | describe Rack::QueryParser do
10 |   it "can normalize values with missing values" do
11 |     query_parser = Rack::QueryParser.make_default(8)
12 |     query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
13 |     query_parser.parse_nested_query("a=").must_equal({"a" => ""})
14 |     query_parser.parse_nested_query("a").must_equal({"a" => nil})
15 |   end
16 | 
17 |   it "accepts bytesize_limit to specify maximum size of query string to parse" do
18 |     query_parser = Rack::QueryParser.make_default(32, bytesize_limit: 3)
19 |     query_parser.parse_query("a=a").must_equal({"a" => "a"})
20 |     query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
21 |     query_parser.parse_nested_query("a=a", '&').must_equal({"a" => "a"})
22 |     proc { query_parser.parse_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
23 |     proc { query_parser.parse_nested_query("a=aa") }.must_raise Rack::QueryParser::QueryLimitError
24 |     proc { query_parser.parse_nested_query("a=aa", '&') }.must_raise Rack::QueryParser::QueryLimitError
25 |   end
26 | 
27 |   it "accepts params_limit to specify maximum number of query parameters to parse" do
28 |     query_parser = Rack::QueryParser.make_default(32, params_limit: 2)
29 |     query_parser.parse_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
30 |     query_parser.parse_nested_query("a=a&b=b").must_equal({"a" => "a", "b" => "b"})
31 |     query_parser.parse_nested_query("a=a&b=b", '&').must_equal({"a" => "a", "b" => "b"})
32 |     proc { query_parser.parse_query("a=a&b=b&c=c") }.must_raise Rack::QueryParser::QueryLimitError
33 |     proc { query_parser.parse_nested_query("a=a&b=b&c=c", '&') }.must_raise Rack::QueryParser::QueryLimitError
34 |     proc { query_parser.parse_query("b[]=a&b[]=b&b[]=c") }.must_raise Rack::QueryParser::QueryLimitError
35 |   end
36 | end
37 | 


--------------------------------------------------------------------------------
/test/spec_recursive.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/recursive'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/mock_request'
 9 |   require_relative '../lib/rack/urlmap'
10 | end
11 | 
12 | describe Rack::Recursive do
13 |   before do
14 |   @app1 = lambda { |env|
15 |     res = Rack::Response.new
16 |     res["x-path-info"] = env["PATH_INFO"]
17 |     res["x-query-string"] = env["QUERY_STRING"]
18 |     res.finish do |inner_res|
19 |       inner_res.write "App1"
20 |     end
21 |   }
22 | 
23 |   @app2 = lambda { |env|
24 |     Rack::Response.new.finish do |res|
25 |       res.write "App2"
26 |       _, _, body = env['rack.recursive.include'].call(env, "/app1")
27 |       body.each { |b|
28 |         res.write b
29 |       }
30 |     end
31 |   }
32 | 
33 |   @app3 = lambda { |env|
34 |     raise Rack::ForwardRequest.new("/app1")
35 |   }
36 | 
37 |   @app4 = lambda { |env|
38 |     raise Rack::ForwardRequest.new("http://example.org/app1/quux?meh")
39 |   }
40 |   end
41 | 
42 |   def recursive(map)
43 |     Rack::Lint.new Rack::Recursive.new(Rack::URLMap.new(map))
44 |   end
45 | 
46 |   it "allow for subrequests" do
47 |     res = Rack::MockRequest.new(recursive("/app1" => @app1,
48 |                                           "/app2" => @app2)).
49 |       get("/app2")
50 | 
51 |     res.must_be :ok?
52 |     res.body.must_equal "App2App1"
53 |   end
54 | 
55 |   it "raise error on requests not below the app" do
56 |     app = Rack::URLMap.new("/app1" => @app1,
57 |                            "/app" => recursive("/1" => @app1,
58 |                                                "/2" => @app2))
59 | 
60 |     lambda {
61 |       Rack::MockRequest.new(app).get("/app/2")
62 |     }.must_raise(ArgumentError).
63 |       message.must_match(/can only include below/)
64 |   end
65 | 
66 |   it "support forwarding" do
67 |     app = recursive("/app1" => @app1,
68 |                     "/app3" => @app3,
69 |                     "/app4" => @app4)
70 | 
71 |     res = Rack::MockRequest.new(app).get("/app3")
72 |     res.must_be :ok?
73 |     res.body.must_equal "App1"
74 | 
75 |     res = Rack::MockRequest.new(app).get("/app4")
76 |     res.must_be :ok?
77 |     res.body.must_equal "App1"
78 |     res["x-path-info"].must_equal "/quux"
79 |     res["x-query-string"].must_equal "meh"
80 |   end
81 | end
82 | 


--------------------------------------------------------------------------------
/test/spec_rewindable_input.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/rewindable_input'
  7 | end
  8 | 
  9 | module RewindableTest
 10 |   extend Minitest::Spec::DSL
 11 | 
 12 |   def setup
 13 |     @rio = Rack::RewindableInput.new(@io)
 14 |   end
 15 | 
 16 |   it "be able to handle to read()" do
 17 |     @rio.read.must_equal "hello world"
 18 |   end
 19 | 
 20 |   it "be able to handle to read(nil)" do
 21 |     @rio.read(nil).must_equal "hello world"
 22 |   end
 23 | 
 24 |   it "be able to handle to read(length)" do
 25 |     @rio.read(1).must_equal "h"
 26 |   end
 27 | 
 28 |   it "be able to handle to read(length, buffer)" do
 29 |     buffer = "".dup
 30 |     result = @rio.read(1, buffer)
 31 |     result.must_equal "h"
 32 |     result.object_id.must_equal buffer.object_id
 33 |   end
 34 | 
 35 |   it "be able to handle to read(nil, buffer)" do
 36 |     buffer = "".dup
 37 |     result = @rio.read(nil, buffer)
 38 |     result.must_equal "hello world"
 39 |     result.object_id.must_equal buffer.object_id
 40 |   end
 41 | 
 42 |   it "rewind to the beginning when #rewind is called" do
 43 |     @rio.rewind
 44 |     @rio.read(1).must_equal 'h'
 45 |     @rio.rewind
 46 |     @rio.read.must_equal "hello world"
 47 |   end
 48 | 
 49 |   it "be able to handle gets" do
 50 |     @rio.gets.must_equal "hello world"
 51 |     @rio.rewind
 52 |     @rio.gets.must_equal "hello world"
 53 |   end
 54 | 
 55 |   it "be able to handle size" do
 56 |     @rio.size.must_equal "hello world".size
 57 |     @rio.size.must_equal "hello world".size
 58 |     @rio.rewind
 59 |     @rio.gets.must_equal "hello world"
 60 |   end
 61 | 
 62 |   it "be able to handle each" do
 63 |     array = []
 64 |     @rio.each do |data|
 65 |       array << data
 66 |     end
 67 |     array.must_equal ["hello world"]
 68 | 
 69 |     @rio.rewind
 70 |     array = []
 71 |     @rio.each do |data|
 72 |       array << data
 73 |     end
 74 |     array.must_equal ["hello world"]
 75 |   end
 76 | 
 77 |   it "not buffer into a Tempfile if no data has been read yet" do
 78 |     @rio.instance_variable_get(:@rewindable_io).must_be_nil
 79 |   end
 80 | 
 81 |   it "buffer into a Tempfile when data has been consumed for the first time" do
 82 |     @rio.read(1)
 83 |     tempfile = @rio.instance_variable_get(:@rewindable_io)
 84 |     tempfile.wont_be :nil?
 85 |     @rio.read(1)
 86 |     tempfile2 = @rio.instance_variable_get(:@rewindable_io)
 87 |     tempfile2.path.must_equal tempfile.path
 88 |   end
 89 | 
 90 |   it "close the underlying tempfile upon calling #close" do
 91 |     @rio.read(1)
 92 |     tempfile = @rio.instance_variable_get(:@rewindable_io)
 93 |     @rio.close
 94 |     tempfile.must_be :closed?
 95 |   end
 96 | 
 97 |   it "handle partial writes to tempfile" do
 98 |     def @rio.filesystem_has_posix_semantics?
 99 |       def @rewindable_io.write(buffer)
100 |         super(buffer[0..1])
101 |       end
102 |       super
103 |     end
104 |     @rio.read(1)
105 |     tempfile = @rio.instance_variable_get(:@rewindable_io)
106 |     @rio.close
107 |     tempfile.must_be :closed?
108 |   end
109 | 
110 |   it "close the underlying tempfile upon calling #close when not using posix semantics" do
111 |     def @rio.filesystem_has_posix_semantics?; false end
112 |     @rio.read(1)
113 |     tempfile = @rio.instance_variable_get(:@rewindable_io)
114 |     @rio.close
115 |     tempfile.must_be :closed?
116 |   end
117 | 
118 |   it "be possible to call #close when no data has been buffered yet" do
119 |     @rio.close.must_be_nil
120 |   end
121 | 
122 |   it "be possible to call #close multiple times" do
123 |     @rio.close.must_be_nil
124 |     @rio.close.must_be_nil
125 |   end
126 | 
127 |   after do
128 |   @rio.close
129 |   @rio = nil
130 |   end
131 | end
132 | 
133 | describe Rack::RewindableInput do
134 |   describe "given an IO object that is already rewindable" do
135 |     def setup
136 |       @io = StringIO.new("hello world".dup)
137 |       super
138 |     end
139 | 
140 |     include RewindableTest
141 |   end
142 | 
143 |   describe "given an IO object that is not rewindable" do
144 |     def setup
145 |       @io = StringIO.new("hello world".dup)
146 |       @io.instance_eval do
147 |         undef :rewind
148 |       end
149 |       super
150 |     end
151 | 
152 |     include RewindableTest
153 |   end
154 | 
155 |   describe "given an IO object whose rewind method raises Errno::ESPIPE" do
156 |     def setup
157 |       @io = StringIO.new("hello world".dup)
158 |       def @io.rewind
159 |         raise Errno::ESPIPE, "You can't rewind this!"
160 |       end
161 |       super
162 |     end
163 | 
164 |     include RewindableTest
165 |   end
166 | end
167 | 
168 | describe Rack::RewindableInput::Middleware do
169 |   it "wraps rack.input in RewindableInput" do
170 |     app = proc{|env| [200, {}, [env['rack.input'].class.to_s]]}
171 |     app.call('rack.input'=>StringIO.new(''))[2].must_equal ['StringIO']
172 |     app = Rack::RewindableInput::Middleware.new(app)
173 |     app.call('rack.input'=>StringIO.new(''))[2].must_equal ['Rack::RewindableInput']
174 |   end
175 | 
176 |   it "preserves a nil rack.input" do
177 |     app = proc{|env| [200, {}, [env['rack.input'].class.to_s]]}
178 |     app.call('rack.input'=>nil)[2].must_equal ['NilClass']
179 |     app = Rack::RewindableInput::Middleware.new(app)
180 |     app.call('rack.input'=>nil)[2].must_equal ['NilClass']
181 |   end
182 | end
183 | 


--------------------------------------------------------------------------------
/test/spec_runtime.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/runtime'
 7 |   require_relative '../lib/rack/lint'
 8 |   require_relative '../lib/rack/mock_request'
 9 | end
10 | 
11 | describe Rack::Runtime do
12 |   def runtime_app(app, *args)
13 |     Rack::Lint.new Rack::Runtime.new(app, *args)
14 |   end
15 | 
16 |   def request
17 |     Rack::MockRequest.env_for
18 |   end
19 | 
20 |   it "sets x-runtime is none is set" do
21 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, "Hello, World!"] }
22 |     response = runtime_app(app).call(request)
23 |     response[1]['x-runtime'].must_match(/[\d\.]+/)
24 |   end
25 | 
26 |   it "doesn't set the x-runtime if it is already set" do
27 |     app = lambda { |env| [200, { 'content-type' => 'text/plain', "x-runtime" => "foobar" }, "Hello, World!"] }
28 |     response = runtime_app(app).call(request)
29 |     response[1]['x-runtime'].must_equal "foobar"
30 |   end
31 | 
32 |   it "allow a suffix to be set" do
33 |     app = lambda { |env| [200, { 'content-type' => 'text/plain' }, "Hello, World!"] }
34 |     response = runtime_app(app, "Test").call(request)
35 |     response[1]['x-runtime-test'].must_match(/[\d\.]+/)
36 |   end
37 | 
38 |   it "allow multiple timers to be set" do
39 |     app = lambda { |env| sleep 0.1; [200, { 'content-type' => 'text/plain' }, "Hello, World!"] }
40 |     runtime = runtime_app(app, "App")
41 | 
42 |     # wrap many times to guarantee a measurable difference
43 |     100.times do |i|
44 |       runtime = Rack::Runtime.new(runtime, i.to_s)
45 |     end
46 |     runtime = Rack::Runtime.new(runtime, "All")
47 | 
48 |     response = runtime.call(request)
49 | 
50 |     response[1]['x-runtime-app'].must_match(/[\d\.]+/)
51 |     response[1]['x-runtime-all'].must_match(/[\d\.]+/)
52 | 
53 |     Float(response[1]['x-runtime-all']).must_be :>, Float(response[1]['x-runtime-app'])
54 |   end
55 | end
56 | 


--------------------------------------------------------------------------------
/test/spec_show_exceptions.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/show_exceptions'
  7 |   require_relative '../lib/rack/lint'
  8 |   require_relative '../lib/rack/mock_request'
  9 | end
 10 | 
 11 | describe Rack::ShowExceptions do
 12 |   def show_exceptions(app)
 13 |     Rack::Lint.new Rack::ShowExceptions.new(app)
 14 |   end
 15 | 
 16 |   it "catches exceptions" do
 17 |     res = nil
 18 | 
 19 |     req = Rack::MockRequest.new(
 20 |       show_exceptions(
 21 |         lambda{|env| raise RuntimeError }
 22 |     ))
 23 | 
 24 |     res = req.get("/", "HTTP_ACCEPT" => "text/html")
 25 | 
 26 |     res.must_be :server_error?
 27 |     res.status.must_equal 500
 28 | 
 29 |     assert_match(res, /RuntimeError/)
 30 |     assert_match(res, /ShowExceptions/)
 31 |     assert_match(res, /No GET data/)
 32 |     assert_match(res, /No POST data/)
 33 |   end
 34 | 
 35 |   it "handles exceptions with backtrace lines for files that are not readable" do
 36 |     res = nil
 37 | 
 38 |     req = Rack::MockRequest.new(
 39 |       show_exceptions(
 40 |         lambda{|env| raise RuntimeError, "foo", ["nonexistent.rb:2:in `a': adf (RuntimeError)", "bad-backtrace"] }
 41 |     ))
 42 | 
 43 |     res = req.get("/", "HTTP_ACCEPT" => "text/html")
 44 | 
 45 |     res.must_be :server_error?
 46 |     res.status.must_equal 500
 47 | 
 48 |     assert_includes(res.body, 'RuntimeError')
 49 |     assert_includes(res.body, 'ShowExceptions')
 50 |     assert_includes(res.body, 'No GET data')
 51 |     assert_includes(res.body, 'No POST data')
 52 |     assert_includes(res.body, 'nonexistent.rb')
 53 |     refute_includes(res.body, 'bad-backtrace')
 54 |   end
 55 | 
 56 |   it "handles invalid POST data exceptions" do
 57 |     res = nil
 58 | 
 59 |     req = Rack::MockRequest.new(
 60 |       show_exceptions(
 61 |         lambda{|env| raise RuntimeError }
 62 |     ))
 63 | 
 64 |     res = req.post("/", "HTTP_ACCEPT" => "text/html", "rack.input" => StringIO.new(String.new << '(%bad-params%)'))
 65 | 
 66 |     res.must_be :server_error?
 67 |     res.status.must_equal 500
 68 | 
 69 |     assert_match(res, /RuntimeError/)
 70 |     assert_match(res, /ShowExceptions/)
 71 |     assert_match(res, /No GET data/)
 72 |     assert_match(res, /Invalid POST data/)
 73 |   end
 74 | 
 75 |   it "works with binary data in the Rack environment" do
 76 |     res = nil
 77 | 
 78 |     # "\xCC" is not a valid UTF-8 string
 79 |     req = Rack::MockRequest.new(
 80 |       show_exceptions(
 81 |         lambda{|env| env['foo'] = "\xCC"; raise RuntimeError }
 82 |     ))
 83 | 
 84 |     res = req.get("/", "HTTP_ACCEPT" => "text/html")
 85 | 
 86 |     res.must_be :server_error?
 87 |     res.status.must_equal 500
 88 | 
 89 |     assert_match(res, /RuntimeError/)
 90 |     assert_match(res, /ShowExceptions/)
 91 |   end
 92 | 
 93 |   it "responds with HTML only to requests accepting HTML" do
 94 |     res = nil
 95 | 
 96 |     req = Rack::MockRequest.new(
 97 |       show_exceptions(
 98 |         lambda{|env| raise RuntimeError, "It was never supposed to work" }
 99 |     ))
100 | 
101 |     [
102 |       # Serve text/html when the client accepts text/html
103 |       ["text/html", ["/", { "HTTP_ACCEPT" => "text/html" }]],
104 |       ["text/html", ["/", { "HTTP_ACCEPT" => "*/*" }]],
105 |       # Serve text/plain when the client does not accept text/html
106 |       ["text/plain", ["/"]],
107 |       ["text/plain", ["/", { "HTTP_ACCEPT" => "application/json" }]]
108 |     ].each do |exmime, rargs|
109 |       res = req.get(*rargs)
110 | 
111 |       res.must_be :server_error?
112 |       res.status.must_equal 500
113 | 
114 |       res.content_type.must_equal exmime
115 | 
116 |       res.body.must_include "RuntimeError"
117 |       res.body.must_include "It was never supposed to work"
118 | 
119 |       if exmime == "text/html"
120 |         res.body.must_include '</html>'
121 |       else
122 |         res.body.wont_include '</html>'
123 |       end
124 |     end
125 |   end
126 | 
127 |   it "handles exceptions without a backtrace" do
128 |     res = nil
129 | 
130 |     req = Rack::MockRequest.new(
131 |       show_exceptions(
132 |         lambda{|env| raise RuntimeError, "", [] }
133 |       )
134 |     )
135 | 
136 |     res = req.get("/", "HTTP_ACCEPT" => "text/html")
137 | 
138 |     res.must_be :server_error?
139 |     res.status.must_equal 500
140 | 
141 |     assert_match(res, /RuntimeError/)
142 |     assert_match(res, /ShowExceptions/)
143 |     assert_match(res, /unknown location/)
144 |   end
145 | 
146 |   it "allows subclasses to override template" do
147 |     c = Class.new(Rack::ShowExceptions) do
148 |       TEMPLATE = ERB.new("foo")
149 | 
150 |       def template
151 |         TEMPLATE
152 |       end
153 |     end
154 | 
155 |     app = lambda { |env| raise RuntimeError, "", [] }
156 | 
157 |     req = Rack::MockRequest.new(
158 |       Rack::Lint.new c.new(app)
159 |     )
160 | 
161 |     res = req.get("/", "HTTP_ACCEPT" => "text/html")
162 | 
163 |     res.must_be :server_error?
164 |     res.status.must_equal 500
165 |     res.body.must_equal "foo"
166 |   end
167 | 
168 |   it "knows to prefer plaintext for non-html" do
169 |     # We don't need an app for this
170 |     exc = Rack::ShowExceptions.new(nil)
171 | 
172 |     [
173 |       [{ "HTTP_ACCEPT" => "text/plain" }, true],
174 |       [{ "HTTP_ACCEPT" => "text/foo" }, true],
175 |       [{ "HTTP_ACCEPT" => "text/html" }, false]
176 |     ].each do |env, expected|
177 |       assert_equal(expected, exc.prefers_plaintext?(env))
178 |     end
179 |   end
180 | 
181 |   it "prefers Exception#detailed_message instead of Exception#message if available" do
182 |     res = nil
183 | 
184 |     custom_exc_class = Class.new(RuntimeError) do
185 |       def detailed_message(highlight: false)
186 |         "detailed_message_test"
187 |       end
188 |     end
189 | 
190 |     req = Rack::MockRequest.new(
191 |       show_exceptions(
192 |         lambda{|env| raise custom_exc_class }
193 |     ))
194 | 
195 |     res = req.get("/", "HTTP_ACCEPT" => "text/html")
196 | 
197 |     res.must_be :server_error?
198 |     res.status.must_equal 500
199 | 
200 |     assert_match(res, /detailed_message_test/)
201 |     assert_match(res, /ShowExceptions/)
202 |     assert_match(res, /No GET data/)
203 |     assert_match(res, /No POST data/)
204 |   end
205 | end
206 | 


--------------------------------------------------------------------------------
/test/spec_show_status.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/show_status'
  7 |   require_relative '../lib/rack/lint'
  8 |   require_relative '../lib/rack/mock_request'
  9 | end
 10 | 
 11 | describe Rack::ShowStatus do
 12 |   def show_status(app)
 13 |     Rack::Lint.new Rack::ShowStatus.new(app)
 14 |   end
 15 | 
 16 |   it "provide a default status message" do
 17 |     req = Rack::MockRequest.new(
 18 |       show_status(lambda{|env|
 19 |         [404, { "content-type" => "text/plain", "content-length" => "0" }, []]
 20 |     }))
 21 | 
 22 |     res = req.get("/", lint: true)
 23 |     res.must_be :not_found?
 24 |     res.wont_be_empty
 25 | 
 26 |     res["content-type"].must_equal "text/html"
 27 |     assert_match(res, /404/)
 28 |     assert_match(res, /Not Found/)
 29 |   end
 30 | 
 31 |   it "let the app provide additional information" do
 32 |     req = Rack::MockRequest.new(
 33 |       show_status(
 34 |         lambda{|env|
 35 |           env["rack.showstatus.detail"] = "gone too meta."
 36 |           [404, { "content-type" => "text/plain", "content-length" => "0" }, []]
 37 |     }))
 38 | 
 39 |     res = req.get("/", lint: true)
 40 |     res.must_be :not_found?
 41 |     res.wont_be_empty
 42 | 
 43 |     res["content-type"].must_equal "text/html"
 44 |     assert_match(res, /404/)
 45 |     assert_match(res, /Not Found/)
 46 |     assert_match(res, /too meta/)
 47 |   end
 48 | 
 49 |   it "let the app provide additional information with non-String details" do
 50 |     req = Rack::MockRequest.new(
 51 |       show_status(
 52 |         lambda{|env|
 53 |           env["rack.showstatus.detail"] = ['gone too meta.']
 54 |           [404, { "content-type" => "text/plain", "content-length" => "0" }, []]
 55 |     }))
 56 | 
 57 |     res = req.get("/", lint: true)
 58 |     res.must_be :not_found?
 59 |     res.wont_be_empty
 60 | 
 61 |     res["content-type"].must_equal "text/html"
 62 |     assert_includes(res.body, '404')
 63 |     assert_includes(res.body, 'Not Found')
 64 |     assert_includes(res.body, '[&quot;gone too meta.&quot;]')
 65 |   end
 66 | 
 67 |   it "escape error" do
 68 |     detail = "<script>alert('hi \"')</script>"
 69 |     req = Rack::MockRequest.new(
 70 |       show_status(
 71 |         lambda{|env|
 72 |           env["rack.showstatus.detail"] = detail
 73 |           [500, { "content-type" => "text/plain", "content-length" => "0" }, []]
 74 |     }))
 75 | 
 76 |     res = req.get("/", lint: true)
 77 |     res.wont_be_empty
 78 | 
 79 |     res["content-type"].must_equal "text/html"
 80 |     assert_match(res, /500/)
 81 |     res.wont_include detail
 82 |     res.body.must_include Rack::Utils.escape_html(detail)
 83 |   end
 84 | 
 85 |   it "not replace existing messages" do
 86 |     req = Rack::MockRequest.new(
 87 |       show_status(
 88 |         lambda{|env|
 89 |           [404, { "content-type" => "text/plain", "content-length" => "4" }, ["foo!"]]
 90 |     }))
 91 | 
 92 |     res = req.get("/", lint: true)
 93 |     res.must_be :not_found?
 94 | 
 95 |     res.body.must_equal "foo!"
 96 |   end
 97 | 
 98 |   it "pass on original headers" do
 99 |     headers = { "www-authenticate" => "Basic blah" }
100 | 
101 |     req = Rack::MockRequest.new(
102 |       show_status(lambda{|env| [401, headers, []] }))
103 |     res = req.get("/", lint: true)
104 | 
105 |     res["www-authenticate"].must_equal "Basic blah"
106 |   end
107 | 
108 |   it "replace existing messages if there is detail" do
109 |     req = Rack::MockRequest.new(
110 |       show_status(
111 |         lambda{|env|
112 |           env["rack.showstatus.detail"] = "gone too meta."
113 |           [404, { "content-type" => "text/plain", "content-length" => "4" }, ["foo!"]]
114 |     }))
115 | 
116 |     res = req.get("/", lint: true)
117 |     res.must_be :not_found?
118 |     res.wont_be_empty
119 | 
120 |     res["content-type"].must_equal "text/html"
121 |     res["content-length"].wont_equal "4"
122 |     assert_match(res, /404/)
123 |     assert_match(res, /too meta/)
124 |     res.body.wont_match(/foo/)
125 |   end
126 | 
127 |   it "close the original body" do
128 |     closed = false
129 | 
130 |     body = Object.new
131 |     def body.each; yield 's' end
132 |     body.define_singleton_method(:close) { closed = true }
133 | 
134 |     req = Rack::MockRequest.new(
135 |       show_status(lambda{|env|
136 |         [404, { "content-type" => "text/plain", "content-length" => "0" }, body]
137 |     }))
138 | 
139 |     req.get("/", lint: true)
140 |     closed.must_equal true
141 |   end
142 | end
143 | 


--------------------------------------------------------------------------------
/test/spec_tempfile_reaper.rb:
--------------------------------------------------------------------------------
  1 | # frozen_string_literal: true
  2 | 
  3 | require_relative 'helper'
  4 | 
  5 | separate_testing do
  6 |   require_relative '../lib/rack/tempfile_reaper'
  7 |   require_relative '../lib/rack/lint'
  8 |   require_relative '../lib/rack/mock_request'
  9 | end
 10 | 
 11 | describe Rack::TempfileReaper do
 12 |   class MockTempfile
 13 |     attr_reader :closed
 14 | 
 15 |     def initialize
 16 |       @closed = false
 17 |     end
 18 | 
 19 |     def close!
 20 |       @closed = true
 21 |     end
 22 |   end
 23 | 
 24 |   before do
 25 |     @env = Rack::MockRequest.env_for
 26 |   end
 27 | 
 28 |   def call(app)
 29 |     Rack::Lint.new(Rack::TempfileReaper.new(app)).call(@env)
 30 |   end
 31 | 
 32 |   it 'do nothing (i.e. not bomb out) without env[rack.tempfiles]' do
 33 |     app = lambda { |_| [200, {}, ['Hello, World!']] }
 34 |     response = call(app)
 35 |     response[2].close
 36 |     response[0].must_equal 200
 37 |   end
 38 | 
 39 |   it 'close env[rack.tempfiles] when app raises an error' do
 40 |     tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
 41 |     @env['rack.tempfiles'] = [ tempfile1, tempfile2 ]
 42 |     app = lambda { |_| raise 'foo' }
 43 |     proc{call(app)}.must_raise RuntimeError
 44 |     tempfile1.closed.must_equal true
 45 |     tempfile2.closed.must_equal true
 46 |   end
 47 | 
 48 |   it 'close env[rack.tempfiles] when app raises an non-StandardError' do
 49 |     tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
 50 |     @env['rack.tempfiles'] = [ tempfile1, tempfile2 ]
 51 |     app = lambda { |_| raise LoadError, 'foo' }
 52 |     proc{call(app)}.must_raise LoadError
 53 |     tempfile1.closed.must_equal true
 54 |     tempfile2.closed.must_equal true
 55 |   end
 56 | 
 57 |   it 'close env[rack.tempfiles] when body is closed' do
 58 |     tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
 59 |     @env['rack.tempfiles'] = [ tempfile1, tempfile2 ]
 60 |     app = lambda { |_| [200, {}, ['Hello, World!']] }
 61 |     call(app)[2].close
 62 |     tempfile1.closed.must_equal true
 63 |     tempfile2.closed.must_equal true
 64 |   end
 65 | 
 66 |   it 'initialize env[rack.tempfiles] when not already present' do
 67 |     tempfile = MockTempfile.new
 68 |     app = lambda do |env|
 69 |       env['rack.tempfiles'] << tempfile
 70 |       [200, {}, ['Hello, World!']]
 71 |     end
 72 |     call(app)[2].close
 73 |     tempfile.closed.must_equal true
 74 |   end
 75 | 
 76 |   it 'append env[rack.tempfiles] when already present' do
 77 |     tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
 78 |     @env['rack.tempfiles'] = [ tempfile1 ]
 79 |     app = lambda do |env|
 80 |       env['rack.tempfiles'] << tempfile2
 81 |       [200, {}, ['Hello, World!']]
 82 |     end
 83 |     call(app)[2].close
 84 |     tempfile1.closed.must_equal true
 85 |     tempfile2.closed.must_equal true
 86 |   end
 87 | 
 88 |   it 'handle missing rack.tempfiles on normal response' do
 89 |     app = lambda do |env|
 90 |       env.delete('rack.tempfiles')
 91 |       [200, {}, ['Hello, World!']]
 92 |     end
 93 |     call(app)[2].close
 94 |   end
 95 | 
 96 |   it 'handle missing rack.tempfiles on error' do
 97 |     app = lambda do |env|
 98 |       env.delete('rack.tempfiles')
 99 |       raise 'Foo'
100 |     end
101 |     proc{call(app)}.must_raise RuntimeError
102 |   end
103 | end
104 | 


--------------------------------------------------------------------------------
/test/spec_version.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'helper'
 4 | 
 5 | separate_testing do
 6 |   require_relative '../lib/rack/version'
 7 | end
 8 | 
 9 | describe Rack do
10 |   describe 'VERSION' do
11 |     it 'is a version string' do
12 |       Rack::VERSION.must_match(/\d+\.\d+\.\d+/)
13 |     end
14 |   end
15 | 
16 |   describe 'RELEASE' do
17 |     it 'is the same as VERSION' do
18 |       Rack::RELEASE.must_equal Rack::VERSION
19 |     end
20 |   end
21 | 
22 |   describe '.release' do
23 |     it 'returns the version string' do
24 |       Rack.release.must_equal Rack::VERSION
25 |     end
26 |   end
27 | end
28 | 


--------------------------------------------------------------------------------
/test/static/another/index.html:
--------------------------------------------------------------------------------
1 | another index!
2 | 


--------------------------------------------------------------------------------
/test/static/foo.html:
--------------------------------------------------------------------------------
1 | foo.html!
2 | 


--------------------------------------------------------------------------------
/test/static/index.html:
--------------------------------------------------------------------------------
1 | index!
2 | 


--------------------------------------------------------------------------------