Skip to content

Commit ae4bbb2

Browse files
committed
[refactor] Add code coverage tool; Refactor gem structure to proper one; Leave only one constant in foreign Rack module
1 parent 3497b74 commit ae4bbb2

File tree

13 files changed

+208
-196
lines changed

13 files changed

+208
-196
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ group :test do
88
gem "webmock"
99
end
1010

11+
group :development, :test do
12+
gem "simplecov"
13+
end

Guardfile

Lines changed: 0 additions & 8 deletions
This file was deleted.

VERSION

Lines changed: 0 additions & 1 deletion
This file was deleted.

lib/rack/exception.rb

Lines changed: 0 additions & 31 deletions
This file was deleted.

lib/rack/reverse_proxy.rb

Lines changed: 2 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,5 @@
1-
require 'net/http'
2-
require 'net/https'
3-
require "rack-proxy"
4-
require "rack/reverse_proxy_matcher"
5-
require "rack/exception"
1+
require "rack_reverse_proxy"
62

73
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, Proxy.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 = Proxy.extract_http_request_headers(source_request.env)
47-
48-
if options[:preserve_host]
49-
if uri.port == uri.default_port
50-
target_request_headers['HOST'] = uri.host
51-
else
52-
target_request_headers['HOST'] = "#{uri.host}:#{uri.port}"
53-
end
54-
end
55-
56-
if options[:x_forwarded_host]
57-
target_request_headers['X-Forwarded-Host'] = source_request.host
58-
target_request_headers['X-Forwarded-Port'] = "#{source_request.port}"
59-
end
60-
61-
target_request.initialize_http_header(target_request_headers)
62-
63-
# Basic auth
64-
target_request.basic_auth options[:username], options[:password] if options[:username] and options[:password]
65-
66-
# Setup body
67-
if target_request.request_body_permitted? && source_request.body
68-
source_request.body.rewind
69-
target_request.body_stream = source_request.body
70-
end
71-
72-
target_request.content_length = source_request.content_length || 0
73-
target_request.content_type = source_request.content_type if source_request.content_type
74-
75-
# Create a streaming response (the actual network communication is deferred, a.k.a. streamed)
76-
target_response = HttpStreamingResponse.new(target_request, uri.host, uri.port)
77-
78-
# pass the timeout configuration through
79-
target_response.read_timeout = options[:timeout] if options[:timeout].to_i > 0
80-
81-
target_response.use_ssl = "https" == uri.scheme
82-
83-
# Let rack set the transfer-encoding header
84-
response_headers = Rack::Utils::HeaderHash.new Proxy.normalize_headers(format_headers(target_response.headers))
85-
response_headers.delete('Transfer-Encoding')
86-
response_headers.delete('Status')
87-
88-
# Replace the location header with the proxy domain
89-
if response_headers['Location'] && options[:replace_response_host]
90-
response_location = URI(response_headers['location'])
91-
response_location.host = source_request.host
92-
response_location.port = source_request.port
93-
response_headers['Location'] = response_location.to_s
94-
end
95-
96-
[target_response.status, response_headers, target_response.body]
97-
end
98-
99-
def get_matcher(path, headers, rackreq)
100-
matches = @matchers.select do |matcher|
101-
matcher.match?(path, headers, rackreq)
102-
end
103-
104-
if matches.length < 1
105-
nil
106-
elsif matches.length > 1 && @global_options[:matching] != :first
107-
raise AmbiguousProxyMatch.new(path, matches)
108-
else
109-
matches.first
110-
end
111-
end
112-
113-
def reverse_proxy_options(options)
114-
@global_options=options
115-
end
116-
117-
def reverse_proxy(matcher, url=nil, opts={})
118-
raise GenericProxyURI.new(url) if matcher.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic
119-
@matchers << ReverseProxyMatcher.new(matcher,url,opts)
120-
end
121-
122-
def format_headers(headers)
123-
headers.reduce({}) do |acc, (key, val)|
124-
formated_key = key.split('-').map(&:capitalize).join('-')
125-
acc[formated_key] = Array(val)
126-
acc
127-
end
128-
end
129-
end
4+
ReverseProxy = RackReverseProxy::Middleware
1305
end

lib/rack_reverse_proxy.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
require "rack_reverse_proxy/errors"
2+
require "rack_reverse_proxy/matcher"
3+
require "rack_reverse_proxy/middleware"

lib/rack_reverse_proxy/errors.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module RackReverseProxy
2+
module Errors
3+
class GenericURI < Exception
4+
attr_reader :url
5+
6+
def intialize(url)
7+
@url = url
8+
end
9+
10+
def to_s
11+
%Q(Your URL "#{@url}" is too generic. Did you mean "http://#{@url}"?)
12+
end
13+
end
14+
15+
class AmbiguousMatch < Exception
16+
attr_reader :path, :matches
17+
def initialize(path, matches)
18+
@path = path
19+
@matches = matches
20+
end
21+
22+
def to_s
23+
%Q(Path "#{path}" matched multiple endpoints: #{formatted_matches})
24+
end
25+
26+
private
27+
28+
def formatted_matches
29+
matches.map {|matcher| matcher.to_s}.join(', ')
30+
end
31+
end
32+
end
33+
end

lib/rack/reverse_proxy_matcher.rb renamed to lib/rack_reverse_proxy/matcher.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
module Rack
2-
class ReverseProxyMatcher
1+
module RackReverseProxy
2+
class Matcher
33
def initialize(matcher, url=nil, options={})
44
@default_url=url
55
@url=url
@@ -50,4 +50,4 @@ def match_path(path, *args)
5050
match
5151
end
5252
end
53-
end
53+
end
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
require 'net/http'
2+
require 'net/https'
3+
require "rack-proxy"
4+
5+
module RackReverseProxy
6+
class Middleware
7+
include NewRelic::Agent::Instrumentation::ControllerInstrumentation if defined? NewRelic
8+
9+
def initialize(app = nil, &b)
10+
@app = app || lambda {|env| [404, [], []] }
11+
@matchers = []
12+
@global_options = {:preserve_host => true, :x_forwarded_host => true, :matching => :all, :replace_response_host => false}
13+
instance_eval(&b) if block_given?
14+
end
15+
16+
def call(env)
17+
rackreq = Rack::Request.new(env)
18+
matcher = get_matcher(rackreq.fullpath, Rack::Proxy.extract_http_request_headers(rackreq.env), rackreq)
19+
return @app.call(env) if matcher.nil?
20+
21+
if @global_options[:newrelic_instrumentation]
22+
action_name = "#{rackreq.path.gsub(/\/\d+/,'/:id').gsub(/^\//,'')}/#{rackreq.request_method}" # Rack::ReverseProxy/foo/bar#GET
23+
perform_action_with_newrelic_trace(:name => action_name, :request => rackreq) do
24+
proxy(env, rackreq, matcher)
25+
end
26+
else
27+
proxy(env, rackreq, matcher)
28+
end
29+
end
30+
31+
private
32+
33+
def proxy(env, source_request, matcher)
34+
uri = matcher.get_uri(source_request.fullpath,env)
35+
if uri.nil?
36+
return @app.call(env)
37+
end
38+
options = @global_options.dup.merge(matcher.options)
39+
40+
# Initialize request
41+
target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(uri.request_uri)
42+
43+
# Setup headers
44+
target_request_headers = Rack::Proxy.extract_http_request_headers(source_request.env)
45+
46+
if options[:preserve_host]
47+
if uri.port == uri.default_port
48+
target_request_headers['HOST'] = uri.host
49+
else
50+
target_request_headers['HOST'] = "#{uri.host}:#{uri.port}"
51+
end
52+
end
53+
54+
if options[:x_forwarded_host]
55+
target_request_headers['X-Forwarded-Host'] = source_request.host
56+
target_request_headers['X-Forwarded-Port'] = "#{source_request.port}"
57+
end
58+
59+
target_request.initialize_http_header(target_request_headers)
60+
61+
# Basic auth
62+
target_request.basic_auth options[:username], options[:password] if options[:username] and options[:password]
63+
64+
# Setup body
65+
if target_request.request_body_permitted? && source_request.body
66+
source_request.body.rewind
67+
target_request.body_stream = source_request.body
68+
end
69+
70+
target_request.content_length = source_request.content_length || 0
71+
target_request.content_type = source_request.content_type if source_request.content_type
72+
73+
# Create a streaming response (the actual network communication is deferred, a.k.a. streamed)
74+
target_response = Rack::HttpStreamingResponse.new(target_request, uri.host, uri.port)
75+
76+
# pass the timeout configuration through
77+
target_response.read_timeout = options[:timeout] if options[:timeout].to_i > 0
78+
79+
target_response.use_ssl = "https" == uri.scheme
80+
81+
# Let rack set the transfer-encoding header
82+
response_headers = Rack::Utils::HeaderHash.new Rack::Proxy.normalize_headers(format_headers(target_response.headers))
83+
response_headers.delete('Transfer-Encoding')
84+
response_headers.delete('Status')
85+
86+
# Replace the location header with the proxy domain
87+
if response_headers['Location'] && options[:replace_response_host]
88+
response_location = URI(response_headers['location'])
89+
response_location.host = source_request.host
90+
response_location.port = source_request.port
91+
response_headers['Location'] = response_location.to_s
92+
end
93+
94+
[target_response.status, response_headers, target_response.body]
95+
end
96+
97+
def get_matcher(path, headers, rackreq)
98+
matches = @matchers.select do |matcher|
99+
matcher.match?(path, headers, rackreq)
100+
end
101+
102+
if matches.length < 1
103+
nil
104+
elsif matches.length > 1 && @global_options[:matching] != :first
105+
raise Errors::AmbiguousMatch.new(path, matches)
106+
else
107+
matches.first
108+
end
109+
end
110+
111+
def reverse_proxy_options(options)
112+
@global_options=options
113+
end
114+
115+
def reverse_proxy(matcher, url=nil, opts={})
116+
raise Errors::GenericURI.new(url) if matcher.is_a?(String) && url.is_a?(String) && URI(url).class == URI::Generic
117+
@matchers << Matcher.new(matcher,url,opts)
118+
end
119+
120+
def format_headers(headers)
121+
headers.reduce({}) do |acc, (key, val)|
122+
formated_key = key.split('-').map(&:capitalize).join('-')
123+
acc[formated_key] = Array(val)
124+
acc
125+
end
126+
end
127+
end
128+
end

lib/rack_reverse_proxy/version.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module RackReverseProxy
2+
VERSION = "0.9.1"
3+
end

0 commit comments

Comments
 (0)