Import Upstream version 0.6.1

This commit is contained in:
Praveen Arimbrathodiyil 2017-04-06 13:02:53 +05:30
commit a803b2684b
16 changed files with 721 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
pkg/*
*.gem
.bundle

19
.travis.yml Normal file
View File

@ -0,0 +1,19 @@
sudo: 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

6
Gemfile Normal file
View File

@ -0,0 +1,6 @@
source "http://rubygems.org"
gem 'rake'
# Specify your gem's dependencies in rack-proxy.gemspec
gemspec

28
Gemfile.lock Normal file
View File

@ -0,0 +1,28 @@
PATH
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

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The 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.

96
README.md Normal file
View File

@ -0,0 +1,96 @@
A 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.

14
Rakefile Normal file
View File

@ -0,0 +1,14 @@
require '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

90
lib/net_http_hacked.rb Normal file
View File

@ -0,0 +1,90 @@
# 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

1
lib/rack-proxy.rb Normal file
View File

@ -0,0 +1 @@
require "rack/proxy"

View File

@ -0,0 +1,77 @@
require "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

129
lib/rack/proxy.rb Normal file
View File

@ -0,0 +1,129 @@
require "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

25
rack-proxy.gemspec Normal file
View File

@ -0,0 +1,25 @@
# -*- 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

View File

@ -0,0 +1,47 @@
require "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

View File

@ -0,0 +1,36 @@
require "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

118
test/rack_proxy_test.rb Normal file
View File

@ -0,0 +1,118 @@
require "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

11
test/test_helper.rb Normal file
View File

@ -0,0 +1,11 @@
require "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