rack-reverse-proxy 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.document +5 -0
- data/.gitignore +16 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +20 -0
- data/README.md +30 -4
- data/Rakefile +10 -0
- data/lib/rack/reverse_proxy.rb +3 -128
- data/lib/rack_reverse_proxy.rb +8 -0
- data/lib/rack_reverse_proxy/errors.rb +36 -0
- data/lib/rack_reverse_proxy/middleware.rb +40 -0
- data/lib/rack_reverse_proxy/response_builder.rb +60 -0
- data/lib/rack_reverse_proxy/roundtrip.rb +263 -0
- data/lib/rack_reverse_proxy/rule.rb +182 -0
- data/lib/rack_reverse_proxy/version.rb +4 -0
- data/rack-reverse-proxy.gemspec +42 -0
- data/script/rubocop +5 -0
- data/spec/rack/reverse_proxy_spec.rb +586 -0
- data/spec/rack_reverse_proxy/response_builder_spec.rb +37 -0
- data/spec/spec_helper.rb +106 -0
- data/spec/support/http_streaming_response_patch.rb +32 -0
- metadata +58 -42
- data/lib/rack/exception.rb +0 -31
- data/lib/rack/reverse_proxy/http_streaming_response.rb +0 -7
- data/lib/rack/reverse_proxy_matcher.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff26702f215fd146c4a043bc583a058f7102001b
|
4
|
+
data.tar.gz: c451349b33d1715e57ce4da1652bba7f4a7b9192
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0916fd37a7f4f5c4a63d3a51d01ad07f60c6a687fa45db392f1816dcb63dd82608b466f6451b7a169f7c66b252dceb697e0e0bdd8c86cad4a7fa145ce9895a4a
|
7
|
+
data.tar.gz: fb4a7b6158ffb9ae52c6689d0e71f95fdcbb1b0a1e131e7d3e5c7c2677096121af205b04be922d1521a075a35ae5ab9ba8ae9875ffab3aece726763a3b27a50c
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Style/StringLiterals:
|
2
|
+
EnforcedStyle: double_quotes
|
3
|
+
|
4
|
+
Metrics/LineLength:
|
5
|
+
Max: 100
|
6
|
+
|
7
|
+
#begin ruby 1.8 support
|
8
|
+
Style/HashSyntax:
|
9
|
+
EnforcedStyle: hash_rockets
|
10
|
+
|
11
|
+
Style/Lambda:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Style/TrailingCommaInLiteral:
|
15
|
+
EnforcedStyleForMultiline: no_comma
|
16
|
+
|
17
|
+
Style/TrailingCommaInArguments:
|
18
|
+
EnforcedStyleForMultiline: no_comma
|
19
|
+
|
20
|
+
Style/EachWithObject:
|
21
|
+
Enabled: false
|
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.8.7
|
4
|
+
- 1.9.3
|
5
|
+
- 2.0.0
|
6
|
+
- 2.1
|
7
|
+
- 2.2
|
8
|
+
- jruby-18mode
|
9
|
+
- jruby-19mode
|
10
|
+
- rbx
|
11
|
+
|
12
|
+
before_install:
|
13
|
+
- gem install bundler
|
14
|
+
|
15
|
+
bundler_args: --without development
|
16
|
+
|
17
|
+
script:
|
18
|
+
- bundle exec rspec
|
19
|
+
- script/rubocop
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.10.0
|
4
|
+
|
5
|
+
- Feature: `options[:verify_mode]` to set SSL verification mode. - [Marv Cool](https://github.com/MrMarvin) [#24](https://github.com/waterlink/rack-reverse-proxy/pull/24) and [#25](https://github.com/waterlink/rack-reverse-proxy/pull/25)
|
6
|
+
|
7
|
+
## 0.9.1
|
8
|
+
|
9
|
+
- Enhancement: Remove `Status` key from response headers as per Rack protocol (see [rack/lint](https://github.com/rack/rack/blob/master/lib/rack/lint.rb#L639)) - [Jan Raasch](https://github.com/janraasch) [#7](https://github.com/waterlink/rack-reverse-proxy/pull/7)
|
10
|
+
|
11
|
+
## 0.9.0
|
12
|
+
|
13
|
+
- Bugfix: Timeout option matches the documentation - [Paul Hepworth](https://github.com/peppyheppy)
|
14
|
+
- Ruby 1.8 compatibility - [anujdas](https://github.com/anujdas)
|
15
|
+
- Bugfix: Omit port in host header for default ports (80, 443), so that it doesn't break some web servers, like "Apache Coyote" - [Peter Suschlik](https://github.com/splattael)
|
16
|
+
- Bugfix: Don't drop source request's port in response's location header - [Eric Koslow](https://github.com/ekosz)
|
17
|
+
- Bugfix: Capitalize headers correctly to prevent duplicate headers when used together with other proxies - [Eric Koslow](https://github.com/ekosz)
|
18
|
+
- Bugfix: Normalize headers from HttpStreamingResponse in order not to break other middlewares - [Jan Raasch](https://github.com/janraasch)
|
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
ruby_version = RUBY_VERSION.to_f
|
6
|
+
rubocop_platform = [:ruby_20, :ruby_21, :ruby_22]
|
7
|
+
rubocop_platform = [:ruby_20, :ruby_21] if ruby_version < 2.0
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem "rspec"
|
11
|
+
gem "rack-test"
|
12
|
+
gem "webmock"
|
13
|
+
gem "rubocop", :platform => rubocop_platform
|
14
|
+
|
15
|
+
gem "addressable", "< 2.4" if ruby_version < 1.9
|
16
|
+
end
|
17
|
+
|
18
|
+
group :development, :test do
|
19
|
+
gem "simplecov"
|
20
|
+
end
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
This is a simple reverse proxy for Rack that pretty heavily rips off Rack Forwarder. It is not meant for production systems (although it may work), as the webserver fronting your app is generally much better at this sort of thing.
|
5
5
|
|
6
6
|
## Installation
|
7
|
-
The gem is available on
|
7
|
+
The gem is available on rubygems. Assuming you have a recent version of Rubygems you should just be able to install it via:
|
8
8
|
|
9
9
|
```
|
10
10
|
gem install rack-reverse-proxy
|
@@ -17,9 +17,10 @@ gem "rack-reverse-proxy", require: "rack/reverse_proxy"
|
|
17
17
|
```
|
18
18
|
|
19
19
|
## Usage
|
20
|
-
Matchers can be a regex or a string. If a regex is used, you can use the subcaptures in your forwarding url by denoting them with a `$`.
|
21
20
|
|
22
|
-
|
21
|
+
Rules can be a regex or a string. If a regex is used, you can use the subcaptures in your forwarding url by denoting them with a `$`.
|
22
|
+
|
23
|
+
Right now if more than one rule matches any given route, it throws an exception for an ambiguous match. This will probably change later. If no match is found, the call is forwarded to your application.
|
23
24
|
|
24
25
|
Below is an example for configuring the middleware:
|
25
26
|
|
@@ -43,12 +44,37 @@ end
|
|
43
44
|
run app
|
44
45
|
```
|
45
46
|
|
46
|
-
reverse_proxy_options sets global options for all reverse proxies. Available options are:
|
47
|
+
`reverse_proxy_options` sets global options for all reverse proxies. Available options are:
|
48
|
+
|
47
49
|
* `:preserve_host` Set to false to omit Host headers
|
48
50
|
* `:username` username for basic auth
|
49
51
|
* `:password` password for basic auth
|
50
52
|
* `:matching` is a global only option, if set to :first the first matched url will be requested (no ambigous error). Default: :all.
|
51
53
|
* `:timeout` seconds to timout the requests
|
54
|
+
* `:force_ssl` redirects to ssl version, if not already using it (requires `:replace_response_host`). Default: false.
|
55
|
+
* `:verify_mode` the `OpenSSL::SSL` verify mode passed to Net::HTTP. Default: `OpenSSL::SSL::VERIFY_PEER`.
|
56
|
+
|
57
|
+
### Sample usage in a Ruby on Rails app
|
58
|
+
|
59
|
+
Rails 3 or less:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
# config/application.rb
|
63
|
+
config.middleware.insert_before(Rack::Lock, Rack::ReverseProxy) do
|
64
|
+
reverse_proxy_options preserve_host: true
|
65
|
+
reverse_proxy '/wiki', 'http://wiki.example.com/'
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
Rails 4+ or if you use `config.threadsafe`, you'll need to `insert_before(Rack::Runtime, Rack::ReverseProxy)` as `Rack::Lock` does not exist when `config.allow_concurrency == true`:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# config/application.rb
|
73
|
+
config.middleware.insert_before(Rack::Runtime, Rack::ReverseProxy) do
|
74
|
+
reverse_proxy_options preserve_host: true
|
75
|
+
reverse_proxy '/wiki', 'http://wiki.example.com/'
|
76
|
+
end
|
77
|
+
```
|
52
78
|
|
53
79
|
## Note on Patches/Pull Requests
|
54
80
|
* Fork the project.
|
data/Rakefile
ADDED
data/lib/rack/reverse_proxy.rb
CHANGED
@@ -1,131 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require 'net/https'
|
3
|
-
require "rack-proxy"
|
4
|
-
require "rack/reverse_proxy_matcher"
|
5
|
-
require "rack/exception"
|
6
|
-
require "rack/reverse_proxy/http_streaming_response"
|
1
|
+
require "rack_reverse_proxy"
|
7
2
|
|
3
|
+
# Re-opening Rack module only to define ReverseProxy constant
|
8
4
|
module Rack
|
9
|
-
|
10
|
-
include NewRelic::Agent::Instrumentation::ControllerInstrumentation if defined? NewRelic
|
11
|
-
|
12
|
-
def initialize(app = nil, &b)
|
13
|
-
@app = app || lambda {|env| [404, [], []] }
|
14
|
-
@matchers = []
|
15
|
-
@global_options = {:preserve_host => true, :x_forwarded_host => true, :matching => :all, :replace_response_host => false}
|
16
|
-
instance_eval(&b) if block_given?
|
17
|
-
end
|
18
|
-
|
19
|
-
def call(env)
|
20
|
-
rackreq = Rack::Request.new(env)
|
21
|
-
matcher = get_matcher(rackreq.fullpath, Proxy.extract_http_request_headers(rackreq.env), rackreq)
|
22
|
-
return @app.call(env) if matcher.nil?
|
23
|
-
|
24
|
-
if @global_options[:newrelic_instrumentation]
|
25
|
-
action_name = "#{rackreq.path.gsub(/\/\d+/,'/:id').gsub(/^\//,'')}/#{rackreq.request_method}" # Rack::ReverseProxy/foo/bar#GET
|
26
|
-
perform_action_with_newrelic_trace(:name => action_name, :request => rackreq) do
|
27
|
-
proxy(env, rackreq, matcher)
|
28
|
-
end
|
29
|
-
else
|
30
|
-
proxy(env, rackreq, matcher)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def proxy(env, source_request, matcher)
|
37
|
-
uri = matcher.get_uri(source_request.fullpath,env)
|
38
|
-
if uri.nil?
|
39
|
-
return @app.call(env)
|
40
|
-
end
|
41
|
-
options = @global_options.dup.merge(matcher.options)
|
42
|
-
|
43
|
-
# Initialize request
|
44
|
-
target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(uri.request_uri)
|
45
|
-
|
46
|
-
# Setup headers
|
47
|
-
target_request_headers = Proxy.extract_http_request_headers(source_request.env)
|
48
|
-
|
49
|
-
if options[:preserve_host]
|
50
|
-
if uri.port == uri.default_port
|
51
|
-
target_request_headers['HOST'] = uri.host
|
52
|
-
else
|
53
|
-
target_request_headers['HOST'] = "#{uri.host}:#{uri.port}"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
if options[:x_forwarded_host]
|
58
|
-
target_request_headers['X-Forwarded-Host'] = source_request.host
|
59
|
-
target_request_headers['X-Forwarded-Port'] = "#{source_request.port}"
|
60
|
-
end
|
61
|
-
|
62
|
-
target_request.initialize_http_header(target_request_headers)
|
63
|
-
|
64
|
-
# Basic auth
|
65
|
-
target_request.basic_auth options[:username], options[:password] if options[:username] and options[:password]
|
66
|
-
|
67
|
-
# Setup body
|
68
|
-
if target_request.request_body_permitted? && source_request.body
|
69
|
-
source_request.body.rewind
|
70
|
-
target_request.body_stream = source_request.body
|
71
|
-
end
|
72
|
-
|
73
|
-
target_request.content_length = source_request.content_length || 0
|
74
|
-
target_request.content_type = source_request.content_type if source_request.content_type
|
75
|
-
|
76
|
-
# Create a streaming response (the actual network communication is deferred, a.k.a. streamed)
|
77
|
-
target_response = HttpStreamingResponse.new(target_request, uri.host, uri.port)
|
78
|
-
|
79
|
-
# pass the timeout configuration through
|
80
|
-
target_response.set_read_timeout(options[:timeout]) if options[:timeout].to_i > 0
|
81
|
-
|
82
|
-
target_response.use_ssl = "https" == uri.scheme
|
83
|
-
|
84
|
-
# Let rack set the transfer-encoding header
|
85
|
-
response_headers = Rack::Utils::HeaderHash.new Proxy.normalize_headers(format_headers(target_response.headers))
|
86
|
-
response_headers.delete('Transfer-Encoding')
|
87
|
-
response_headers.delete('Status')
|
88
|
-
|
89
|
-
# Replace the location header with the proxy domain
|
90
|
-
if response_headers['Location'] && options[:replace_response_host]
|
91
|
-
response_location = URI(response_headers['location'])
|
92
|
-
response_location.host = source_request.host
|
93
|
-
response_location.port = source_request.port
|
94
|
-
response_headers['Location'] = response_location.to_s
|
95
|
-
end
|
96
|
-
|
97
|
-
[target_response.status, response_headers, target_response.body]
|
98
|
-
end
|
99
|
-
|
100
|
-
def get_matcher(path, headers, rackreq)
|
101
|
-
matches = @matchers.select do |matcher|
|
102
|
-
matcher.match?(path, headers, rackreq)
|
103
|
-
end
|
104
|
-
|
105
|
-
if matches.length < 1
|
106
|
-
nil
|
107
|
-
elsif matches.length > 1 && @global_options[:matching] != :first
|
108
|
-
raise AmbiguousProxyMatch.new(path, matches)
|
109
|
-
else
|
110
|
-
matches.first
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def reverse_proxy_options(options)
|
115
|
-
@global_options=options
|
116
|
-
end
|
117
|
-
|
118
|
-
def reverse_proxy(matcher, url=nil, opts={})
|
119
|
-
raise GenericProxyURI.new(url) if matcher.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic
|
120
|
-
@matchers << ReverseProxyMatcher.new(matcher,url,opts)
|
121
|
-
end
|
122
|
-
|
123
|
-
def format_headers(headers)
|
124
|
-
headers.reduce({}) do |acc, (key, val)|
|
125
|
-
formated_key = key.split('-').map(&:capitalize).join('-')
|
126
|
-
acc[formated_key] = Array(val)
|
127
|
-
acc
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
5
|
+
ReverseProxy = RackReverseProxy::Middleware
|
131
6
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RackReverseProxy
|
2
|
+
module Errors
|
3
|
+
# GenericURI indicates that url is too generic
|
4
|
+
class GenericURI < Exception
|
5
|
+
attr_reader :url
|
6
|
+
|
7
|
+
def intialize(url)
|
8
|
+
@url = url
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
%(Your URL "#{@url}" is too generic. Did you mean "http://#{@url}"?)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# AmbiguousMatch indicates that path matched more than one endpoint
|
17
|
+
class AmbiguousMatch < Exception
|
18
|
+
attr_reader :path, :matches
|
19
|
+
|
20
|
+
def initialize(path, matches)
|
21
|
+
@path = path
|
22
|
+
@matches = matches
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
%(Path "#{path}" matched multiple endpoints: #{formatted_matches})
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def formatted_matches
|
32
|
+
matches.map(&:to_s).join(", ")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "net/https"
|
3
|
+
require "rack-proxy"
|
4
|
+
require "rack_reverse_proxy/roundtrip"
|
5
|
+
|
6
|
+
module RackReverseProxy
|
7
|
+
# Rack middleware for handling reverse proxying
|
8
|
+
class Middleware
|
9
|
+
include NewRelic::Agent::Instrumentation::ControllerInstrumentation if defined? NewRelic
|
10
|
+
|
11
|
+
def initialize(app = nil, &b)
|
12
|
+
@app = app || lambda { |_| [404, [], []] }
|
13
|
+
@rules = []
|
14
|
+
@global_options = {
|
15
|
+
:preserve_host => true,
|
16
|
+
:x_forwarded_host => true,
|
17
|
+
:matching => :all,
|
18
|
+
:replace_response_host => false
|
19
|
+
}
|
20
|
+
instance_eval(&b) if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
RoundTrip.new(@app, env, @global_options, @rules).call
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def reverse_proxy_options(options)
|
30
|
+
@global_options = options
|
31
|
+
end
|
32
|
+
|
33
|
+
def reverse_proxy(rule, url = nil, opts = {})
|
34
|
+
if rule.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic
|
35
|
+
raise Errors::GenericURI.new, url
|
36
|
+
end
|
37
|
+
@rules << Rule.new(rule, url, opts)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module RackReverseProxy
|
2
|
+
# ResponseBuilder knows target response building process
|
3
|
+
class ResponseBuilder
|
4
|
+
def initialize(target_request, uri, options)
|
5
|
+
@target_request = target_request
|
6
|
+
@uri = uri
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch
|
11
|
+
setup_response
|
12
|
+
target_response
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def setup_response
|
18
|
+
set_read_timeout
|
19
|
+
handle_https
|
20
|
+
handle_verify_mode
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_read_timeout
|
24
|
+
return unless read_timeout?
|
25
|
+
target_response.read_timeout = options[:timeout]
|
26
|
+
end
|
27
|
+
|
28
|
+
def read_timeout?
|
29
|
+
options[:timeout].to_i > 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_https
|
33
|
+
return unless https?
|
34
|
+
target_response.use_ssl = true
|
35
|
+
end
|
36
|
+
|
37
|
+
def https?
|
38
|
+
"https" == uri.scheme
|
39
|
+
end
|
40
|
+
|
41
|
+
def handle_verify_mode
|
42
|
+
return unless verify_mode?
|
43
|
+
target_response.verify_mode = options[:verify_mode]
|
44
|
+
end
|
45
|
+
|
46
|
+
def verify_mode?
|
47
|
+
options.key?(:verify_mode)
|
48
|
+
end
|
49
|
+
|
50
|
+
def target_response
|
51
|
+
@_target_response ||= Rack::HttpStreamingResponse.new(
|
52
|
+
target_request,
|
53
|
+
uri.host,
|
54
|
+
uri.port
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :target_request, :uri, :options
|
59
|
+
end
|
60
|
+
end
|