roda-proxy/lib/roda/proxy.rb

181 lines
7.6 KiB
Ruby

# frozen_string_literal: true
require 'roda/proxy/version'
require 'net_http_unix'
# :nodoc:
class Roda
# :nodoc:
module RodaPlugins
# Roda plugin for simple API proxying
module Proxy
# Respond to the configure method to set the destination when proxying
# Expects the following options:
# [to] Required. The scheme and host of the proxy. Should not end with a slash.
# [path_prefix] Optional. The path to append to the above for proxying.
# The current request path will be prefixed on to this value.
# Should begin and end with a +/+. Defaults to +/+.
# For example, if the path prefix is +/foo/+ and the request received
# by Roda is +GET /postcode/lookup+, The proxied request will be dispatched
# to +GET /home/postcode/lookup+
# Example:
# plugin :proxy, to: 'https://foo.bar', path: '/my/api'
def self.configure(app, opts = {})
app.opts[:proxy_to] = opts.fetch(:to, nil)
app.opts[:proxy_path] = opts.fetch(:path_prefix, '/')
raise 'Proxy host not set, use "plugin :proxy, to: http://example.com"' unless app.opts[:proxy_to]
end
# :nodoc:
module RequestMethods
# Proxies the request, forwarding all headers except +Host+ which is
# rewritten to be the destination host. The response headers, body and
# status are returned to the client.
def verify_and_decrypt_session_cookie(cookie, secret_key_base)
cookie = CGI.unescape(cookie)
#################
# generate keys #
#################
encrypted_cookie_salt = 'encrypted cookie' # default: Rails.application.config.action_dispatch.encrypted_cookie_salt
encrypted_signed_cookie_salt = 'signed encrypted cookie' # default: Rails.application.config.action_dispatch.encrypted_signed_cookie_salt
iterations = 1000
key_size = 64
secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, encrypted_cookie_salt, iterations, key_size)[0, OpenSSL::Cipher.new('aes-256-cbc').key_len]
sign_secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, encrypted_signed_cookie_salt, iterations, key_size)
##########
# Verify #
##########
data, digest = cookie.split('--')
raise 'invalid message' unless digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_secret, data)
# you better use secure compare instead of `==` to prevent time based attact,
# ref: ActiveSupport::SecurityUtils.secure_compare
###########
# Decrypt #
###########
encrypted_message = Base64.strict_decode64(data)
encrypted_data, iv = encrypted_message.split('--').map{|v| Base64.strict_decode64(v) }
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.decrypt
cipher.key = secret
cipher.iv = iv
decrypted_data = cipher.update(encrypted_data)
decrypted_data << cipher.final
Marshal.load(decrypted_data)
end
def proxy
client = NetX::HTTPUnix.new(_sock_url)
request_class = Net::HTTP.const_get(:"#{env['REQUEST_METHOD'].to_s.downcase.capitalize}")
req = request_class.new(_proxy_url.to_s, _proxy_headers)
env_body = env["rack.input"].read
env_content_type = env['CONTENT_TYPE']
secret_key_base = YAML.load(File.read(File.expand_path("../../../../config/secrets.yml",__FILE__)))['production']['secret_key_base']
unless env_body.nil?
req.body = env_body
end
unless env_content_type.nil?
req.content_type = env_content_type
end
f_response = client.request(req)
f_response.content_length = nil #prevent duplicate header Content-Length and content-length
# if env['REQUEST_METHOD'].to_s.downcase == 'post'
# cookie = f_response['set-cookie'].to_s.split(/[;,]\s?/).map do |s|
# k,v = s.split('=', 2)
# [k, v.to_s]
# end.to_h
# puts ["cookie", cookie]
# if cookie['_orbit_store_session']
# secret_key_base = YAML.load(File.read(File.expand_path("../../../../config/secrets.yml",__FILE__)))['production']['secret_key_base']
# puts verify_and_decrypt_session_cookie(cookie['_orbit_store_session'], secret_key_base) rescue {}
# puts "---------------------------------"
# print f_response.get_fields('set-cookie') #['set-cookie']
# puts nil
# puts "================================="
# end
# end
_respond(f_response)
end
# Conditionally proxies when +condition+ is true and with selective probability.
# For instance, to proxy 50% of the time:
# r.proxy_when(probability: 0.5)
# Condition can be a truthy value or a block / lambda, in which case
# the result from the +#call+ is expected to be truthy.
# r.proxy_when( r.env['HTTP_PROXY_ME'] == 'true' )
# The two parameters can be combined, the probability is evaluated first.
# r.proxy_when( r.env['HTTP_PROXY_ME'] == 'true', probability: 0.8 )
# If and only if this method choses not to proxy is the block evaluated.
# The block is then expected to return a meaningful response to Roda.
def proxy_when(condition = true, probability: 1.0)
shall_proxy = Random.rand(0.0..1.0) <= probability
if shall_proxy && ( condition.respond_to?(:call) ? condition.call : condition )
proxy
else
yield(self)
end
end
private
def _sock_url
roda_class.opts[:proxy_to]
end
def _proxy_url
uri = URI("#{roda_class.opts[:proxy_path]}#{env['PATH_INFO'][1..-1]}")
if !env['QUERY_STRING'].empty?
uri.query = env['QUERY_STRING']
end
uri
end
def _proxy_headers
env_host = env['HTTP_HOST'].to_s
env_new = env
.select { |k, _v| k.start_with? 'HTTP_' }
.reject { |k, _v| k == 'HTTP_HOST' }
.transform_keys do |k|
k.sub(/^HTTP_/, '')
.split('_')
.map(&:capitalize)
.join('-')
end
if env_host
env_new.merge!({'Host' => env_host})
end
env_new
end
def _respond(proxied_response)
response.status = proxied_response.code.to_i
proxied_response
.to_hash
.reject { |k, v| k.downcase == 'transfer-encoding' }
.each { |k, v| response[k] = v.join("\n") }
# cookie = proxied_response['set-cookie'].to_s.split('; ').map{|s| k,v = s.split('='); [k, v.to_s]}.to_h
# if cookie['_orbit_store_session']
# secret_key_base = YAML.load(File.read(File.expand_path("../../../../config/secrets.yml",__FILE__)))['production']['secret_key_base']
# puts verify_and_decrypt_session_cookie(cookie['_orbit_store_session'], secret_key_base) rescue {}
# end
# puts proxied_response
# .each_header.to_h
# proxied_response['set-cookie'].split('; ').map do |s|
# k,v = s.split('=')
# response.set_cookie(k,v)
# end
response.write(proxied_response.body)
end
end
end
register_plugin :proxy, Proxy
end
end