rails-reverse-proxy/lib/reverse_proxy/client.rb

184 lines
4.9 KiB
Ruby

require 'rack'
require 'addressable/uri'
module ReverseProxy
class Client
@@callback_methods = [
:on_response,
:on_set_cookies,
:on_connect,
: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: {},
http: {},
path: nil,
username: nil,
password: nil,
verify_ssl: true,
reset_accept_encoding: false
)
source_request = Rack::Request.new(env)
# We can pass in a custom path
uri = Addressable::URI.parse("#{url}#{options[:path] || env['ORIGINAL_FULLPATH']}")
# Define headers
target_request_headers = extract_http_request_headers(source_request.env).merge(options[:headers])
# Initialize request
target_request = Net::HTTP.const_get(source_request.request_method.capitalize).new(uri.request_uri, 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
if options[:reset_accept_encoding]
# Clear the "Accept-Encoding" header (which will
# disable compression or other server-side encodings)
target_request['Accept-Encoding'] = nil
end
http_options = {}
http_options[:use_ssl] = (uri.scheme == "https")
http_options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE unless options[:verify_ssl]
http_options.merge!(options[:http]) if options[:http]
# Make the request
Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
callbacks[:on_connect].call(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 = parse_cookie(set_cookie_header)
name = set_cookie_hash[: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) || k == "HTTP_VERSION" || 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
COOKIE_PARAM_PATTERN = /\A([^(),\/<>@;:\\\"\[\]?={}\s]+)(?:=([^;]*))?\Z/
COOKIE_SPLIT_PATTERN = /;\s*/
def parse_cookie(cookie_str)
params = cookie_str.split(COOKIE_SPLIT_PATTERN)
info = params.shift.match(COOKIE_PARAM_PATTERN)
return {} unless info
cookie = {
name: info[1],
value: CGI.unescape(info[2]),
}
params.each do |param|
result = param.match(COOKIE_PARAM_PATTERN)
next unless result
key = result[1].downcase.to_sym
value = result[2]
case key
when :expires
begin
cookie[:expires] = Time.parse(value)
rescue ArgumentError
end
when *[:httponly, :secure]
cookie[key] = true
else
cookie[key] = value
end
end
cookie
end
end
end