├── .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, '["gone too meta."]') 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 | --------------------------------------------------------------------------------