require 'puppet/sslcertificates'

# A module to handle reading of certificates.
module Puppet::SSLCertificates::Support
  class MissingCertificate < Puppet::Error; end
  class InvalidCertificate < Puppet::Error; end

  attr_reader :cacert

  # Some metaprogramming to create methods for retrieving and creating keys.
  # This probably isn't fewer lines than defining each separately...
  def self.keytype(name, options, &block)
    var = "@#{name}"

    maker = "mk_#{name}"
    reader = "read_#{name}"

    unless param = options[:param]
      raise ArgumentError, "You must specify the parameter for the key"
    end

    unless klass = options[:class]
      raise ArgumentError, "You must specify the class for the key"
    end

    # Define the method that creates it.
    define_method(maker, &block)

    # Define the reading method.
    define_method(reader) do
      return nil unless FileTest.exists?(Puppet[param]) or rename_files_with_uppercase(Puppet[param])

      begin
        instance_variable_set(var, klass.new(File.read(Puppet[param])))
      rescue => detail
        raise InvalidCertificate, "Could not read #{param}: #{detail}"
      end
    end

    # Define the overall method, which just calls the reader and maker
    # as appropriate.
    define_method(name) do
      unless cert = instance_variable_get(var)
        unless cert = send(reader)
          cert = send(maker)
          Puppet.settings.write(param) { |f| f.puts cert.to_pem }
        end
        instance_variable_set(var, cert)
      end
      cert
    end
  end

  # The key pair.
  keytype :key, :param => :hostprivkey, :class => OpenSSL::PKey::RSA do
    Puppet.info "Creating a new SSL key at #{Puppet[:hostprivkey]}"
    key = OpenSSL::PKey::RSA.new(Puppet[:keylength])

    # Our key meta programming can only handle one file, so we have
    # to separately write out the public key.
    Puppet.settings.write(:hostpubkey) do |f|
      f.print key.public_key.to_pem
    end
    return key
  end

  # Our certificate request
  keytype :csr, :param => :hostcsr, :class => OpenSSL::X509::Request do
    Puppet.info "Creating a new certificate request for #{Puppet[:certname]}"

    csr = OpenSSL::X509::Request.new
    csr.version = 0
    csr.subject = OpenSSL::X509::Name.new([["CN", Puppet[:certname]]])
    csr.public_key = key.public_key
    csr.sign(key, OpenSSL::Digest::MD5.new)

    return csr
  end

  keytype :cert, :param => :hostcert, :class => OpenSSL::X509::Certificate do
    raise MissingCertificate, "No host certificate"
  end

  keytype :ca_cert, :param => :localcacert, :class => OpenSSL::X509::Certificate do
    raise MissingCertificate, "No CA certificate"
  end

  # Request a certificate from the remote system.  This does all of the work
  # of creating the cert request, contacting the remote system, and
  # storing the cert locally.
  def requestcert
    begin
      cert, cacert = caclient.getcert(@csr.to_pem)
    rescue => detail
      puts detail.backtrace if Puppet[:trace]
      raise Puppet::Error.new("Certificate retrieval failed: #{detail}")
    end

    if cert.nil? or cert == ""
      return nil
    end
    Puppet.settings.write(:hostcert) do |f| f.print cert end
    Puppet.settings.write(:localcacert) do |f| f.print cacert end
    #File.open(@certfile, "w", 0644) { |f| f.print cert }
    #File.open(@cacertfile, "w", 0644) { |f| f.print cacert }
    begin
      @cert = OpenSSL::X509::Certificate.new(cert)
      @cacert = OpenSSL::X509::Certificate.new(cacert)
      retrieved = true
    rescue => detail
      raise Puppet::Error.new(
        "Invalid certificate: #{detail}"
      )
    end

    raise Puppet::DevError, "Received invalid certificate" unless @cert.check_private_key(@key)
    retrieved
  end

  # A hack method to deal with files that exist with a different case.
  # Just renames it; doesn't read it in or anything.
  def rename_files_with_uppercase(file)
    dir = File.dirname(file)
    short = File.basename(file)

    # If the dir isn't present, we clearly don't have the file.
    #return nil unless FileTest.directory?(dir)

    raise ArgumentError, "Tried to fix SSL files to a file containing uppercase" unless short.downcase == short

    return false unless File.directory?(dir)

    real_file = Dir.entries(dir).reject { |f| f =~ /^\./ }.find do |other|
      other.downcase == short
    end

    return nil unless real_file

    full_file = File.join(dir, real_file)

    Puppet.notice "Fixing case in #{full_file}; renaming to #{file}"
    File.rename(full_file, file)

    true
  end
end
