require 'net/http'
require 'openid'
require 'openid/util'

begin
  require 'net/https'
rescue LoadError
  OpenID::Util.log('WARNING: no SSL support found.  Will not be able ' +
                   'to fetch HTTPS URLs!')
  require 'net/http'
end

MAX_RESPONSE_KB = 1024

module Net
  class HTTP
    def post_connection_check(hostname)
      check_common_name = true
      cert = @socket.io.peer_cert
      cert.extensions.each { |ext|
        next if ext.oid != "subjectAltName"
        ext.value.split(/,\s+/).each{ |general_name|
          if /\ADNS:(.*)/ =~ general_name
            check_common_name = false
            reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
            return true if /\A#{reg}\z/i =~ hostname
          elsif /\AIP Address:(.*)/ =~ general_name
            check_common_name = false
            return true if $1 == hostname
          end
        }
      }
      if check_common_name
        cert.subject.to_a.each{ |oid, value|
          if oid == "CN"
            reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
            return true if /\A#{reg}\z/i =~ hostname
          end
        }
      end
      raise OpenSSL::SSL::SSLError, "hostname does not match"
    end
  end
end

module OpenID
  # Our HTTPResponse class extends Net::HTTPResponse with an additional
  # method, final_url.
  class HTTPResponse
    attr_accessor :final_url

    attr_accessor :_response

    def self._from_net_response(response, final_url, headers=nil)
      me = self.new
      me._response = response
      me.final_url = final_url
      return me
    end

    def method_missing(method, *args)
      @_response.send(method, *args)
    end

    def body=(s)
      @_response.instance_variable_set('@body', s)
      # XXX Hack to work around ruby's HTTP library behavior.  @body
      # is only returned if it has been read from the response
      # object's socket, but since we're not using a socket in this
      # case, we need to set the @read flag to true to avoid a bug in
      # Net::HTTPResponse.stream_check when @socket is nil.
      @_response.instance_variable_set('@read', true)
    end
  end

  class FetchingError < OpenIDError
  end

  class HTTPRedirectLimitReached < FetchingError
  end

  class SSLFetchingError < FetchingError
  end

  @fetcher = nil

  def self.fetch(url, body=nil, headers=nil,
                 redirect_limit=StandardFetcher::REDIRECT_LIMIT)
    return fetcher.fetch(url, body, headers, redirect_limit)
  end

  def self.fetcher
    if @fetcher.nil?
      @fetcher = StandardFetcher.new
    end

    return @fetcher
  end

  def self.fetcher=(fetcher)
    @fetcher = fetcher
  end

  # Set the default fetcher to use the HTTP proxy defined in the environment
  # variable 'http_proxy'.
  def self.fetcher_use_env_http_proxy
    proxy_string = ENV['http_proxy']
    return unless proxy_string

    proxy_uri = URI.parse(proxy_string)
    @fetcher = StandardFetcher.new(proxy_uri.host, proxy_uri.port,
                                   proxy_uri.user, proxy_uri.password)
  end
  
  class StandardFetcher

    USER_AGENT = "ruby-openid/#{OpenID::VERSION} (#{RUBY_PLATFORM})"

    REDIRECT_LIMIT = 5
    TIMEOUT = 60

    attr_accessor :ca_file
    attr_accessor :timeout

    # I can fetch through a HTTP proxy; arguments are as for Net::HTTP::Proxy.
    def initialize(proxy_addr=nil, proxy_port=nil,
                   proxy_user=nil, proxy_pass=nil)
      @ca_file = nil
      @proxy = Net::HTTP::Proxy(proxy_addr, proxy_port, proxy_user, proxy_pass)
      @timeout = TIMEOUT
    end

    def supports_ssl?(conn)
      return conn.respond_to?(:use_ssl=)
    end

    def make_http(uri)
      http = @proxy.new(uri.host, uri.port)
      http.read_timeout = @timeout
      http.open_timeout = @timeout
      return http
    end

    def set_verified(conn, verify)
      if verify
        conn.verify_mode = OpenSSL::SSL::VERIFY_PEER
      else
        conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
      end
    end

    def make_connection(uri)
      conn = make_http(uri)

      if !conn.is_a?(Net::HTTP)
        raise RuntimeError, sprintf("Expected Net::HTTP object from make_http; got %s",
                                    conn.class)
      end

      if uri.scheme == 'https'
        if supports_ssl?(conn)

          conn.use_ssl = true

          if @ca_file
            set_verified(conn, true)
            conn.ca_file = @ca_file
          else
            Util.log("WARNING: making https request to #{uri} without verifying " +
                     "server certificate; no CA path was specified.")
            set_verified(conn, false)
          end
        else
          raise RuntimeError, "SSL support not found; cannot fetch #{uri}"
        end
      end

      return conn
    end

    def fetch(url, body=nil, headers=nil, redirect_limit=REDIRECT_LIMIT)
      unparsed_url = url.dup
      url = URI::parse(url)
      if url.nil?
        raise FetchingError, "Invalid URL: #{unparsed_url}"
      end

      headers ||= {}
      headers['User-agent'] ||= USER_AGENT
      headers['Range'] ||= "0-#{MAX_RESPONSE_KB*1024}"

      begin
        conn = make_connection(url)
        response = nil

        response = conn.start {
          # Check the certificate against the URL's hostname
          if supports_ssl?(conn) and conn.use_ssl?
            conn.post_connection_check(url.host)
          end

          if body.nil?
            conn.request_get(url.request_uri, headers)
          else
            headers["Content-type"] ||= "application/x-www-form-urlencoded"
            conn.request_post(url.request_uri, body, headers)
          end
        }
      rescue RuntimeError => why
        raise why
      rescue OpenSSL::SSL::SSLError => why
        raise SSLFetchingError, "Error connecting to SSL URL #{url}: #{why}"
      rescue FetchingError => why
        raise why
      rescue Exception => why
        # Things we've caught here include a Timeout::Error, which descends
        # from SignalException.
        raise FetchingError, "Error fetching #{url}: #{why}"
      end

      case response
      when Net::HTTPRedirection
        if redirect_limit <= 0
          raise HTTPRedirectLimitReached.new(
            "Too many redirects, not fetching #{response['location']}")
        end
        begin
          return fetch(response['location'], body, headers, redirect_limit - 1)
        rescue HTTPRedirectLimitReached => e
          raise e
        rescue FetchingError => why
          raise FetchingError, "Error encountered in redirect from #{url}: #{why}"
        end
      else
        return HTTPResponse._from_net_response(response, unparsed_url)
      end
    end
  end
end
