├── .document ├── .gitignore ├── .rspec ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── lib └── rack │ ├── exception.rb │ ├── reverse_proxy.rb │ └── reverse_proxy_matcher.rb ├── rack-reverse-proxy.gemspec └── spec ├── rack └── reverse_proxy_spec.rb ├── spec_helper.rb └── support └── http_streaming_response_patch.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rack-reverse-proxy (0.8.1) 5 | rack (>= 1.0.0) 6 | rack-proxy (~> 0.5) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | addressable (2.3.6) 12 | coderay (1.0.9) 13 | crack (0.4.2) 14 | safe_yaml (~> 1.0.0) 15 | diff-lcs (1.2.5) 16 | ffi (1.9.0) 17 | ffi (1.9.0-java) 18 | formatador (0.2.4) 19 | guard (1.8.1) 20 | formatador (>= 0.2.4) 21 | listen (>= 1.0.0) 22 | lumberjack (>= 1.0.2) 23 | pry (>= 0.9.10) 24 | thor (>= 0.14.6) 25 | guard-bundler (1.0.0) 26 | bundler (~> 1.0) 27 | guard (~> 1.1) 28 | guard-rspec (1.2.1) 29 | guard (>= 1.1) 30 | listen (1.2.2) 31 | rb-fsevent (>= 0.9.3) 32 | rb-inotify (>= 0.9) 33 | rb-kqueue (>= 0.2) 34 | lumberjack (1.0.4) 35 | method_source (0.8.1) 36 | pry (0.9.12.2) 37 | coderay (~> 1.0.5) 38 | method_source (~> 0.8) 39 | slop (~> 3.4) 40 | pry (0.9.12.2-java) 41 | coderay (~> 1.0.5) 42 | method_source (~> 0.8) 43 | slop (~> 3.4) 44 | spoon (~> 0.0) 45 | rack (1.4.1) 46 | rack-proxy (0.5.15) 47 | rack 48 | rack-test (0.6.2) 49 | rack (>= 1.0) 50 | rake (10.3.2) 51 | rb-fsevent (0.9.3) 52 | rb-inotify (0.9.0) 53 | ffi (>= 0.5.0) 54 | rb-kqueue (0.2.0) 55 | ffi (>= 0.5.0) 56 | rspec (3.1.0) 57 | rspec-core (~> 3.1.0) 58 | rspec-expectations (~> 3.1.0) 59 | rspec-mocks (~> 3.1.0) 60 | rspec-core (3.1.3) 61 | rspec-support (~> 3.1.0) 62 | rspec-expectations (3.1.1) 63 | diff-lcs (>= 1.2.0, < 2.0) 64 | rspec-support (~> 3.1.0) 65 | rspec-mocks (3.1.0) 66 | rspec-support (~> 3.1.0) 67 | rspec-support (3.1.0) 68 | safe_yaml (1.0.3) 69 | slop (3.4.5) 70 | spoon (0.0.4) 71 | ffi 72 | thor (0.18.1) 73 | webmock (1.18.0) 74 | addressable (>= 2.3.6) 75 | crack (>= 0.3.2) 76 | 77 | PLATFORMS 78 | java 79 | ruby 80 | 81 | DEPENDENCIES 82 | guard-bundler 83 | guard-rspec 84 | rack-reverse-proxy! 85 | rack-test (~> 0.6) 86 | rake (~> 10.3) 87 | rspec (~> 3.1) 88 | webmock (~> 1.18) 89 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :rspec do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 7 | watch('spec/spec_helper.rb') { "spec" } 8 | end 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Jon Swope 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Reverse Proxy for Rack 2 | 3 | ## This repo has been moved to [waterlink/rack-reverse-proxy](https://github.com/waterlink/rack-reverse-proxy) 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) do |spec| 7 | spec.pattern = 'spec/**/*_spec.rb' 8 | end 9 | 10 | task :default => :spec 11 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.8.1 2 | -------------------------------------------------------------------------------- /lib/rack/exception.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class GenericProxyURI < Exception 3 | attr_reader :url 4 | 5 | def intialize(url) 6 | @url = url 7 | end 8 | 9 | def to_s 10 | %Q(Your URL "#{@url}" is too generic. Did you mean "http://#{@url}"?) 11 | end 12 | end 13 | 14 | class AmbiguousProxyMatch < Exception 15 | attr_reader :path, :matches 16 | def initialize(path, matches) 17 | @path = path 18 | @matches = matches 19 | end 20 | 21 | def to_s 22 | %Q(Path "#{path}" matched multiple endpoints: #{formatted_matches}) 23 | end 24 | 25 | private 26 | 27 | def formatted_matches 28 | matches.map {|matcher| matcher.to_s}.join(', ') 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/rack/reverse_proxy.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'net/https' 3 | require "rack-proxy" 4 | require "rack/reverse_proxy_matcher" 5 | require "rack/exception" 6 | 7 | module Rack 8 | class ReverseProxy 9 | include NewRelic::Agent::Instrumentation::ControllerInstrumentation if defined? NewRelic 10 | 11 | def initialize(app = nil, &b) 12 | @app = app || lambda {|env| [404, [], []] } 13 | @matchers = [] 14 | @global_options = {:preserve_host => true, :x_forwarded_host => true, :matching => :all, :replace_response_host => false} 15 | instance_eval &b if block_given? 16 | end 17 | 18 | def call(env) 19 | rackreq = Rack::Request.new(env) 20 | matcher = get_matcher(rackreq.fullpath, extract_http_request_headers(rackreq.env), rackreq) 21 | return @app.call(env) if matcher.nil? 22 | 23 | if @global_options[:newrelic_instrumentation] 24 | action_name = "#{rackreq.path.gsub(/\/\d+/,'/:id').gsub(/^\//,'')}/#{rackreq.request_method}" # Rack::ReverseProxy/foo/bar#GET 25 | perform_action_with_newrelic_trace(:name => action_name, :request => rackreq) do 26 | proxy(env, rackreq, matcher) 27 | end 28 | else 29 | proxy(env, rackreq, matcher) 30 | end 31 | end 32 | 33 | private 34 | 35 | def proxy(env, source_request, matcher) 36 | uri = matcher.get_uri(source_request.fullpath,env) 37 | if uri.nil? 38 | return @app.call(env) 39 | end 40 | options = @global_options.dup.merge(matcher.options) 41 | 42 | # Initialize request 43 | target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(uri.request_uri) 44 | 45 | # Setup headers 46 | target_request_headers = extract_http_request_headers(source_request.env) 47 | 48 | if options[:preserve_host] 49 | target_request_headers['HOST'] = "#{uri.host}:#{uri.port}" 50 | end 51 | 52 | if options[:x_forwarded_host] 53 | target_request_headers['X-Forwarded-Host'] = source_request.host 54 | target_request_headers['X-Forwarded-Port'] = "#{source_request.port}" 55 | end 56 | 57 | target_request.initialize_http_header(target_request_headers) 58 | 59 | # Basic auth 60 | target_request.basic_auth options[:username], options[:password] if options[:username] and options[:password] 61 | 62 | # Setup body 63 | if target_request.request_body_permitted? && source_request.body 64 | source_request.body.rewind 65 | target_request.body_stream = source_request.body 66 | end 67 | 68 | target_request.content_length = source_request.content_length || 0 69 | target_request.content_type = source_request.content_type if source_request.content_type 70 | 71 | # Create a streaming response (the actual network communication is deferred, a.k.a. streamed) 72 | target_response = HttpStreamingResponse.new(target_request, uri.host, uri.port) 73 | 74 | target_response.use_ssl = "https" == uri.scheme 75 | 76 | # Let rack set the transfer-encoding header 77 | response_headers = target_response.headers 78 | response_headers.delete('transfer-encoding') 79 | 80 | # Replace the location header with the proxy domain 81 | if response_headers['location'] && options[:replace_response_host] 82 | response_location = URI(response_headers['location'][0]) 83 | response_location.host = source_request.host 84 | response_headers['location'] = response_location.to_s 85 | end 86 | 87 | [target_response.status, response_headers, target_response.body] 88 | end 89 | 90 | def extract_http_request_headers(env) 91 | headers = env.reject do |k, v| 92 | !(/^HTTP_[A-Z_]+$/ === k) || v.nil? 93 | end.map do |k, v| 94 | [reconstruct_header_name(k), v] 95 | end.inject(Utils::HeaderHash.new) do |hash, k_v| 96 | k, v = k_v 97 | hash[k] = v 98 | hash 99 | end 100 | 101 | x_forwarded_for = (headers["X-Forwarded-For"].to_s.split(/, +/) << env["REMOTE_ADDR"]).join(", ") 102 | 103 | headers.merge!("X-Forwarded-For" => x_forwarded_for) 104 | end 105 | 106 | def reconstruct_header_name(name) 107 | name.sub(/^HTTP_/, "").gsub("_", "-") 108 | end 109 | 110 | def get_matcher(path, headers, rackreq) 111 | matches = @matchers.select do |matcher| 112 | matcher.match?(path, headers, rackreq) 113 | end 114 | 115 | if matches.length < 1 116 | nil 117 | elsif matches.length > 1 && @global_options[:matching] != :first 118 | raise AmbiguousProxyMatch.new(path, matches) 119 | else 120 | matches.first 121 | end 122 | end 123 | 124 | def reverse_proxy_options(options) 125 | @global_options=options 126 | end 127 | 128 | def reverse_proxy(matcher, url=nil, opts={}) 129 | raise GenericProxyURI.new(url) if matcher.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic 130 | @matchers << ReverseProxyMatcher.new(matcher,url,opts) 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/rack/reverse_proxy_matcher.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class ReverseProxyMatcher 3 | def initialize(matcher,url=nil,options) 4 | @default_url=url 5 | @url=url 6 | @options=options 7 | 8 | if matcher.kind_of?(String) 9 | @matcher = /^#{matcher.to_s}/ 10 | elsif matcher.respond_to?(:match) 11 | @matcher = matcher 12 | else 13 | raise "Invalid Matcher for reverse_proxy" 14 | end 15 | end 16 | 17 | attr_reader :matcher,:url, :default_url,:options 18 | 19 | def match?(path, *args) 20 | match_path(path, *args) ? true : false 21 | end 22 | 23 | def get_uri(path,env) 24 | return nil if url.nil? 25 | _url=(url.respond_to?(:call) ? url.call(env) : url.clone) 26 | if _url =~/\$\d/ 27 | match_path(path).to_a.each_with_index { |m, i| _url.gsub!("$#{i.to_s}", m) } 28 | URI(_url) 29 | else 30 | default_url.nil? ? URI.parse(_url) : URI.join(_url, path) 31 | end 32 | end 33 | 34 | def to_s 35 | %Q("#{matcher.to_s}" => "#{url}") 36 | end 37 | 38 | private 39 | def match_path(path, *args) 40 | headers = args[0] 41 | rackreq = args[1] 42 | arity = matcher.method(:match).arity 43 | if arity == -1 44 | match = matcher.match(path) 45 | else 46 | params = [path, (@options[:accept_headers] ? headers : nil), rackreq] 47 | match = matcher.match(*params[0..(arity - 1)]) 48 | end 49 | @url = match.url(path) if match && default_url.nil? 50 | match 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /rack-reverse-proxy.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'rack-reverse-proxy' 3 | s.version = "0.8.1" 4 | s.authors = ["Jon Swope", "Ian Ehlert", "Roman Ernst"] 5 | s.description = 'A Rack based reverse proxy for basic needs. Useful for testing or in cases where webserver configuration is unavailable.' 6 | s.email = ["jaswope@gmail.com", "ehlertij@gmail.com", "rernst@farbenmeer.net"] 7 | s.files = Dir['README.md', 'LICENSE', 'lib/**/*'] 8 | s.homepage = 'http://github.com/pex/rack-reverse-proxy' 9 | s.require_paths = ["lib"] 10 | s.summary = 'A Simple Reverse Proxy for Rack' 11 | 12 | s.add_development_dependency "rspec", "~> 3.1" 13 | s.add_development_dependency "rake", "~> 10.3" 14 | s.add_development_dependency "rack-test", "~> 0.6" 15 | s.add_development_dependency "webmock", "~> 1.18" 16 | s.add_development_dependency "guard-rspec" 17 | s.add_development_dependency "guard-bundler" 18 | 19 | s.add_dependency "rack", ">= 1.0.0" 20 | s.add_dependency "rack-proxy", "~> 0.5" 21 | end 22 | -------------------------------------------------------------------------------- /spec/rack/reverse_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Rack::ReverseProxy do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | Rack::ReverseProxy.new 8 | end 9 | 10 | def dummy_app 11 | lambda { |env| [200, {}, ['Dummy App']] } 12 | end 13 | 14 | describe "as middleware" do 15 | def app 16 | Rack::ReverseProxy.new(dummy_app) do 17 | reverse_proxy '/test', 'http://example.com/', {:preserve_host => true} 18 | reverse_proxy '/2test', lambda{ |env| 'http://example.com/'} 19 | end 20 | end 21 | 22 | it "should forward requests to the calling app when the path is not matched" do 23 | get '/' 24 | last_response.body.should == "Dummy App" 25 | last_response.should be_ok 26 | end 27 | 28 | it "should proxy requests when a pattern is matched" do 29 | stub_request(:get, 'http://example.com/test').to_return({:body => "Proxied App"}) 30 | get '/test' 31 | last_response.body.should == "Proxied App" 32 | end 33 | 34 | it "should proxy requests to a lambda url when a pattern is matched" do 35 | stub_request(:get, 'http://example.com/2test').to_return({:body => "Proxied App2"}) 36 | get '/2test' 37 | last_response.body.should == "Proxied App2" 38 | end 39 | 40 | it "should set the Host header" do 41 | stub_request(:any, 'example.com/test/stuff') 42 | get '/test/stuff' 43 | a_request(:get, 'http://example.com/test/stuff').with(:headers => {"Host" => "example.com:80"}).should have_been_made 44 | end 45 | 46 | it "should set the X-Forwarded-Host header to the proxying host by default" do 47 | stub_request(:any, 'example.com/test/stuff') 48 | get '/test/stuff' 49 | a_request(:get, 'http://example.com/test/stuff').with(:headers => {'X-Forwarded-Host' => 'example.org'}).should have_been_made 50 | end 51 | 52 | describe "with preserve host turned off" do 53 | def app 54 | Rack::ReverseProxy.new(dummy_app) do 55 | reverse_proxy '/test', 'http://example.com/', {:preserve_host => false} 56 | end 57 | end 58 | 59 | it "should not set the Host header" do 60 | stub_request(:any, 'example.com/test/stuff') 61 | get '/test/stuff' 62 | a_request(:get, 'http://example.com/test/stuff').with(:headers => {"Host" => "example.com"}).should_not have_been_made 63 | a_request(:get, 'http://example.com/test/stuff').should have_been_made 64 | end 65 | end 66 | 67 | describe "with x_forwarded_host turned off" do 68 | def app 69 | Rack::ReverseProxy.new(dummy_app) do 70 | reverse_proxy_options :x_forwarded_host => false 71 | reverse_proxy '/test', 'http://example.com/' 72 | end 73 | end 74 | 75 | it "should not set the X-Forwarded-Host header to the proxying host" do 76 | stub_request(:any, 'example.com/test/stuff') 77 | get '/test/stuff' 78 | a_request(:get, 'http://example.com/test/stuff').with(:headers => {'X-Forwarded-Host' => 'example.org'}).should_not have_been_made 79 | a_request(:get, 'http://example.com/test/stuff').should have_been_made 80 | end 81 | end 82 | 83 | describe "with basic auth turned on" do 84 | def app 85 | Rack::ReverseProxy.new(dummy_app) do 86 | reverse_proxy '/test', 'http://example.com/', {:username => "joe", :password => "shmoe"} 87 | end 88 | end 89 | 90 | it "should make request with basic auth" do 91 | stub_request(:get, "http://joe:shmoe@example.com/test/stuff").to_return(:body => "secured content") 92 | get '/test/stuff' 93 | last_response.body.should == "secured content" 94 | end 95 | end 96 | 97 | describe "with preserve response host turned on" do 98 | def app 99 | Rack::ReverseProxy.new(dummy_app) do 100 | reverse_proxy '/test', 'http://example.com/', {:replace_response_host => true} 101 | end 102 | end 103 | 104 | it "should replace the location response header" do 105 | stub_request(:get, "http://example.com/test/stuff").to_return(:headers => {"location" => "http://test.com/bar"}) 106 | get 'http://example.com/test/stuff' 107 | # puts last_response.headers.inspect 108 | last_response.headers['location'].should == "http://example.com/bar" 109 | end 110 | end 111 | 112 | describe "with ambiguous routes and all matching" do 113 | def app 114 | Rack::ReverseProxy.new(dummy_app) do 115 | reverse_proxy_options :matching => :all 116 | reverse_proxy '/test', 'http://example.com/' 117 | reverse_proxy /^\/test/, 'http://example.com/' 118 | end 119 | end 120 | 121 | it "should throw an exception" do 122 | lambda { get '/test' }.should raise_error(Rack::AmbiguousProxyMatch) 123 | end 124 | end 125 | 126 | describe "with ambiguous routes and first matching" do 127 | def app 128 | Rack::ReverseProxy.new(dummy_app) do 129 | reverse_proxy_options :matching => :first 130 | reverse_proxy '/test', 'http://example1.com/' 131 | reverse_proxy /^\/test/, 'http://example2.com/' 132 | end 133 | end 134 | 135 | it "should throw an exception" do 136 | stub_request(:get, 'http://example1.com/test').to_return({:body => "Proxied App"}) 137 | get '/test' 138 | last_response.body.should == "Proxied App" 139 | end 140 | end 141 | 142 | describe "with a route as a regular expression" do 143 | def app 144 | Rack::ReverseProxy.new(dummy_app) do 145 | reverse_proxy %r|^/test(/.*)$|, 'http://example.com$1' 146 | end 147 | end 148 | 149 | it "should support subcaptures" do 150 | stub_request(:get, 'http://example.com/path').to_return({:body => "Proxied App"}) 151 | get '/test/path' 152 | last_response.body.should == "Proxied App" 153 | end 154 | end 155 | 156 | describe "with a https route" do 157 | def app 158 | Rack::ReverseProxy.new(dummy_app) do 159 | reverse_proxy '/test', 'https://example.com' 160 | end 161 | end 162 | 163 | it "should make a secure request" do 164 | stub_request(:get, 'https://example.com/test/stuff').to_return({:body => "Proxied Secure App"}) 165 | get '/test/stuff' 166 | last_response.body.should == "Proxied Secure App" 167 | end 168 | 169 | end 170 | 171 | describe "with a route as a string" do 172 | def app 173 | Rack::ReverseProxy.new(dummy_app) do 174 | reverse_proxy '/test', 'http://example.com' 175 | reverse_proxy '/path', 'http://example.com/foo$0' 176 | end 177 | end 178 | 179 | it "should append the full path to the uri" do 180 | stub_request(:get, 'http://example.com/test/stuff').to_return({:body => "Proxied App"}) 181 | get '/test/stuff' 182 | last_response.body.should == "Proxied App" 183 | end 184 | 185 | end 186 | 187 | describe "with a generic url" do 188 | def app 189 | Rack::ReverseProxy.new(dummy_app) do 190 | reverse_proxy '/test', 'example.com' 191 | end 192 | end 193 | 194 | it "should throw an exception" do 195 | lambda{ app }.should raise_error(Rack::GenericProxyURI) 196 | end 197 | end 198 | 199 | describe "with a matching route" do 200 | def app 201 | Rack::ReverseProxy.new(dummy_app) do 202 | reverse_proxy '/test', 'http://example.com/' 203 | end 204 | end 205 | 206 | %w|get head delete put post|.each do |method| 207 | describe "and using method #{method}" do 208 | it "should forward the correct request" do 209 | stub_request(method.to_sym, 'http://example.com/test').to_return({:body => "Proxied App for #{method}"}) 210 | eval "#{method} '/test'" 211 | last_response.body.should == "Proxied App for #{method}" 212 | end 213 | 214 | if %w|put post|.include?(method) 215 | it "should forward the request payload" do 216 | stub_request(method.to_sym, 'http://example.com/test').to_return { |req| {:body => req.body} } 217 | eval "#{method} '/test', {:test => 'test'}" 218 | last_response.body.should == "test=test" 219 | end 220 | end 221 | end 222 | end 223 | end 224 | 225 | describe "with a matching class" do 226 | class Matcher 227 | def self.match(path) 228 | if path.match(/^\/(test|users)/) 229 | Matcher.new 230 | end 231 | end 232 | 233 | def url(path) 234 | if path.include?("user") 235 | 'http://users-example.com' + path 236 | else 237 | 'http://example.com' + path 238 | end 239 | end 240 | end 241 | 242 | def app 243 | Rack::ReverseProxy.new(dummy_app) do 244 | reverse_proxy Matcher 245 | end 246 | end 247 | 248 | it "should forward requests to the calling app when the path is not matched" do 249 | get '/' 250 | last_response.body.should == "Dummy App" 251 | last_response.should be_ok 252 | end 253 | 254 | it "should proxy requests when a pattern is matched" do 255 | stub_request(:get, 'http://example.com/test').to_return({:body => "Proxied App"}) 256 | stub_request(:get, 'http://users-example.com/users').to_return({:body => "User App"}) 257 | get '/test' 258 | last_response.body.should == "Proxied App" 259 | get '/users' 260 | last_response.body.should == "User App" 261 | end 262 | end 263 | 264 | describe "with a matching class" do 265 | class RequestMatcher 266 | attr_accessor :rackreq 267 | 268 | def initialize(rackreq) 269 | self.rackreq = rackreq 270 | end 271 | 272 | def self.match(path, headers, rackreq) 273 | if path.match(/^\/(test|users)/) 274 | RequestMatcher.new(rackreq) 275 | end 276 | end 277 | 278 | def url(path) 279 | if rackreq.params["user"] == 'omer' 280 | 'http://users-example.com' + path 281 | end 282 | end 283 | end 284 | 285 | def app 286 | Rack::ReverseProxy.new(dummy_app) do 287 | reverse_proxy RequestMatcher 288 | end 289 | end 290 | 291 | it "should forward requests to the calling app when the path is not matched" do 292 | get '/' 293 | last_response.body.should == "Dummy App" 294 | last_response.should be_ok 295 | end 296 | 297 | it "should proxy requests when a pattern is matched" do 298 | stub_request(:get, 'http://users-example.com/users?user=omer').to_return({:body => "User App"}) 299 | get '/test', user: "mark" 300 | last_response.body.should == "Dummy App" 301 | get '/users', user: 'omer' 302 | last_response.body.should == "User App" 303 | end 304 | end 305 | 306 | 307 | describe "with a matching class that accepts headers" do 308 | class MatcherHeaders 309 | def self.match(path, headers) 310 | if path.match(/^\/test/) && headers['ACCEPT'] && headers['ACCEPT'] == 'foo.bar' 311 | MatcherHeaders.new 312 | end 313 | end 314 | 315 | def url(path) 316 | 'http://example.com' + path 317 | end 318 | end 319 | 320 | def app 321 | Rack::ReverseProxy.new(dummy_app) do 322 | reverse_proxy MatcherHeaders, nil, {:accept_headers => true} 323 | end 324 | end 325 | 326 | it "should proxy requests when a pattern is matched and correct headers are passed" do 327 | stub_request(:get, 'http://example.com/test').to_return({:body => "Proxied App with Headers"}) 328 | get '/test', {}, {'HTTP_ACCEPT' => 'foo.bar'} 329 | last_response.body.should == "Proxied App with Headers" 330 | end 331 | 332 | it "should not proxy requests when a pattern is matched and incorrect headers are passed" do 333 | stub_request(:get, 'http://example.com/test').to_return({:body => "Proxied App with Headers"}) 334 | get '/test', {}, {'HTTP_ACCEPT' => 'bar.foo'} 335 | last_response.body.should_not == "Proxied App with Headers" 336 | end 337 | end 338 | end 339 | 340 | describe "as a rack app" do 341 | it "should respond with 404 when the path is not matched" do 342 | get '/' 343 | last_response.should be_not_found 344 | end 345 | end 346 | 347 | end 348 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rack/reverse_proxy' 3 | require 'rspec' 4 | require 'rack/test' 5 | require 'webmock/rspec' 6 | # Patch HttpStreamingResponse to make rack-proxy compatible with webmocks 7 | require 'support/http_streaming_response_patch' 8 | 9 | $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib') 10 | $LOAD_PATH << File.join(File.dirname(__FILE__)) 11 | 12 | RSpec.configure do |config| 13 | config.expect_with :rspec do |expectations| 14 | # This option will default to `true` in RSpec 4. 15 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 16 | expectations.syntax = [:should] 17 | end 18 | config.mock_with :rspec do |mocks| 19 | mocks.verify_doubled_constant_names = true 20 | mocks.verify_partial_doubles = true 21 | mocks.syntax = [:should] 22 | # Prevents you from mocking or stubbing a method that does not exist on 23 | # a real object. This is generally recommended, and will default to 24 | # `true` in RSpec 4. 25 | mocks.verify_partial_doubles = true 26 | end 27 | 28 | WebMock.disable_net_connect! 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/http_streaming_response_patch.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Patch HttpStreamingResponse 3 | # in order to support webmocks and still use rack-proxy 4 | # 5 | # Inspired by @ehlertij commits on sportngin/rack-proxy: 6 | # 616574e452fa731f5427d2ff2aff6823fcf28bde 7 | # d8c377f7485997b229ced23c33cfef87d3fb8693 8 | # 75b446a26ceb519ddc28f38b33309e9a2799074c 9 | # 10 | module Rack 11 | class HttpStreamingResponse 12 | def each(&block) 13 | response.read_body(&block) 14 | ensure 15 | session.end_request_hacked unless mocking? 16 | end 17 | 18 | protected 19 | 20 | def response 21 | if mocking? 22 | @response ||= session.request(@request) 23 | else 24 | super 25 | end 26 | end 27 | 28 | def mocking? 29 | defined?(WebMock) || defined?(FakeWeb) 30 | end 31 | end 32 | end 33 | --------------------------------------------------------------------------------