require 'billy/handlers/handler' require 'addressable/uri' require 'eventmachine' require 'em-synchrony/em-http' module Billy class ProxyHandler include Handler def handles_request?(_method, url, _headers, _body) !disabled_request?(url) end def handle_request(method, url, headers, body) if handles_request?(method, url, headers, body) opts = { inactivity_timeout: Billy.config.proxied_request_inactivity_timeout, connect_timeout: Billy.config.proxied_request_connect_timeout } if url =~ /^https/ opts.merge!({tls: {verify_peer: Billy.config.verify_peer}}) end if Billy.config.proxied_request_host && !bypass_internal_proxy?(url) opts.merge!({ proxy: { host: Billy.config.proxied_request_host, port: Billy.config.proxied_request_port }} ) end req = EventMachine::HttpRequest.new(url, opts) req = req.send(method.downcase, build_request_options(url, headers, body)) if req.error return { error: "Request to #{url} failed with error: #{req.error}" } end if req.response response = process_response(req) unless allowed_response_code?(response[:status]) if Billy.config.non_successful_error_level == :error return { error: "#{method} Request failed due to response status #{response[:status]} for '#{url}' which was not allowed." } else Billy.log(:warn, "puffing-billy: Received response status code #{response[:status]} for '#{url}'") end end if cacheable?(url, response[:headers], response[:status]) cache_scope = Billy::Cache.instance.scope cache_key = Billy::Cache.instance.key(method.downcase, url, body) Billy::Cache.instance.store( cache_key, cache_scope, method.downcase, url, headers, body, response[:headers], response[:status], response[:content] ) end Billy.log(:info, "puffing-billy: PROXY #{method} succeeded for '#{url}'") return response end end nil end private def build_request_options(url, headers, body) headers = Hash[headers.map { |k, v| [k.downcase, v] }] headers['accept-encoding'] = '' uri = Addressable::URI.parse(url) headers.merge!({'authorization' => [uri.user, uri.password]}) if uri.userinfo req_opts = { redirects: 0, keepalive: false, head: headers, ssl: { verify: false } } req_opts[:body] = body if body req_opts end def process_response(req) response = { status: req.response_header.status, headers: req.response_header.raw, content: req.response.force_encoding('BINARY') } response[:headers].merge!('Connection' => 'close') response end def disabled_request?(url) return false unless Billy.config.non_whitelisted_requests_disabled uri = Addressable::URI.parse(url) # In isolated environments, you may want to stop the request from happening # or else you get "getaddrinfo: Name or service not known" errors blacklisted_path?(uri.path) || !whitelisted_url?(uri) end def allowed_response_code?(status) successful_status?(status) end def cacheable?(url, _headers, status) return false unless Billy.config.cache url = Addressable::URI.parse(url) # Cache the responses if they aren't whitelisted host[:port]s but always cache blacklisted paths on any hosts cacheable_status?(status) && (!whitelisted_url?(url) || blacklisted_path?(url.path) || cache_whitelisted_url?(url)) end def whitelisted_url?(url) Billy.config.whitelist.any? do |value| if value.is_a?(Regexp) url.to_s =~ value || url.omit(:port).to_s =~ value else value =~ /^#{url.host}(?::#{url.port})?$/ end end end def cache_whitelisted_url?(url) Billy.config.cache_whitelist.any? do |value| if value.is_a?(Regexp) url.to_s =~ value || url.omit(:port).to_s =~ value else value =~ /^#{url.host}(?::#{url.port})?$/ end end end def blacklisted_path?(path) !Billy.config.path_blacklist.index { |bl| bl.is_a?(Regexp) ? path =~ bl : path.include?(bl) }.nil? end def successful_status?(status) (200..299).include?(status) || status == 304 end def cacheable_status?(status) Billy.config.non_successful_cache_disabled ? successful_status?(status) : true end def bypass_internal_proxy?(url) url.include?('localhost') || url.include?('127.') || url.include?('.dev') || url.include?('.fin') end end end