# 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