rack-proxy-0.6.1/0000755000175600017570000000000013071366676012675 5ustar pravipravirack-proxy-0.6.1/LICENSE0000644000175600017570000000211613071366676013702 0ustar pravipraviThe MIT License (MIT) Copyright (c) 2013 Jacek Becela jacek.becela@gmail.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rack-proxy-0.6.1/.travis.yml0000644000175600017570000000047213071366676015011 0ustar pravipravisudo: false cache: bundler language: ruby before_install: - "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc" - gem install bundler - gem update bundler script: bundle exec rake test rvm: - 2.0.0 - 2.1.5 - 2.2.2 - 2.2.3 - 2.3.0 - 2.3.1 env: - RAILS_ENV=test RACK_ENV=test notifications: email: false rack-proxy-0.6.1/rack-proxy.gemspec0000644000175600017570000000165613071366676016351 0ustar pravipravi# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "rack-proxy" Gem::Specification.new do |s| s.name = "rack-proxy" s.version = Rack::Proxy::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Jacek Becela"] s.email = ["jacek.becela@gmail.com"] s.homepage = "http://rubygems.org/gems/rack-proxy" s.summary = %q{A request/response rewriting HTTP proxy. A Rack app.} s.description = %q{A Rack app that provides request/response rewriting proxy capabilities with streaming.} s.rubyforge_project = "rack-proxy" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] s.add_dependency("rack") s.add_development_dependency("rack-test") s.add_development_dependency("test-unit") end rack-proxy-0.6.1/README.md0000644000175600017570000000375513071366676014166 0ustar pravipraviA request/response rewriting HTTP proxy. A Rack app. Subclass `Rack::Proxy` and provide your `rewrite_env` and `rewrite_response` methods. Example ------- ```ruby class Foo < Rack::Proxy def rewrite_env(env) env["HTTP_HOST"] = "example.com" env end def rewrite_response(triplet) status, headers, body = triplet headers["X-Foo"] = "Bar" triplet end end ``` ### Disable SSL session verification when proxying a server with e.g. self-signed SSL certs ```ruby class TrustingProxy < Rack::Proxy def rewrite_env(env) env["rack.ssl_verify_none"] = true env end end ``` The same can be achieved for *all* requests going through the `Rack::Proxy` instance by using ```ruby Rack::Proxy.new(ssl_verify_none: true) ``` Using it as a middleware: ------------------------- Example: Proxying only requests that end with ".php" could be done like this: ```ruby require 'rack/proxy' class RackPhpProxy < Rack::Proxy def perform_request(env) request = Rack::Request.new(env) if request.path =~ %r{\.php} env["HTTP_HOST"] = "localhost" env["REQUEST_PATH"] = "/php/#{request.fullpath}" super(env) else @app.call(env) end end end ``` To use the middleware, please consider the following: 1) For Rails we could add a configuration in config/application.rb ```ruby config.middleware.use RackPhpProxy, {ssl_verify_none: true} ``` 2) For Sinatra or any Rack-based application: ```ruby class MyAwesomeSinatra < Sinatra::Base use RackPhpProxy, {ssl_verify_none: true} end ``` This will allow to run the other requests through the application and only proxy the requests that match the condition from the middleware. See tests for more examples. WARNING ------- Doesn't work with fakeweb/webmock. Both libraries monkey-patch net/http code. Todos ----- - Make the docs up to date with the current use case for this code: everything except streaming which involved a rather ugly monkey patch and only worked in 1.8, but does not work now. rack-proxy-0.6.1/test/0000755000175600017570000000000013071366676013654 5ustar pravipravirack-proxy-0.6.1/test/test_helper.rb0000644000175600017570000000030513071366676016515 0ustar pravipravirequire "rubygems" require 'bundler/setup' require 'bundler/gem_tasks' require "test/unit" require "rack" require "rack/test" Test::Unit::TestCase.class_eval do include Rack::Test::Methods end rack-proxy-0.6.1/test/http_streaming_response_test.rb0000644000175600017570000000201113071366676022200 0ustar pravipravirequire "test_helper" require "rack/http_streaming_response" class HttpStreamingResponseTest < Test::Unit::TestCase def setup host, req = "www.trix.pl", Net::HTTP::Get.new("/") @response = Rack::HttpStreamingResponse.new(req, host) end def test_streaming # Response status assert @response.code == 200 assert @response.status == 200 # Headers headers = @response.headers assert headers.size > 0 assert headers["content-type"] == ["text/html;charset=utf-8"] assert headers["CoNtEnT-TyPe"] == headers["content-type"] assert headers["content-length"].first.to_i > 0 # Body chunks = [] @response.body.each do |chunk| chunks << chunk end assert chunks.size > 0 chunks.each do |chunk| assert chunk.is_a?(String) end end def test_to_s assert_equal @response.headers["Content-Length"].first.to_i, @response.body.to_s.size end def test_to_s_called_twice body = @response.body assert_equal body.to_s, body.to_s end end rack-proxy-0.6.1/test/net_http_hacked_test.rb0000644000175600017570000000133213071366676020363 0ustar pravipravirequire "test_helper" require "net_http_hacked" class NetHttpHackedTest < Test::Unit::TestCase def test_net_http_hacked req = Net::HTTP::Get.new("/") http = Net::HTTP.start("www.iana.org", "80") # Response code res = http.begin_request_hacked(req) assert res.code == "200" # Headers headers = {} res.each_header { |k, v| headers[k] = v } assert headers.size > 0 assert headers["content-type"] == "text/html; charset=UTF-8" assert !headers["date"].nil? # Body chunks = [] res.read_body do |chunk| chunks << chunk end assert chunks.size > 0 chunks.each do |chunk| assert chunk.is_a?(String) end http.end_request_hacked end end rack-proxy-0.6.1/test/rack_proxy_test.rb0000644000175600017570000000653013071366676017425 0ustar pravipravirequire "test_helper" require "rack/proxy" class RackProxyTest < Test::Unit::TestCase class HostProxy < Rack::Proxy attr_accessor :host def rewrite_env(env) env["HTTP_HOST"] = self.host || 'www.trix.pl' env end end def app(opts = {}) return @app ||= HostProxy.new(opts) end def test_http_streaming get "/" assert last_response.ok? assert_match(/Jacek Becela/, last_response.body) end def test_http_full_request app(:streaming => false) get "/" assert last_response.ok? assert_match(/Jacek Becela/, last_response.body) end def test_http_full_request_headers app(:streaming => false) app.host = 'www.google.com' get "/" assert !Array(last_response['Set-Cookie']).empty?, 'Google always sets a cookie, yo. Where my cookies at?!' end def test_https_streaming app.host = 'www.apple.com' get 'https://example.com' assert last_response.ok? assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body) end def test_https_streaming_tls app(:ssl_version => :TLSv1).host = 'www.apple.com' get 'https://example.com' assert last_response.ok? assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body) end def test_https_full_request app(:streaming => false).host = 'www.apple.com' get 'https://example.com' assert last_response.ok? assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body) end def test_https_full_request_tls app({:streaming => false, :ssl_version => :TLSv1}).host = 'www.apple.com' get 'https://example.com' assert last_response.ok? assert_match(/(itunes|iphone|ipod|mac|ipad)/, last_response.body) end def test_normalize_headers proxy_class = Rack::Proxy headers = { 'header_array' => ['first_entry'], 'header_non_array' => :entry } normalized_headers = proxy_class.send(:normalize_headers, headers) assert normalized_headers.instance_of?(Rack::Utils::HeaderHash) assert normalized_headers['header_array'] == 'first_entry' assert normalized_headers['header_non_array'] == :entry end def test_header_reconstruction proxy_class = Rack::Proxy header = proxy_class.send(:reconstruct_header_name, "HTTP_ABC") assert header == "ABC" header = proxy_class.send(:reconstruct_header_name, "HTTP_ABC_D") assert header == "ABC-D" end def test_extract_http_request_headers proxy_class = Rack::Proxy env = { 'NOT-HTTP-HEADER' => 'test-value', 'HTTP_ACCEPT' => 'text/html', 'HTTP_CONNECTION' => nil, 'HTTP_CONTENT_MD5' => 'deadbeef' } headers = proxy_class.extract_http_request_headers(env) assert headers.key?('ACCEPT') assert headers.key?('CONTENT-MD5') assert !headers.key?('CONNECTION') assert !headers.key?('NOT-HTTP-HEADER') end def test_duplicate_headers proxy_class = Rack::Proxy env = { 'Set-Cookie' => ["cookie1=foo", "cookie2=bar"] } headers = proxy_class.normalize_headers(env) assert headers['Set-Cookie'].include?('cookie1=foo'), "Include the first value" assert headers['Set-Cookie'].include?("\n"), "Join multiple cookies with newlines" assert headers['Set-Cookie'].include?('cookie2=bar'), "Include the second value" end def test_handles_missing_content_length assert_nothing_thrown do post "/", nil, "CONTENT_LENGTH" => nil end end end rack-proxy-0.6.1/Gemfile0000644000175600017570000000015213071366676014166 0ustar pravipravisource "http://rubygems.org" gem 'rake' # Specify your gem's dependencies in rack-proxy.gemspec gemspec rack-proxy-0.6.1/lib/0000755000175600017570000000000013071366676013443 5ustar pravipravirack-proxy-0.6.1/lib/rack-proxy.rb0000644000175600017570000000002413071366676016063 0ustar pravipravirequire "rack/proxy"rack-proxy-0.6.1/lib/net_http_hacked.rb0000644000175600017570000000464413071366676017124 0ustar pravipravi# We are hacking net/http to change semantics of streaming handling # from "block" semantics to regular "return" semnatics. # We need it to construct a streamable rack triplet: # # [status, headers, streamable_body] # # See http://github.com/aniero/rack-streaming-proxy # for alternative that uses additional process. # # BTW I don't like monkey patching either # but this is not real monkey patching. # I just added some methods and named them very uniquely # to avoid eventual conflicts. You're safe. Trust me. # # Also, in Ruby 1.9.2 you could use Fibers to avoid hacking net/http. require 'net/https' class Net::HTTP # Original #request with block semantics. # # def request(req, body = nil, &block) # unless started? # start { # req['connection'] ||= 'close' # return request(req, body, &block) # } # end # if proxy_user() # unless use_ssl? # req.proxy_basic_auth proxy_user(), proxy_pass() # end # end # # req.set_body_internal body # begin_transport req # req.exec @socket, @curr_http_version, edit_path(req.path) # begin # res = HTTPResponse.read_new(@socket) # end while res.kind_of?(HTTPContinue) # res.reading_body(@socket, req.response_body_permitted?) { # yield res if block_given? # } # end_transport req, res # # res # end def begin_request_hacked(req) begin_transport req req.exec @socket, @curr_http_version, edit_path(req.path) begin res = Net::HTTPResponse.read_new(@socket) end while res.kind_of?(Net::HTTPContinue) res.begin_reading_body_hacked(@socket, req.response_body_permitted?) @req_hacked, @res_hacked = req, res @res_hacked end def end_request_hacked @res_hacked.end_reading_body_hacked end_transport @req_hacked, @res_hacked @res_hacked end end class Net::HTTPResponse # Original #reading_body with block semantics # # def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only # @socket = sock # @body_exist = reqmethodallowbody && self.class.body_permitted? # begin # yield # self.body # ensure to read body # ensure # @socket = nil # end # end def begin_reading_body_hacked(sock, reqmethodallowbody) @socket = sock @body_exist = reqmethodallowbody && self.class.body_permitted? end def end_reading_body_hacked self.body @socket = nil end end rack-proxy-0.6.1/lib/rack/0000755000175600017570000000000013071366676014363 5ustar pravipravirack-proxy-0.6.1/lib/rack/proxy.rb0000644000175600017570000001011113071366676016063 0ustar pravipravirequire "net_http_hacked" require "rack/http_streaming_response" module Rack # Subclass and bring your own #rewrite_request and #rewrite_response class Proxy VERSION = "0.6.1" class << self def extract_http_request_headers(env) headers = env.reject do |k, v| !(/^HTTP_[A-Z0-9_]+$/ === k) || v.nil? end.map do |k, v| [reconstruct_header_name(k), v] end.inject(Utils::HeaderHash.new) do |hash, k_v| k, v = k_v hash[k] = v hash end x_forwarded_for = (headers["X-Forwarded-For"].to_s.split(/, +/) << env["REMOTE_ADDR"]).join(", ") headers.merge!("X-Forwarded-For" => x_forwarded_for) end def normalize_headers(headers) mapped = headers.map do |k, v| [k, if v.is_a? Array then v.join("\n") else v end] end Utils::HeaderHash.new Hash[mapped] end protected def reconstruct_header_name(name) name.sub(/^HTTP_/, "").gsub("_", "-") end end # @option opts [String, URI::HTTP] :backend Backend host to proxy requests to def initialize(app = nil, opts= {}) if app.is_a?(Hash) opts = app @app = nil else @app = app end @streaming = opts.fetch(:streaming, true) @ssl_verify_none = opts.fetch(:ssl_verify_none, false) @backend = URI(opts[:backend]) if opts[:backend] @read_timeout = opts.fetch(:read_timeout, 60) @ssl_version = opts[:ssl_version] if opts[:ssl_version] end def call(env) rewrite_response(perform_request(rewrite_env(env))) end # Return modified env def rewrite_env(env) env end # Return a rack triplet [status, headers, body] def rewrite_response(triplet) triplet end protected def perform_request(env) source_request = Rack::Request.new(env) # Initialize request if source_request.fullpath == "" full_path = URI.parse(env['REQUEST_URI']).request_uri else full_path = source_request.fullpath end target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(full_path) # Setup headers target_request.initialize_http_header(self.class.extract_http_request_headers(source_request.env)) # Setup body if target_request.request_body_permitted? && source_request.body target_request.body_stream = source_request.body target_request.content_length = source_request.content_length.to_i target_request.content_type = source_request.content_type if source_request.content_type target_request.body_stream.rewind end backend = env.delete('rack.backend') || @backend || source_request use_ssl = backend.scheme == "https" ssl_verify_none = (env.delete('rack.ssl_verify_none') || @ssl_verify_none) == true read_timeout = env.delete('http.read_timeout') || @read_timeout # Create the response if @streaming # streaming response (the actual network communication is deferred, a.k.a. streamed) target_response = HttpStreamingResponse.new(target_request, backend.host, backend.port) target_response.use_ssl = use_ssl target_response.read_timeout = read_timeout target_response.verify_mode = OpenSSL::SSL::VERIFY_NONE if use_ssl && ssl_verify_none target_response.ssl_version = @ssl_version if @ssl_version else http = Net::HTTP.new(backend.host, backend.port) http.use_ssl = use_ssl if use_ssl http.read_timeout = read_timeout http.verify_mode = OpenSSL::SSL::VERIFY_NONE if use_ssl && ssl_verify_none http.ssl_version = @ssl_version if @ssl_version target_response = http.start do http.request(target_request) end end headers = (target_response.respond_to?(:headers) && target_response.headers) || self.class.normalize_headers(target_response.to_hash) body = target_response.body || [""] body = [body] unless body.respond_to?(:each) [target_response.code, headers, body] end end end rack-proxy-0.6.1/lib/rack/http_streaming_response.rb0000644000175600017570000000255113071366676021661 0ustar pravipravirequire "net_http_hacked" module Rack # Wraps the hacked net/http in a Rack way. class HttpStreamingResponse attr_accessor :use_ssl attr_accessor :verify_mode attr_accessor :read_timeout attr_accessor :ssl_version def initialize(request, host, port = nil) @request, @host, @port = request, host, port end def body self end def code response.code.to_i end # #status is deprecated alias_method :status, :code def headers h = Utils::HeaderHash.new response.to_hash.each do |k, v| h[k] = v end h end # Can be called only once! def each(&block) response.read_body(&block) ensure session.end_request_hacked session.finish end def to_s @body ||= begin lines = [] each do |line| lines << line end lines.join end end protected # Net::HTTPResponse def response @response ||= session.begin_request_hacked(@request) end # Net::HTTP def session @session ||= begin http = Net::HTTP.new @host, @port http.use_ssl = self.use_ssl http.verify_mode = self.verify_mode http.read_timeout = self.read_timeout http.ssl_version = self.ssl_version if self.use_ssl http.start end end end end rack-proxy-0.6.1/Gemfile.lock0000644000175600017570000000053613071366676015123 0ustar pravipraviPATH remote: . specs: rack-proxy (0.6.1) rack GEM remote: http://rubygems.org/ specs: power_assert (0.2.6) rack (1.2.1) rack-test (0.5.6) rack (>= 1.0) rake (0.9.2.2) test-unit (3.1.5) power_assert PLATFORMS ruby DEPENDENCIES rack-proxy! rack-test rake test-unit BUNDLED WITH 1.14.6 rack-proxy-0.6.1/Rakefile0000644000175600017570000000040313071366676014337 0ustar pravipravirequire 'rubygems' require 'bundler' Bundler::GemHelper.install_tasks require "rake/testtask" task :test do Rake::TestTask.new do |t| t.libs << "test" t.test_files = FileList['test/*_test.rb'] t.verbose = true end end task :default => :test rack-proxy-0.6.1/.gitignore0000644000175600017570000000002413071366676014661 0ustar pravipravipkg/* *.gem .bundle