diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..f73039e --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,80 @@ +GEM + remote: http://rubygems.org/ + specs: + activesupport (4.2.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.3.8) + builder (3.2.2) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + docile (1.1.5) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) + git (1.2.9.1) + github_api (0.12.3) + addressable (~> 2.3) + descendants_tracker (~> 0.0.4) + faraday (~> 0.8, < 0.10) + hashie (>= 3.3) + multi_json (>= 1.7.5, < 2.0) + nokogiri (~> 1.6.3) + oauth2 + hashie (3.4.2) + highline (1.7.2) + i18n (0.7.0) + jeweler (2.0.1) + builder + bundler (>= 1.0) + git (>= 1.2.5) + github_api + highline (>= 1.6.15) + nokogiri (>= 1.5.10) + rake + rdoc + json (1.8.3) + jwt (1.5.0) + mini_portile (0.6.2) + minitest (5.7.0) + multi_json (1.11.0) + multi_xml (0.5.5) + multipart-post (2.0.0) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) + oauth2 (1.0.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (~> 1.2) + rack (1.6.1) + rake (10.4.2) + rdoc (3.12.2) + json (~> 1.4) + shoulda (3.5.0) + shoulda-context (~> 1.0, >= 1.0.1) + shoulda-matchers (>= 1.4.1, < 3.0) + shoulda-context (1.2.1) + shoulda-matchers (2.8.0) + activesupport (>= 3.0.0) + simplecov (0.10.0) + docile (~> 1.1.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) + thread_safe (0.3.5) + tzinfo (1.2.2) + thread_safe (~> 0.1) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 1.0) + jeweler (~> 2.0.1) + rdoc (~> 3.12) + shoulda + simplecov diff --git a/lib/rails-reverse-proxy.rb b/lib/rails-reverse-proxy.rb deleted file mode 100644 index e69de29..0000000 diff --git a/lib/reverse-proxy.rb b/lib/reverse-proxy.rb new file mode 100644 index 0000000..2bcd356 --- /dev/null +++ b/lib/reverse-proxy.rb @@ -0,0 +1,2 @@ +module ReverseProxy +end \ No newline at end of file diff --git a/lib/reverse-proxy/client.rb b/lib/reverse-proxy/client.rb new file mode 100644 index 0000000..2bcd7df --- /dev/null +++ b/lib/reverse-proxy/client.rb @@ -0,0 +1,143 @@ +require 'rack' +require 'rack-proxy' + +module ReverseProxy + class Client + @@callback_methods = [ + :on_response, + :on_set_cookies, + :on_success, + :on_redirect, + :on_missing, + :on_error, + :on_complete + ] + + # Define callback setters + @@callback_methods.each do |method| + define_method(method) do |&block| + self.callbacks[method] = block + end + end + + attr_accessor :url, :callbacks + + def initialize(url) + self.url = url + self.callbacks = {} + + # Initialize default callbacks with empty Proc + @@callback_methods.each do |method| + self.callbacks[method] = Proc.new {} + end + + yield(self) if block_given? + end + + def request(env, options = {}, &block) + options.reverse_merge!( + headers: {}, + path: nil, + username: nil, + password: nil + ) + + source_request = Rack::Request.new(env) + + # We can pass in a custom path + uri = URI.parse("#{url}#{options[:path] || env['ORIGINAL_FULLPATH']}") + + # Initialize request + target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(uri.request_uri) + + # Setup headers + target_request_headers = extract_http_request_headers(source_request.env).merge(options[:headers]) + + target_request.initialize_http_header(target_request_headers) + + # Basic auth + target_request.basic_auth(options[:username], options[:password]) if options[:username] and options[:password] + + # Setup body + if target_request.request_body_permitted? \ + && source_request.body + source_request.body.rewind + target_request.body_stream = source_request.body + end + + target_request.content_length = source_request.content_length || 0 + target_request.content_type = source_request.content_type if source_request.content_type + + # Hold the response here + target_response = nil + + # Don't encode response/support compression which was + # causing content length not match the actual content + # length of the response which ended up causing issues + # within Varnish (503) + target_request['Accept-Encoding'] = nil + + # Make the request + Net::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == "https")) do |http| + target_response = http.request(target_request) + end + + status_code = target_response.code.to_i + payload = [status_code, target_response] + + callbacks[:on_response].call(payload) + + if set_cookie_headers = target_response.to_hash['set-cookie'] + set_cookies_hash = {} + + set_cookie_headers.each do |set_cookie_header| + set_cookie_hash = CookieJar::CookieValidation.parse_set_cookie(set_cookie_header) + set_cookie_hash[:value] = CGI.unescape(set_cookie_hash[:value]) + + name = set_cookie_hash.delete(:name) + + set_cookies_hash[name] = set_cookie_hash + end + + callbacks[:on_set_cookies].call(payload | [set_cookies_hash]) + end + + case status_code + when 200..299 + callbacks[:on_success].call(payload) + when 300..399 + if redirect_url = target_response['Location'] + callbacks[:on_redirect].call(payload | [redirect_url]) + end + when 400..499 + callbacks[:on_missing].call(payload) + when 500..599 + callbacks[:on_error].call(payload) + end + + callbacks[:on_complete].call(payload) + + payload + end + + private + + def extract_http_request_headers(env) + headers = env.reject do |k, v| + !(/^HTTP_[A-Z_]+$/ === k) || v.nil? + end.map do |k, v| + [reconstruct_header_name(k), v] + end.inject(Rack::Utils::HeaderHash.new) do |hash, k_v| + k, v = k_v + hash[k] = v + hash + end + + headers + end + + def reconstruct_header_name(name) + name.sub(/^HTTP_/, "").gsub("_", "-") + end + end +end \ No newline at end of file diff --git a/lib/reverse-proxy/controller.rb b/lib/reverse-proxy/controller.rb new file mode 100644 index 0000000..3b1d742 --- /dev/null +++ b/lib/reverse-proxy/controller.rb @@ -0,0 +1,60 @@ +module ReverseProxy + module Controller + def reverse_proxy(proxy_url, options = {}) + proxy_uri = Addressable::URI.parse(proxy_url) + + client = ReverseProxy::Client.new(proxy_url) do |config| + config.on_response do |code, response| + blacklist = [ + 'Connection', # Always close connection + 'Transfer-Encoding', # Let Rails calculate this + 'Content-Length' # Let Rails calculate this + ] + + response.each_capitalized do |key, value| + next if blacklist.include?(key) + + headers[key] = value + end + end + + config.on_set_cookies do |code, response, set_cookies| + set_cookies.each do |key, attributes| + cookies[key] = attributes + end + end + + config.on_redirect do |code, response, redirect_url| + request_uri = Addressable::URI.parse(request.url) + redirect_uri = Addressable::URI.parse(redirect_url) + + # Make redirect uri absolute if it's relative by + # joining it with the request url + redirect_uri = request_uri.join(redirect_url) if redirect_uri.host.nil? + + unless redirect_uri.port.nil? + # Make sure it's consistent with our request port + redirect_uri.port = request.port if redirect_uri.port == proxy_uri.port + end + + redirect_to redirect_uri.to_s, status: code and return + end + + config.on_complete do |code, response| + content_type = response['Content-Type'] + body = response.body.to_s + + if content_type and content_type.match /image/ + send_data body, content_type: content_type, disposition: "inline", status: code + else + render text: body, content_type: content_type, status: code + end + end + + yield(config) if block_given? + end + + client.request(env, options) + end + end +end \ No newline at end of file