def send_uri(uri, method: 'GET', headers: {}, body: '')
      uri = uri.to_s
      body = body.to_s
      headers = {} unless headers.is_a?(Hash)

      # validate url
      unless uri.start_with?('http://', 'https://')
        raise SSRFProxy::HTTP::Error::InvalidClientRequest,
              'Invalid request URI'
      end

      # set request method
      if @forward_method
        if @SUPPORTED_METHODS.include?(method)
          request_method = method
        else
          raise SSRFProxy::HTTP::Error::InvalidClientRequest,
                "Request method '#{method}' is not supported"
        end
      else
        request_method = @method
      end

      # parse request headers
      client_headers = {}
      headers.each do |k, v|
        if v.is_a?(Array)
          client_headers[k.downcase] = v.flatten.first
        elsif v.is_a?(String)
          client_headers[k.downcase] = v.to_s
        else
          raise SSRFProxy::HTTP::Error::InvalidClientRequest,
                "Request header #{k.inspect} value is malformed: #{v}"
        end
      end

      # reject websocket requests
      if client_headers['upgrade'].to_s.start_with?('WebSocket')
        logger.warn('WebSocket tunneling is not supported')
        raise SSRFProxy::HTTP::Error::InvalidClientRequest,
              'WebSocket tunneling is not supported'
      end

      # copy request body to URL
      if @body_to_uri && !body.eql?('')
        logger.debug("Parsing request body: #{body}")
        separator = uri.include?('?') ? '&' : '?'
        uri = "#{uri}#{separator}#{body}"
        logger.info("Added request body to URI: #{body.inspect}")
      end

      # copy basic authentication credentials to uri
      if @auth_to_uri && client_headers['authorization'].to_s.downcase.start_with?('basic ')
        logger.debug("Parsing basic authentication header: #{client_headers['authorization']}")
        begin
          creds = client_headers['authorization'].split(' ')[1]
          user = Base64.decode64(creds).chomp
          uri = uri.gsub!(%r{://}, "://#{CGI.escape(user).gsub(/\+/, '%20').gsub('%3A', ':')}@")
          logger.info("Using basic authentication credentials: #{user}")
        rescue
          logger.warn('Could not parse request authorization header: ' \
                      "#{client_headers['authorization']}")
        end
      end

      # copy cookies to uri
      cookies = []
      if @cookies_to_uri && !client_headers['cookie'].nil?
        logger.debug("Parsing request cookies: #{client_headers['cookie']}")
        client_headers['cookie'].split(/;\s*/).each do |c|
          cookies << c.to_s unless c.nil?
        end
        separator = uri.include?('?') ? '&' : '?'
        uri = "#{uri}#{separator}#{cookies.join('&')}"
        logger.info("Added cookies to URI: #{cookies.join('&')}")
      end

      # add cache buster
      if @cache_buster
        separator = uri.include?('?') ? '&' : '?'
        junk = "#{rand(36**6).to_s(36)}=#{rand(36**6).to_s(36)}"
        uri = "#{uri}#{separator}#{junk}"
      end

      # set request headers
      request_headers = @headers.dup

      # forward request cookies
      new_cookie = []
      new_cookie << @headers['cookie'] unless @headers['cookie'].to_s.eql?('')
      if @forward_cookies && !client_headers['cookie'].nil?
        client_headers['cookie'].split(/;\s*/).each do |c|
          new_cookie << c.to_s unless c.nil?
        end
      end
      unless new_cookie.empty?
        request_headers['cookie'] = new_cookie.uniq.join('; ')
        logger.info("Using cookie: #{new_cookie.join('; ')}")
      end

      # forward request headers and strip proxy headers
      if @forward_headers && !client_headers.empty?
        client_headers.each do |k, v|
          next if k.eql?('proxy-connection')
          next if k.eql?('proxy-authorization')
          if v.is_a?(Array)
            request_headers[k.downcase] = v.flatten.first
          elsif v.is_a?(String)
            request_headers[k.downcase] = v.to_s
          end
        end
      end

      # encode target host ip
      ip_encoded_uri = @ip_encoding ? encode_ip(uri, @ip_encoding) : uri

      # run request URI through rules
      target_uri = run_rules(ip_encoded_uri, @rules).to_s

      # URL encode target URI
      unless @no_urlencode
        target_uri = CGI.escape(target_uri).gsub(/\+/, '%20').to_s
      end

      # set path and query string
      if @url.query.to_s.eql?('')
        ssrf_url = @url.path.to_s
      else
        ssrf_url = "#{@url.path}?#{@url.query}"
      end

      # replace xxURLxx placeholder in request URL
      ssrf_url.gsub!(/#{@placeholder}/, target_uri)

      # replace xxURLxx placeholder in request body
      post_data = @post_data.gsub(/#{@placeholder}/, target_uri)

      # set request body
      if @forward_body && !body.eql?('')
        request_body = post_data.eql?('') ? body : "#{post_data}&#{body}"
      else
        request_body = post_data
      end

      # replace xxURLxx in request header values
      request_headers.each do |k, v|
        request_headers[k] = v.gsub(/#{@placeholder}/, target_uri)
      end

      # set content type
      if request_headers['content-type'].nil? && !request_body.eql?('')
        request_headers['content-type'] = 'application/x-www-form-urlencoded'
      end

      # set content length
      request_headers['content-length'] = request_body.length.to_s

      # send request
      response = nil
      start_time = Time.now
      begin
        response = send_http_request(ssrf_url,
                                     request_method,
                                     request_headers,
                                     request_body)
        if response['content-encoding'].to_s.downcase.eql?('gzip') && response.body
          begin
            sio = StringIO.new(response.body)
            gz = Zlib::GzipReader.new(sio)
            response.body = gz.read
          rescue
            logger.warn('Could not decompress response body')
          end
        end

        result = { 'url'          => uri,
                   'http_version' => response.http_version,
                   'code'         => response.code,
                   'message'      => response.message,
                   'headers'      => '',
                   'body'         => response.body.to_s || '' }
      rescue SSRFProxy::HTTP::Error::ConnectionTimeout => e
        unless @timeout_ok
          raise SSRFProxy::HTTP::Error::ConnectionTimeout, e.message
        end
        result = { 'url'          => uri,
                   'http_version' => '1.0',
                   'code'         => 200,
                   'message'      => 'Timeout',
                   'headers'      => '',
                   'body'         => '' }
        logger.info('Changed HTTP status code 504 to 200')
      end

      # set duration
      end_time = Time.now
      duration = ((end_time - start_time) * 1000).round(3)
      result['duration'] = duration

      # body content encoding
      result['body'].force_encoding('BINARY')
      unless result['body'].valid_encoding?
        begin
          result['body'] = result['body'].encode(
            'UTF-8',
            'binary',
            :invalid => :replace,
            :undef   => :replace,
            :replace => ''
          )
        rescue
        end
      end

      logger.info("Received #{result['body'].bytes.length} bytes in #{duration} ms")

      # match response content
      unless @match_regex.nil?
        matches = result['body'].scan(/#{@match_regex}/m)
        if !matches.empty?
          result['body'] = matches.flatten.first.to_s
          logger.info("Response body matches pattern '#{@match_regex}'")
        else
          result['body'] = ''
          logger.warn("Response body does not match pattern '#{@match_regex}'")
        end
      end

      # return 502 if matched response body is empty
      if @fail_no_content
        if result['body'].to_s.eql?('')
          result['code'] = 502
          result['message'] = 'Bad Gateway'
          result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"
          return result
        end
      end

      # unescape response body
      if @unescape
        # unescape slashes
        result['body'] = result['body'].tr('\\', '\\')
        result['body'] = result['body'].gsub('\\/', '/')
        # unescape whitespace
        result['body'] = result['body'].gsub('\r', "\r")
        result['body'] = result['body'].gsub('\n', "\n")
        result['body'] = result['body'].gsub('\t', "\t")
        # unescape quotes
        result['body'] = result['body'].gsub('\"', '"')
        result['body'] = result['body'].gsub("\\'", "'")
      end

      # decode HTML entities
      if @decode_html
        result['body'] = HTMLEntities.new.decode(result['body'])
      end

      # set title
      result['title'] = result['body'][0..8192] =~ %r{<title>([^<]*)</title>}im ? $1.to_s : ''

      # guess HTTP response code and message
      if @guess_status
        head = result['body'][0..8192]
        status = guess_status(head)
        unless status.empty?
          result['code'] = status['code']
          result['message'] = status['message']
          logger.info("Using HTTP response status: #{result['code']} #{result['message']}")
        end
      end

      # replace timeout response with 200 OK
      if @timeout_ok
        if result['code'].eql?('504')
          logger.info('Changed HTTP status code 504 to 200')
          result['code'] = 200
        end
      end

      # detect headers in response body
      if @detect_headers
        headers = ''
        head = result['body'][0..8192] # use first 8192 byes
        detected_headers = head.scan(%r{HTTP/(1\.\d) (\d+) (.*?)\r?\n(.*?)\r?\n\r?\n}m)

        if detected_headers.empty?
          logger.info('Found no HTTP response headers in response body.')
        else
          # HTTP redirects may contain more than one set of HTTP response headers
          # Use the last
          logger.info("Found #{detected_headers.count} sets of HTTP response headers in reponse. Using last.")
          version = detected_headers.last[0]
          code = detected_headers.last[1]
          message = detected_headers.last[2]
          detected_headers.last[3].split(/\r?\n/).each do |line|
            if line =~ /^[A-Za-z0-9\-_\.]+: /
              k = line.split(': ').first
              v = line.split(': ')[1..-1].flatten.first
              headers << "#{k}: #{v}\n"
            else
              logger.warn('Could not use response headers in response body : Headers are malformed.')
              headers = ''
              break
            end
          end
        end
        unless headers.eql?('')
          result['http_version'] = version
          result['code'] = code.to_i
          result['message'] = message
          result['headers'] = headers
          result['body'] = result['body'].split(/\r?\n\r?\n/)[detected_headers.count..-1].flatten.join("\n\n")
        end
      end

      # set status line
      result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"

      # strip unwanted HTTP response headers
      unless response.nil?
        response.each_header do |header_name, header_value|
          if header_name.downcase.eql?('content-encoding')
            next if header_value.downcase.eql?('gzip')
          end

          if @strip.include?(header_name.downcase)
            logger.info("Removed response header: #{header_name}")
            next
          end
          result['headers'] << "#{header_name}: #{header_value}\n"
        end
      end

      # add wildcard CORS header
      if @cors
        result['headers'] << "Access-Control-Allow-Origin: *\n"
      end

      # advise client to close HTTP connection
      if result['headers'] =~ /^connection:.*$/i
        result['headers'].gsub!(/^connection:.*$/i, 'Connection: close')
      else
        result['headers'] << "Connection: close\n"
      end

      # guess mime type and add content-type header
      content_type = nil
      if @sniff_mime
        head = result['body'][0..8192] # use first 8192 byes
        content_type = sniff_mime(head)
        if content_type.nil?
          content_type = guess_mime(File.extname(uri.to_s.split('?').first))
        end
      elsif @guess_mime
        content_type = guess_mime(File.extname(uri.to_s.split('?').first))
      end

      unless content_type.nil?
        logger.info("Using content-type: #{content_type}")
        if result['headers'] =~ /^content\-type:.*$/i
          result['headers'].gsub!(/^content\-type:.*$/i,
                                  "Content-Type: #{content_type}")
        else
          result['headers'] << "Content-Type: #{content_type}\n"
        end
      end

      # prompt for password if unauthorised
      if result['code'] == 401
        if result['headers'] !~ /^WWW-Authenticate:.*$/i
          auth_uri = URI.parse(uri.to_s.split('?').first)
          realm = "#{auth_uri.host}:#{auth_uri.port}"
          result['headers'] << "WWW-Authenticate: Basic realm=\"#{realm}\"\n"
          logger.info("Added WWW-Authenticate header for realm: #{realm}")
        end
      end

      # set location header if redirected
      if result['code'] == 301 || result['code'] == 302
        if result['headers'] !~ /^location:.*$/i
          location = nil
          if result['body'] =~ /This document may be found <a href="(.+)">/i
            location = $1
          elsif result['body'] =~ /The document has moved <a href="(.+)">/i
            location = $1
          end
          unless location.nil?
            result['headers'] << "Location: #{location}\n"
            logger.info("Added Location header: #{location}")
          end
        end
      end

      # set content length
      content_length = result['body'].length
      if result['headers'] =~ /^transfer\-encoding:.*$/i
        result['headers'].gsub!(/^transfer\-encoding:.*$/i,
                                "Content-Length: #{content_length}")
      elsif result['headers'] =~ /^content\-length:.*$/i
        result['headers'].gsub!(/^content\-length:.*$/i,
                                "Content-Length: #{content_length}")
      else
        result['headers'] << "Content-Length: #{content_length}\n"
      end

      # return HTTP response
      logger.debug("Response:\n" \
                   "#{result['status_line']}\n" \
                   "#{result['headers']}\n" \
                   "#{result['body']}")
      result
    end
def send_uri(uri, method: 'GET', headers: {}, body: '')
      uri = uri.to_s
      body = body.to_s
      headers = {} unless headers.is_a?(Hash)

      # validate url
      unless uri.start_with?('http://', 'https://')
        raise SSRFProxy::HTTP::Error::InvalidClientRequest,
              'Invalid request URI'
      end

      # set request method
      if @forward_method
        if @SUPPORTED_METHODS.include?(method)
          request_method = method
        else
          raise SSRFProxy::HTTP::Error::InvalidClientRequest,
                "Request method '#{method}' is not supported"
        end
      else
        request_method = @method
      end

      # parse request headers
      client_headers = {}
      headers.each do |k, v|
        if v.is_a?(Array)
          client_headers[k.downcase] = v.flatten.first
        elsif v.is_a?(String)
          client_headers[k.downcase] = v.to_s
        else
          raise SSRFProxy::HTTP::Error::InvalidClientRequest,
                "Request header #{k.inspect} value is malformed: #{v}"
        end
      end

      # reject websocket requests
      if client_headers['upgrade'].to_s.start_with?('WebSocket')
        logger.warn('WebSocket tunneling is not supported')
        raise SSRFProxy::HTTP::Error::InvalidClientRequest,
              'WebSocket tunneling is not supported'
      end

      # copy request body to URL
      if @body_to_uri && !body.eql?('')
        logger.debug("Parsing request body: #{body}")
        separator = uri.include?('?') ? '&' : '?'
        uri = "#{uri}#{separator}#{body}"
        logger.info("Added request body to URI: #{body.inspect}")
      end

      # copy basic authentication credentials to uri
      if @auth_to_uri && client_headers['authorization'].to_s.downcase.start_with?('basic ')
        logger.debug("Parsing basic authentication header: #{client_headers['authorization']}")
        begin
          creds = client_headers['authorization'].split(' ')[1]
          user = Base64.decode64(creds).chomp
          uri = uri.gsub!(%r{://}, "://#{CGI.escape(user).gsub(/\+/, '%20').gsub('%3A', ':')}@")
          logger.info("Using basic authentication credentials: #{user}")
        rescue
          logger.warn('Could not parse request authorization header: ' \
                      "#{client_headers['authorization']}")
        end
      end

      # copy cookies to uri
      cookies = []
      if @cookies_to_uri && !client_headers['cookie'].nil?
        logger.debug("Parsing request cookies: #{client_headers['cookie']}")
        client_headers['cookie'].split(/;\s*/).each do |c|
          cookies << c.to_s unless c.nil?
        end
        separator = uri.include?('?') ? '&' : '?'
        uri = "#{uri}#{separator}#{cookies.join('&')}"
        logger.info("Added cookies to URI: #{cookies.join('&')}")
      end

      # add cache buster
      if @cache_buster
        separator = uri.include?('?') ? '&' : '?'
        junk = "#{rand(36**6).to_s(36)}=#{rand(36**6).to_s(36)}"
        uri = "#{uri}#{separator}#{junk}"
      end

      # set request headers
      request_headers = @headers.dup

      # forward request cookies
      new_cookie = []
      new_cookie << @headers['cookie'] unless @headers['cookie'].to_s.eql?('')
      if @forward_cookies && !client_headers['cookie'].nil?
        client_headers['cookie'].split(/;\s*/).each do |c|
          new_cookie << c.to_s unless c.nil?
        end
      end
      unless new_cookie.empty?
        request_headers['cookie'] = new_cookie.uniq.join('; ')
        logger.info("Using cookie: #{new_cookie.join('; ')}")
      end

      # forward request headers and strip proxy headers
      if @forward_headers && !client_headers.empty?
        client_headers.each do |k, v|
          next if k.eql?('proxy-connection')
          next if k.eql?('proxy-authorization')
          if v.is_a?(Array)
            request_headers[k.downcase] = v.flatten.first
          elsif v.is_a?(String)
            request_headers[k.downcase] = v.to_s
          end
        end
      end

      # encode target host ip
      ip_encoded_uri = @ip_encoding ? encode_ip(uri, @ip_encoding) : uri

      # run request URI through rules
      target_uri = run_rules(ip_encoded_uri, @rules).to_s

      # URL encode target URI
      unless @no_urlencode
        target_uri = CGI.escape(target_uri).gsub(/\+/, '%20').to_s
      end

      # set path and query string
      if @url.query.to_s.eql?('')
        ssrf_url = @url.path.to_s
      else
        ssrf_url = "#{@url.path}?#{@url.query}"
      end

      # replace xxURLxx placeholder in request URL
      ssrf_url.gsub!(/#{@placeholder}/, target_uri)

      # replace xxURLxx placeholder in request body
      post_data = @post_data.gsub(/#{@placeholder}/, target_uri)

      # set request body
      if @forward_body && !body.eql?('')
        request_body = post_data.eql?('') ? body : "#{post_data}&#{body}"
      else
        request_body = post_data
      end

      # replace xxURLxx in request header values
      request_headers.each do |k, v|
        request_headers[k] = v.gsub(/#{@placeholder}/, target_uri)
      end

      # set content type
      if request_headers['content-type'].nil? && !request_body.eql?('')
        request_headers['content-type'] = 'application/x-www-form-urlencoded'
      end

      # set content length
      request_headers['content-length'] = request_body.length.to_s

      # send request
      response = nil
      start_time = Time.now
      begin
        response = send_http_request(ssrf_url,
                                     request_method,
                                     request_headers,
                                     request_body)
        if response['content-encoding'].to_s.downcase.eql?('gzip') && response.body
          begin
            sio = StringIO.new(response.body)
            gz = Zlib::GzipReader.new(sio)
            response.body = gz.read
          rescue
            logger.warn('Could not decompress response body')
          end
        end

        result = { 'url'          => uri,
                   'http_version' => response.http_version,
                   'code'         => response.code,
                   'message'      => response.message,
                   'headers'      => '',
                   'body'         => response.body.to_s || '' }
      rescue SSRFProxy::HTTP::Error::ConnectionTimeout => e
        unless @timeout_ok
          raise SSRFProxy::HTTP::Error::ConnectionTimeout, e.message
        end
        result = { 'url'          => uri,
                   'http_version' => '1.0',
                   'code'         => 200,
                   'message'      => 'Timeout',
                   'headers'      => '',
                   'body'         => '' }
        logger.info('Changed HTTP status code 504 to 200')
      end

      # set duration
      end_time = Time.now
      duration = ((end_time - start_time) * 1000).round(3)
      result['duration'] = duration

      # body content encoding
      result['body'].force_encoding('BINARY')
      unless result['body'].valid_encoding?
        begin
          result['body'] = result['body'].encode(
            'UTF-8',
            'binary',
            :invalid => :replace,
            :undef   => :replace,
            :replace => ''
          )
        rescue
        end
      end

      logger.info("Received #{result['body'].bytes.length} bytes in #{duration} ms")

      # match response content
      unless @match_regex.nil?
        matches = result['body'].scan(/#{@match_regex}/m)
        if !matches.empty?
          result['body'] = matches.flatten.first.to_s
          logger.info("Response body matches pattern '#{@match_regex}'")
        else
          result['body'] = ''
          logger.warn("Response body does not match pattern '#{@match_regex}'")
        end
      end

      # return 502 if matched response body is empty
      if @fail_no_content
        if result['body'].to_s.eql?('')
          result['code'] = 502
          result['message'] = 'Bad Gateway'
          result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"
          return result
        end
      end

      # unescape response body
      if @unescape
        # unescape slashes
        result['body'] = result['body'].tr('\\', '\\')
        result['body'] = result['body'].gsub('\\/', '/')
        # unescape whitespace
        result['body'] = result['body'].gsub('\r', "\r")
        result['body'] = result['body'].gsub('\n', "\n")
        result['body'] = result['body'].gsub('\t', "\t")
        # unescape quotes
        result['body'] = result['body'].gsub('\"', '"')
        result['body'] = result['body'].gsub("\\'", "'")
      end

      # decode HTML entities
      if @decode_html
        result['body'] = HTMLEntities.new.decode(result['body'])
      end

      # set title
      result['title'] = result['body'][0..8192] =~ %r{<title>([^<]*)</title>}im ? $1.to_s : ''

      # guess HTTP response code and message
      if @guess_status
        head = result['body'][0..8192]
        status = guess_status(head)
        unless status.empty?
          result['code'] = status['code']
          result['message'] = status['message']
          logger.info("Using HTTP response status: #{result['code']} #{result['message']}")
        end
      end

      # replace timeout response with 200 OK
      if @timeout_ok
        if result['code'].eql?('504')
          logger.info('Changed HTTP status code 504 to 200')
          result['code'] = 200
        end
      end

      # detect headers in response body
      if @detect_headers
        headers = ''
        head = result['body'][0..8192] # use first 8192 byes
        detected_headers = head.scan(%r{HTTP/(1\.\d) (\d+) (.*?)\r?\n(.*?)\r?\n\r?\n}m)

        if detected_headers.empty?
          logger.info('Found no HTTP response headers in response body.')
        else
          # HTTP redirects may contain more than one set of HTTP response headers
          # Use the last
          logger.info("Found #{detected_headers.count} sets of HTTP response headers in reponse. Using last.")
          version = detected_headers.last[0]
          code = detected_headers.last[1]
          message = detected_headers.last[2]
          detected_headers.last[3].split(/\r?\n/).each do |line|
            if line =~ /^[A-Za-z0-9\-_\.]+: /
              k = line.split(': ').first
              v = line.split(': ')[1..-1].flatten.first
              headers << "#{k}: #{v}\n"
            else
              logger.warn('Could not use response headers in response body : Headers are malformed.')
              headers = ''
              break
            end
          end
        end
        unless headers.eql?('')
          result['http_version'] = version
          result['code'] = code.to_i
          result['message'] = message
          result['headers'] = headers
          result['body'] = result['body'].split(/\r?\n\r?\n/)[detected_headers.count..-1].flatten.join("\n\n")
        end
      end

      # set status line
      result['status_line'] = "HTTP/#{result['http_version']} #{result['code']} #{result['message']}"

      # strip unwanted HTTP response headers
      unless response.nil?
        response.each_header do |header_name, header_value|
          if header_name.downcase.eql?('content-encoding')
            next if header_value.downcase.eql?('gzip')
          end

          if @strip.include?(header_name.downcase)
            logger.info("Removed response header: #{header_name}")
            next
          end
          result['headers'] << "#{header_name}: #{header_value}\n"
        end
      end

      # add wildcard CORS header
      if @cors
        result['headers'] << "Access-Control-Allow-Origin: *\n"
      end

      # advise client to close HTTP connection
      if result['headers'] =~ /^connection:.*$/i
        result['headers'].gsub!(/^connection:.*$/i, 'Connection: close')
      else
        result['headers'] << "Connection: close\n"
      end

      # guess mime type and add content-type header
      content_type = nil
      if @sniff_mime
        head = result['body'][0..8192] # use first 8192 byes
        content_type = sniff_mime(head)
        if content_type.nil?
          content_type = guess_mime(File.extname(uri.to_s.split('?').first))
        end
      elsif @guess_mime
        content_type = guess_mime(File.extname(uri.to_s.split('?').first))
      end

      unless content_type.nil?
        logger.info("Using content-type: #{content_type}")
        if result['headers'] =~ /^content\-type:.*$/i
          result['headers'].gsub!(/^content\-type:.*$/i,
                                  "Content-Type: #{content_type}")
        else
          result['headers'] << "Content-Type: #{content_type}\n"
        end
      end

      # prompt for password if unauthorised
      if result['code'] == 401
        if result['headers'] !~ /^WWW-Authenticate:.*$/i
          auth_uri = URI.parse(uri.to_s.split('?').first)
          realm = "#{auth_uri.host}:#{auth_uri.port}"
          result['headers'] << "WWW-Authenticate: Basic realm=\"#{realm}\"\n"
          logger.info("Added WWW-Authenticate header for realm: #{realm}")
        end
      end

      # set location header if redirected
      if result['code'] == 301 || result['code'] == 302
        if result['headers'] !~ /^location:.*$/i
          location = nil
          if result['body'] =~ /This document may be found <a href="(.+)">/i
            location = $1
          elsif result['body'] =~ /The document has moved <a href="(.+)">/i
            location = $1
          end
          unless location.nil?
            result['headers'] << "Location: #{location}\n"
            logger.info("Added Location header: #{location}")
          end
        end
      end

      # set content length
      content_length = result['body'].length
      if result['headers'] =~ /^transfer\-encoding:.*$/i
        result['headers'].gsub!(/^transfer\-encoding:.*$/i,
                                "Content-Length: #{content_length}")
      elsif result['headers'] =~ /^content\-length:.*$/i
        result['headers'].gsub!(/^content\-length:.*$/i,
                                "Content-Length: #{content_length}")
      else
        result['headers'] << "Content-Length: #{content_length}\n"
      end

      # return HTTP response
      logger.debug("Response:\n" \
                   "#{result['status_line']}\n" \
                   "#{result['headers']}\n" \
                   "#{result['body']}")
      result
    end
Fetch a URI via SSRF

 @param [String] uri URI to fetch
 @param [String] method HTTP request method
 @param [Hash] headers HTTP request headers
 @param [String] body HTTP request body

 @raise [SSRFProxy::HTTP::Error::InvalidClientRequest]
        An invalid client HTTP request was supplied.

 @return [Hash] HTTP response hash (version, code, message, headers, body, etc)