require 'uri'
require 'openssl'
require 'puppet/network/http'

module Puppet::Util::HttpProxy
  def self.proxy(uri)
    if http_proxy_host && !no_proxy?(uri)
      Net::HTTP.new(uri.host, uri.port, self.http_proxy_host, self.http_proxy_port, self.http_proxy_user, self.http_proxy_password)
    else
      http = Net::HTTP.new(uri.host, uri.port, nil, nil, nil, nil)
      # Net::HTTP defaults the proxy port even though we said not to
      # use one. Set it to nil so caller is not surprised
      http.proxy_port = nil if http.respond_to?(:proxy_port=)
      http
    end
  end

  def self.http_proxy_env
    # Returns a URI object if proxy is set, or nil
    proxy_env = ENV["http_proxy"] || ENV["HTTP_PROXY"]
    begin
      return URI.parse(proxy_env) if proxy_env
    rescue URI::InvalidURIError
      return nil
    end
    return nil
  end

  # The documentation around the format of the no_proxy variable seems
  # inconsistent.  Some suggests the use of the * as a way of matching any
  # hosts under a domain, e.g.:
  #   *.example.com
  # Other documentation suggests that just a leading '.' indicates a domain
  # level exclusion, e.g.:
  #   .example.com
  # We'll accommodate both here.
  def self.no_proxy?(dest)
    unless no_proxy = self.no_proxy
      return false
    end

    unless dest.is_a? URI
      begin
        dest = URI.parse(dest)
      rescue URI::InvalidURIError
        return false
      end
    end

    no_proxy.split(/\s*,\s*/).each do |d|
      host, port = d.split(':')
      host = Regexp.escape(host).gsub('\*', '.*')

      #If this no_proxy entry specifies a port, we want to match it against
      #the destination port.  Otherwise just match hosts.
      if port
        no_proxy_regex  = %r(#{host}:#{port}$)
        dest_string     = "#{dest.host}:#{dest.port}"
      else
        no_proxy_regex  = %r(#{host}$)
        dest_string     = "#{dest.host}"
      end

      if no_proxy_regex.match(dest_string)
        return true
      end
    end

    return false
  end

  def self.http_proxy_host
    env = self.http_proxy_env

    if env and env.host
      return env.host
    end

    if Puppet.settings[:http_proxy_host] == 'none'
      return nil
    end

    return Puppet.settings[:http_proxy_host]
  end

  def self.http_proxy_port
    env = self.http_proxy_env

    if env and env.port
      return env.port
    end

    return Puppet.settings[:http_proxy_port]
  end

  def self.http_proxy_user
    env = self.http_proxy_env

    if env and env.user
      return env.user
    end

    if Puppet.settings[:http_proxy_user] == 'none'
      return nil
    end

    return Puppet.settings[:http_proxy_user]
  end

  def self.http_proxy_password
    env = self.http_proxy_env

    if env and env.password
      return env.password
    end

    if Puppet.settings[:http_proxy_user] == 'none' or Puppet.settings[:http_proxy_password] == 'none'
      return nil
    end

    return Puppet.settings[:http_proxy_password]
  end

  def self.no_proxy
    no_proxy_env = ENV["no_proxy"] || ENV["NO_PROXY"]

    if no_proxy_env
      return no_proxy_env
    end

    if Puppet.settings[:no_proxy] == 'none'
      return nil
    end

    return Puppet.settings[:no_proxy]
  end

  # Return a Net::HTTP::Proxy object.
  #
  # This method optionally configures SSL correctly if the URI scheme is
  # 'https', including setting up the root certificate store so remote server
  # SSL certificates can be validated.
  #
  # @param [URI] uri The URI that is to be accessed.
  # @return [Net::HTTP::Proxy] object constructed tailored for the passed URI
  def self.get_http_object(uri)
    proxy = proxy(uri)

    if uri.scheme == 'https'
      cert_store = OpenSSL::X509::Store.new
      cert_store.set_default_paths

      proxy.use_ssl = true
      proxy.verify_mode = OpenSSL::SSL::VERIFY_PEER
      proxy.cert_store = cert_store
    end

    if Puppet[:http_debug]
      proxy.set_debug_output($stderr)
    end

    proxy.open_timeout = Puppet[:http_connect_timeout]
    proxy.read_timeout = Puppet[:http_read_timeout]

    proxy
  end

  # Retrieve a document through HTTP(s), following redirects if necessary. The
  # returned response body may be compressed, and it is the caller's
  # responsibility to decompress it based on the 'content-encoding' header.
  #
  # Based on the the client implementation in the HTTP pool.
  #
  # @see Puppet::Network::HTTP::Connection#request_with_redirects
  #
  # @param [URI] uri The address of the resource to retrieve.
  # @param [symbol] method The name of the Net::HTTP method to use, typically :get, :head, :post etc.
  # @param [FixNum] redirect_limit The number of redirections that can be followed.
  # @return [Net::HTTPResponse] a response object
  def self.request_with_redirects(uri, method, redirect_limit = 10, &block)
    current_uri = uri
    response = nil

    0.upto(redirect_limit) do |redirection|
      proxy = get_http_object(current_uri)

      headers = { 'Accept' => '*/*', 'User-Agent' => Puppet[:http_user_agent] }
      if Puppet.features.zlib?
        headers["Accept-Encoding"] = Puppet::Network::HTTP::Compression::ACCEPT_ENCODING
      end

      response = proxy.send(:head, current_uri.path, headers)
      Puppet.debug("HTTP HEAD request to #{current_uri} returned #{response.code} #{response.message}")

      if [301, 302, 307].include?(response.code.to_i)
        # handle the redirection
        current_uri = URI.parse(response['location'])
        next
      end

      if method != :head
        if block_given?
          response = proxy.send("request_#{method}".to_sym, current_uri.path, headers, &block)
        else
          response = proxy.send(method, current_uri.path, headers)
        end

        Puppet.debug("HTTP #{method.to_s.upcase} request to #{current_uri} returned #{response.code} #{response.message}")
      end

      return response
    end

    raise RedirectionLimitExceededException, _("Too many HTTP redirections for %{uri}") % { uri: uri }
  end
end
