├── Gemfile ├── .rspec ├── lib ├── rack-canonical-host.rb └── rack │ ├── canonical_host │ ├── version.rb │ ├── request.rb │ └── redirect.rb │ └── canonical_host.rb ├── gemfiles ├── rack_1.6.gemfile ├── rack_2.0.gemfile ├── rack_2.1.gemfile ├── rack_2.2.gemfile └── rack_3.0.gemfile ├── Rakefile ├── .gitignore ├── Appraisals ├── spec ├── spec_helper.rb ├── support │ └── matchers │ │ ├── be_bad_request.rb │ │ ├── have_header.rb │ │ └── redirect_to.rb └── rack │ └── canonical_host_spec.rb ├── rack-canonical-host.gemspec ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── CHANGELOG.md └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --warnings 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/rack-canonical-host.rb: -------------------------------------------------------------------------------- 1 | require 'rack/canonical_host' 2 | -------------------------------------------------------------------------------- /lib/rack/canonical_host/version.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class CanonicalHost 3 | VERSION = '1.3.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /gemfiles/rack_1.6.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rack", "~> 1.6.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rack_2.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rack", "~> 2.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rack_2.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rack", "~> 2.1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rack_2.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rack", "~> 2.2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rack_3.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rack", "~> 3.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .rvmrc 6 | .yardoc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc 12 | gemfiles/*.lock 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rack-1.6' do 2 | gem 'rack', '~> 1.6.0' 3 | end 4 | 5 | appraise 'rack-2.0' do 6 | gem 'rack', '~> 2.0.0' 7 | end 8 | 9 | appraise 'rack-2.1' do 10 | gem 'rack', '~> 2.1.0' 11 | end 12 | 13 | appraise 'rack-2.2' do 14 | gem 'rack', '~> 2.2.0' 15 | end 16 | 17 | appraise 'rack-3.0' do 18 | gem 'rack', '~> 3.0' 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rack/canonical_host' 2 | 3 | Dir[File.expand_path('../support/**/*.rb', __FILE__)].each do |file| 4 | require(file) 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.filter_run :focus 9 | config.run_all_when_everything_filtered = true 10 | 11 | config.order = :random 12 | Kernel.srand config.seed 13 | 14 | if config.files_to_run.one? 15 | config.default_formatter = 'doc' 16 | end 17 | 18 | config.expect_with :rspec do |expectations| 19 | expectations.syntax = :expect 20 | end 21 | 22 | config.mock_with :rspec do |mocks| 23 | mocks.syntax = :expect 24 | mocks.verify_partial_doubles = true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /rack-canonical-host.gemspec: -------------------------------------------------------------------------------- 1 | require './lib/rack/canonical_host/version' 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = 'rack-canonical-host' 5 | gem.version = Rack::CanonicalHost::VERSION 6 | gem.licenses = ['MIT'] 7 | gem.summary = 'Rack middleware for defining a canonical host name.' 8 | gem.homepage = 'https://github.com/tylerhunt/rack-canonical-host' 9 | gem.author = 'Tyler Hunt' 10 | 11 | gem.add_dependency 'addressable', '> 0', '< 3' 12 | gem.add_dependency 'rack', '>= 1.6', '< 4' 13 | gem.add_development_dependency 'appraisal', '~> 2.2' 14 | gem.add_development_dependency 'rake', '~> 13.0' 15 | gem.add_development_dependency 'rspec', '~> 3.0' 16 | 17 | gem.files = Dir['lib/**/*.rb'] + ['README.md', 'CHANGELOG.md', 'LICENSE'] 18 | gem.require_paths = ['lib'] 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Ruby ${{ matrix.ruby }} & Rack ${{ matrix.rack }} 8 | runs-on: 'ubuntu-latest' 9 | strategy: 10 | matrix: 11 | ruby: 12 | - '3.2' 13 | - '3.1' 14 | - '3.0' 15 | - '2.7' 16 | - '2.6' 17 | rack: 18 | - '1.6' 19 | - '2.0' 20 | - '2.1' 21 | - '2.2' 22 | - '3.0' 23 | fail-fast: false 24 | env: 25 | BUNDLE_GEMFILE: "gemfiles/rack_${{ matrix.rack }}.gemfile" 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true # 'bundle install' and cache 32 | - run: bundle exec rake 33 | -------------------------------------------------------------------------------- /spec/support/matchers/be_bad_request.rb: -------------------------------------------------------------------------------- 1 | module BadRequest 2 | class Matcher 3 | def matches?(response) 4 | status_code, _, _ = response 5 | 6 | status_code_matches?(status_code) 7 | end 8 | 9 | def description 10 | 'be a bad request' 11 | end 12 | 13 | def failure_message 14 | "Expected response to #{description}" 15 | end 16 | 17 | def failure_message_when_negated 18 | "Did not expect response to #{description}" 19 | end 20 | 21 | protected 22 | 23 | attr_accessor :actual_status_code 24 | 25 | private 26 | 27 | STATUS = 400 28 | 29 | def status_code_matches?(actual_status_code) 30 | STATUS == actual_status_code 31 | end 32 | end 33 | 34 | def be_bad_request 35 | Matcher.new 36 | end 37 | end 38 | 39 | RSpec.configure do |config| 40 | config.include BadRequest 41 | end 42 | -------------------------------------------------------------------------------- /lib/rack/canonical_host/request.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | require 'rack' 3 | 4 | module Rack 5 | class CanonicalHost 6 | class Request 7 | BAD_REQUEST = <<-HTML.gsub(/^\s+/, '') 8 | 9 | 10 |
The document has moved here.
14 | 15 | 16 | HTML 17 | 18 | def initialize(env, host, options={}) 19 | self.env = env 20 | self.host = host 21 | self.ignore = Array(options[:ignore]) 22 | self.conditions = Array(options[:if]) 23 | self.cache_control = options[:cache_control] 24 | end 25 | 26 | def canonical? 27 | return true unless enabled? 28 | known? || ignored? 29 | end 30 | 31 | def response 32 | [301, headers, [HTML_TEMPLATE % new_url]] 33 | end 34 | 35 | protected 36 | 37 | attr_accessor :env 38 | attr_accessor :host 39 | attr_accessor :ignore 40 | attr_accessor :conditions 41 | attr_accessor :cache_control 42 | 43 | private 44 | 45 | def any_match?(patterns, request_uri) 46 | patterns.any? { |pattern| 47 | case pattern 48 | when Proc then pattern.call(request_uri) 49 | when Regexp then request_uri.host =~ pattern 50 | when String then request_uri.host == pattern 51 | else false 52 | end 53 | } 54 | end 55 | 56 | def headers 57 | { 58 | 'cache-control' => cache_control, 59 | 'content-type' => 'text/html', 60 | 'location' => new_url, 61 | }.reject { |_, value| !value } 62 | end 63 | 64 | def enabled? 65 | return true if conditions.empty? 66 | 67 | any_match?(conditions, request_uri) 68 | end 69 | 70 | def ignored? 71 | return false if ignore.empty? 72 | 73 | any_match?(ignore, request_uri) 74 | end 75 | 76 | def known? 77 | host.nil? || request_uri.host == host 78 | end 79 | 80 | def new_url 81 | uri = request_uri.dup 82 | uri.host = host 83 | uri.normalize.to_s 84 | end 85 | 86 | def request_uri 87 | @request_uri ||= Addressable::URI.parse(Rack::Request.new(env).url) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.3.0 (2024-03-15) 4 | 5 | * Respond to invalid request URL with 400 ([Gareth Jones][G-Rath]) 6 | * Drop support for Rack 1.5 ([Tyler Hunt][tylerhunt]) 7 | 8 | ## 1.2.0 (2023-04-14) 9 | 10 | * Add support for Rack 3.0 ([Vinny Diehl][vinnydiehl]) 11 | * Remove unneeded gem directives ([Olle Jonsson][olleolleolle]) 12 | 13 | ## 1.1.0 (2021-11-10) 14 | 15 | * Support lambda/proc on `:if` and `:ignore` options ([Sean Huber][shuber]) 16 | * Drop support for Ruby versions 2.3, 2.4, and 2.5 ([Tyler Hunt][tylerhunt]) 17 | 18 | ## 1.0.0 (2020-04-16) 19 | 20 | * Use equality to determine string matches on `:if` and `:ignore` 21 | 22 | ## 0.2.3 (2017-04-20) 23 | 24 | * Add regexp support for `:ignore` option ([Daniel Searles][squaresurf]) 25 | 26 | ## 0.2.2 (2016-05-17) 27 | 28 | * Add `:cache_control` option ([Pete Nicholls][Aupajo]) 29 | 30 | ## 0.2.1 (2016-03-28) 31 | 32 | * Relax Rack dependency to allow for Rack 2 ([Tyler Ewing][zoso10]) 33 | 34 | ## 0.2.0 (2016-03-28) 35 | 36 | * Normalize redirect URL to avoid XSS vulnerability ([Thomas Maurer][tma]) 37 | * Remove `:force_ssl` option in favor of using [rack-ssl][rack-ssl] 38 | ([Nathaniel Bibler][nbibler]) 39 | 40 | [rack-ssl]: https://rubygems.org/gems/rack-ssl 41 | 42 | ## 0.1.0 (2014-04-16) 43 | 44 | * Add `:force_ssl` option ([Jeff Carbonella][jcarbo]) 45 | 46 | ## 0.0.9 (2014-02-14) 47 | 48 | * Add `:if` option ([Nick Ostrovsky][firedev]) 49 | * Improve documentation ([Joost Schuur][jschuur]) 50 | 51 | ## 0.0.8 (2012-06-22) 52 | 53 | * Switch to `Addressable::URI` for URI parsing ([Tyler Hunt][tylerhunt]) 54 | 55 | ## 0.0.7 (2012-06-21) 56 | 57 | * Fix handling of URLs containing `|` characters ([Tyler Hunt][tylerhunt]) 58 | 59 | ## 0.0.6 (2012-06-21) 60 | 61 | * Prevent redirect if the canonical host is `nil` ([Tyler Hunt][tylerhunt]) 62 | 63 | ## 0.0.5 (2012-06-21) 64 | 65 | * Rename `ignored_hosts` option to `ignore` ([Tyler Hunt][tylerhunt]) 66 | 67 | ## 0.0.4 (2012-06-20) 68 | 69 | * Add option to ignored certain hosts ([Eric Allam][rubymaverick]) 70 | * Add tests ([Nathaniel Bibler][nbibler]) 71 | * Add HTML response body on redirect 72 | * Set `Content-Type` header on redirect ([Jon Wood][jellybob]) 73 | * Improve documentation ([Peter Baker][finack]) 74 | 75 | ## 0.0.3 (2011-02-09) 76 | 77 | * Allow `env` to be passed to the optional block ([Tyler Hunt][tylerhunt]) 78 | 79 | ## 0.0.2 (2010-11-18) 80 | 81 | * Move `CanonicalHost` into `Rack` namespace ([Tyler Hunt][tylerhunt]) 82 | 83 | ## 0.0.1 (2009-11-04) 84 | 85 | * Initial release ([Tyler Hunt][tylerhunt]) 86 | 87 | [Aupajo]: https://github.com/Aupajo 88 | [finack]: https://github.com/finack 89 | [firedev]: https://github.com/firedev 90 | [jcarbo]: https://github.com/jcarbo 91 | [jellybob]: https://github.com/jellybob 92 | [jschuur]: https://github.com/jschuur 93 | [nbibler]: https://github.com/nbibler 94 | [rubymaverick]: https://github.com/ericallam 95 | [shuber]: https://github.com/shuber 96 | [squaresurf]: httpss://github.com/squaresurf 97 | [tma]: https://github.com/tma 98 | [tylerhunt]: https://github.com/tylerhunt 99 | [zoso10]: https://github.com/zoso10 100 | [olleolleolle]: https://github.com/olleolleolle 101 | [vinnydiehl]: https://github.com/vinnydiehl 102 | [G-Rath]: https://github.com/G-Rath 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rack Canonical Host 2 | 3 | Rack middleware that lets you define a single host name as the canonical host 4 | for your application. Requests for other host names will then be redirected to 5 | the canonical host. 6 | 7 | [](http://rubygems.org/gems/rack-canonical-host) 8 | [](https://github.com/tylerhunt/rack-canonical-host/actions/workflows/ci.yml) 9 | [](https://codeclimate.com/github/tylerhunt/rack-canonical-host) 10 | 11 | ## Installation 12 | 13 | Add this line to your application’s `Gemfile`: 14 | 15 | ```ruby 16 | gem 'rack-canonical-host' 17 | ``` 18 | 19 | And then execute: 20 | 21 | $ bundle 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install rack-canonical-host 26 | 27 | 28 | ## Usage 29 | 30 | For most applications, you can insert the middleware into the `config.ru` file 31 | in the root of the application. 32 | 33 | Here’s a simple example of what the `config.ru` in a Rails application might 34 | look like after adding the `Rack::CanonicalHost` middleware. 35 | 36 | ```ruby 37 | require 'rack/canonical_host' 38 | require ::File.expand_path('../config/environment', __FILE__) 39 | 40 | use Rack::CanonicalHost, 'example.com' 41 | run YourRailsApp::Application 42 | ``` 43 | 44 | In this case, any requests coming in that aren't for the specified host, 45 | `example.com`, will be redirected, keeping the requested path intact. 46 | 47 | 48 | ### Environment-Specific Configuration 49 | 50 | You probably don't want your redirect to happen when developing locally. One 51 | way to prevent this from happening is to use environment variables in your 52 | production environment to set the canonical host name. 53 | 54 | With Heroku, you would do this like so: 55 | 56 | $ heroku config:add CANONICAL_HOST=example.com 57 | 58 | Then, can configure the middleware like this: 59 | 60 | ```ruby 61 | use Rack::CanonicalHost, ENV['CANONICAL_HOST'] if ENV['CANONICAL_HOST'] 62 | ``` 63 | 64 | Now, the middleware will only be used if a canonical host has been defined. 65 | 66 | 67 | ### Options 68 | 69 | If you’d like the middleware to ignore certain hosts, use the `:ignore` option, 70 | which accepts a string, a regular expression, a proc, or an array of those 71 | objects. 72 | 73 | ```ruby 74 | use Rack::CanonicalHost, 'example.com', ignore: 'api.example.com' 75 | ``` 76 | 77 | In this case, requests for the host `api.example.com` will not be redirected. 78 | 79 | Alternatively, you can pass a block whose return value will be used as the 80 | canonical host name. 81 | 82 | ```ruby 83 | use Rack::CanonicalHost do |env| 84 | case env['RACK_ENV'].to_sym 85 | when :staging then 'staging.example.com' 86 | when :production then 'example.com' 87 | end 88 | end 89 | ``` 90 | 91 | If you want it to react only on specific hosts within a multi-domain 92 | environment, use the `:if` option, which accepts a string, a regular 93 | expression, a `lambda` or `proc`, or an array of those objects. 94 | 95 | ```ruby 96 | use Rack::CanonicalHost, 'example.com', if: /.*\.example\.com/ 97 | use Rack::CanonicalHost, 'example.org', 98 | if: ->(uri) { uri.host == 'www.example.org' } 99 | ``` 100 | 101 | ### Cache-Control 102 | 103 | To avoid browsers indefinitely caching a `301` redirect, it’s a sensible idea 104 | to set an expiry on each redirect, to hedge against the chance you may need to 105 | change that redirect in the future. 106 | 107 | ```ruby 108 | # Leave caching up to the browser (which could cache it indefinitely): 109 | use Rack::CanonicalHost, 'example.com' 110 | 111 | # Cache the redirect for up to an hour: 112 | use Rack::CanonicalHost, 'example.com', cache_control: 'max-age=3600' 113 | 114 | # Prevent caching of redirects: 115 | use Rack::CanonicalHost, 'example.com', cache_control: 'no-cache' 116 | ``` 117 | 118 | ## Contributing 119 | 120 | 1. Fork it 121 | 2. Create your feature branch (`git checkout -b my-new-feature`) 122 | 3. Commit your changes (`git commit -am 'Add some feature.'`) 123 | 4. Push to the branch (`git push origin my-new-feature`) 124 | 5. Create a new Pull Request 125 | 126 | 127 | ## Contributors 128 | 129 | Thanks to the following people who have contributed patches or helpful 130 | suggestions: 131 | 132 | * [Pete Nicholls](https://github.com/Aupajo) 133 | * [Tyler Ewing](https://github.com/zoso10) 134 | * [Thomas Maurer](https://github.com/tma) 135 | * [Jeff Carbonella](https://github.com/jcarbo) 136 | * [Joost Schuur](https://github.com/jellybob) 137 | * [Jon Wood](https://github.com/jellybob) 138 | * [Peter Baker](https://github.com/finack) 139 | * [Nathaniel Bibler](https://github.com/nbibler) 140 | * [Eric Allam](https://github.com/ericallam) 141 | * [Fabrizio Regini](https://github.com/freegenie) 142 | * [Daniel Searles](https://github.com/squaresurf) 143 | * [Sean Huber](https://github.com/shuber) 144 | * [Olle Jonsson](https://github.com/olleolleolle) 145 | * [Vinny Diehl](https://github.com/vinnydiehl) 146 | * [Gareth Jones](https://github.com/G-Rath) 147 | 148 | ## Copyright 149 | 150 | Copyright © 2009 Tyler Hunt. 151 | 152 | Released under the terms of the MIT license. See LICENSE for details. 153 | -------------------------------------------------------------------------------- /spec/rack/canonical_host_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Rack::CanonicalHost do 2 | let(:app_response) { [200, { 'content-type' => 'text/plain' }, %w(OK)] } 3 | let(:inner_app) { ->(env) { app_response } } 4 | 5 | def build_app(host=nil, options={}, inner_app=inner_app(), &block) 6 | Rack::Builder.new do 7 | use Rack::Lint 8 | use Rack::CanonicalHost, host, options, &block 9 | run inner_app 10 | end 11 | end 12 | 13 | shared_examples 'a matching request' do 14 | context 'with a request to a matching host' do 15 | it { should_not be_redirect } 16 | 17 | it 'calls the inner app' do 18 | expect(inner_app).to receive(:call).with(env).and_call_original 19 | call_app 20 | end 21 | end 22 | end 23 | 24 | shared_examples 'a non-matching request' do 25 | context 'with a request to a non-matching host' do 26 | it { should redirect_to('http://example.com/full/path') } 27 | 28 | it 'does not call the inner app' do 29 | expect(inner_app).to_not receive(:call) 30 | call_app 31 | end 32 | 33 | it { should_not have_header('cache-control') } 34 | end 35 | end 36 | 37 | context '#call' do 38 | let(:url) { 'http://example.com/full/path' } 39 | let(:headers) { {} } 40 | 41 | let(:app) { build_app('example.com') } 42 | let(:env) { Rack::MockRequest.env_for(url, headers) } 43 | 44 | def call_app 45 | app.call(env) 46 | end 47 | 48 | subject(:response) { call_app } 49 | 50 | it_behaves_like 'a matching request' 51 | 52 | it_behaves_like 'a non-matching request' do 53 | let(:url) { 'http://www.example.com/full/path' } 54 | end 55 | 56 | context 'when the request has a pipe in the URL' do 57 | let(:url) { 'https://example.com/full/path?value=withPIPE' } 58 | 59 | before do 60 | env['QUERY_STRING'].sub!('PIPE', '|') 61 | end 62 | 63 | it { expect { call_app }.to_not raise_error } 64 | end 65 | 66 | context 'when the request has JavaScript in the URL' do 67 | let(:url) { 'http://www.example.com/full/path' } 68 | 69 | let(:app) { build_app('example.com') } 70 | 71 | it 'escapes the JavaScript' do 72 | allow_any_instance_of(Rack::Request) 73 | .to receive(:query_string) 74 | .and_return('">') 75 | 76 | expect(response) 77 | .to redirect_to('http://example.com/full/path?%22%3E%3Cscript%3Ealert(73541)%3B%3C/script%3E') 78 | end 79 | end 80 | 81 | context 'when the app raises an invalid URI error' do 82 | let(:inner_app) { ->(env) { raise Addressable::URI::InvalidURIError } } 83 | 84 | it 'raises the error' do 85 | expect { call_app }.to raise_error Addressable::URI::InvalidURIError 86 | end 87 | end 88 | 89 | context 'with an X-Forwarded-Host' do 90 | let(:url) { 'http://proxy.test/full/path' } 91 | 92 | context 'which matches the canonical host' do 93 | let(:headers) { { 'HTTP_X_FORWARDED_HOST' => 'example.com:80' } } 94 | 95 | it_behaves_like 'a matching request' 96 | end 97 | 98 | context 'which does not match the canonical host' do 99 | let(:headers) { { 'HTTP_X_FORWARDED_HOST' => 'www.example.com:80' } } 100 | 101 | it_behaves_like 'a non-matching request' 102 | end 103 | end 104 | 105 | context 'with an invalid X-Forwarded-Host' do 106 | let(:headers) { 107 | { 108 | 'HTTP_X_FORWARDED_HOST' => 109 | '[${jndi:ldap://172.16.26.190:52314/nessus}]/' 110 | } 111 | } 112 | 113 | it { should_not be_redirect } 114 | it { should be_bad_request } 115 | it { should_not have_header('cache-control') } 116 | 117 | it 'does not call the inner app' do 118 | expect(inner_app).to_not receive(:call) 119 | 120 | call_app 121 | end 122 | end 123 | 124 | context 'without a host' do 125 | let(:app) { build_app(nil) } 126 | 127 | it_behaves_like 'a matching request' 128 | end 129 | 130 | context 'with :ignore option' do 131 | context 'with lambda/proc' do 132 | let(:app) { 133 | build_app( 134 | 'example.com', 135 | ignore: ->(uri) { uri.host == 'example.net' } 136 | ) 137 | } 138 | 139 | it_behaves_like 'a matching request' 140 | 141 | it_behaves_like 'a non-matching request' do 142 | let(:url) { 'http://www.example.com/full/path' } 143 | end 144 | 145 | context 'with a request to an ignored host' do 146 | let(:url) { 'http://example.net/full/path' } 147 | 148 | it { should_not be_redirect } 149 | 150 | it 'calls the inner app' do 151 | expect(inner_app).to receive(:call).with(env).and_call_original 152 | call_app 153 | end 154 | end 155 | end 156 | 157 | context 'with string' do 158 | let(:app) { build_app('example.com', ignore: 'example.net') } 159 | 160 | it_behaves_like 'a matching request' 161 | 162 | it_behaves_like 'a non-matching request' do 163 | let(:url) { 'http://www.example.com/full/path' } 164 | end 165 | 166 | context 'with a request to an ignored host' do 167 | let(:url) { 'http://example.net/full/path' } 168 | 169 | it { should_not be_redirect } 170 | 171 | it 'calls the inner app' do 172 | expect(inner_app).to receive(:call).with(env).and_call_original 173 | call_app 174 | end 175 | end 176 | end 177 | 178 | context 'with regular expression' do 179 | let(:app) { build_app('example.com', ignore: /ex.*\.net/) } 180 | 181 | it_behaves_like 'a matching request' 182 | 183 | it_behaves_like 'a non-matching request' do 184 | let(:url) { 'http://www.example.com/full/path' } 185 | end 186 | 187 | context 'with a request to an ignored host' do 188 | let(:url) { 'http://example.net/full/path' } 189 | 190 | it { should_not be_redirect } 191 | 192 | it 'calls the inner app' do 193 | expect(inner_app).to receive(:call).with(env).and_call_original 194 | call_app 195 | end 196 | end 197 | end 198 | end 199 | 200 | context 'with :if option' do 201 | context 'with a lambda/proc' do 202 | let(:app) { 203 | build_app( 204 | 'www.example.com', 205 | if: ->(uri) { uri.host == 'example.com' } 206 | ) 207 | } 208 | 209 | context 'with a request to a matching host' do 210 | let(:url) { 'http://example.com/full/path' } 211 | 212 | it { should redirect_to('http://www.example.com/full/path') } 213 | end 214 | 215 | context 'with a request to a non-matching host' do 216 | let(:url) { 'http://api.example.com/full/path' } 217 | 218 | it { should_not be_redirect } 219 | end 220 | end 221 | 222 | context 'with string' do 223 | let(:app) { build_app('www.example.com', if: 'example.com') } 224 | 225 | context 'with a request to a matching host' do 226 | let(:url) { 'http://example.com/full/path' } 227 | 228 | it { should redirect_to('http://www.example.com/full/path') } 229 | end 230 | 231 | context 'with a request to a non-matching host' do 232 | let(:url) { 'http://api.example.com/full/path' } 233 | 234 | it { should_not be_redirect } 235 | end 236 | end 237 | 238 | context 'with a regular expression' do 239 | let(:app) { build_app('example.com', if: '.*\.example\.com') } 240 | 241 | context 'with a request to a matching host' do 242 | let(:url) { 'http://www.example.com/full/path' } 243 | 244 | it { should_not redirect_to('http://example.com/full/path') } 245 | end 246 | 247 | context 'with a request to a non-matching host' do 248 | let(:url) { 'http://www.example.net/full/path' } 249 | 250 | it { should_not be_redirect } 251 | end 252 | end 253 | end 254 | 255 | context 'with a :cache_control option' do 256 | let(:url) { 'http://subdomain.example.net/full/path' } 257 | 258 | context 'with a max-age value' do 259 | let(:app) { build_app('example.com', cache_control: 'max-age=3600') } 260 | 261 | it { should have_header('cache-control').with('max-age=3600') } 262 | end 263 | 264 | context 'with a no-cache value' do 265 | let(:app) { build_app('example.com', cache_control: 'no-cache') } 266 | 267 | it { expect(subject).to have_header('cache-control').with('no-cache') } 268 | end 269 | 270 | context 'with a false value' do 271 | let(:app) { build_app('example.com', cache_control: false) } 272 | 273 | it { expect(subject).to_not have_header('cache-control') } 274 | end 275 | 276 | context 'with a nil value' do 277 | let(:app) { build_app('example.com', cache_control: false) } 278 | 279 | it { expect(subject).to_not have_header('cache-control') } 280 | end 281 | end 282 | 283 | context 'with a block' do 284 | let(:app) { build_app { 'example.com' } } 285 | 286 | it_behaves_like 'a matching request' 287 | 288 | it_behaves_like 'a non-matching request' do 289 | let(:url) { 'http://www.example.com/full/path' } 290 | end 291 | 292 | context 'that returns nil' do 293 | let(:app) { build_app('example.com') { nil } } 294 | 295 | it_behaves_like 'a matching request' 296 | 297 | it_behaves_like 'a non-matching request' do 298 | let(:url) { 'http://www.example.com/full/path' } 299 | end 300 | end 301 | end 302 | end 303 | end 304 | --------------------------------------------------------------------------------