require 'openssl'
require 'puppet'
require 'puppet/sslcertificates'
require 'xmlrpc/server'

# Much of this was taken from QuickCert:
#   http://segment7.net/projects/ruby/QuickCert/

class Puppet::Network::Handler
  class CA < Handler
    attr_reader :ca

    desc "Provides an interface for signing CSRs.  Accepts a CSR and returns
    the CA certificate and the signed certificate, or returns nil if
    the cert is not signed."

    @interface = XMLRPC::Service::Interface.new("puppetca") { |iface|
      iface.add_method("array getcert(csr)")
    }

    def autosign
      if defined?(@autosign)
        @autosign
      else
        Puppet[:autosign]
      end
    end

    # FIXME autosign? should probably accept both hostnames and IP addresses
    def autosign?(hostname)
      # simple values are easy
      if autosign == true or autosign == false
        return autosign
      end

      # we only otherwise know how to handle files
      unless autosign =~ /^\//
        raise Puppet::Error, "Invalid autosign value #{autosign.inspect}"
      end

      unless FileTest.exists?(autosign)
        unless defined?(@@warnedonautosign)
          @@warnedonautosign = true
          Puppet.info "Autosign is enabled but #{autosign} is missing"
        end
        return false
      end
      auth = Puppet::Network::AuthStore.new
      File.open(autosign) { |f|
        f.each { |line|
          next if line =~ /^\s*#/
          next if line =~ /^\s*$/
          auth.allow(line.chomp)
        }
      }

      # for now, just cheat and pass a fake IP address to allowed?
      auth.allowed?(hostname, "127.1.1.1")
    end

    def initialize(hash = {})
      Puppet.settings.use(:main, :ssl, :ca)
      @autosign = hash[:autosign] if hash.include? :autosign

      @ca = Puppet::SSLCertificates::CA.new(hash)
    end

    # our client sends us a csr, and we either store it for later signing,
    # or we sign it right away
    def getcert(csrtext, client = nil, clientip = nil)
      csr = OpenSSL::X509::Request.new(csrtext)

      # Use the hostname from the CSR, not from the network.
      subject = csr.subject

      nameary = subject.to_a.find { |ary|
        ary[0] == "CN"
      }

      if nameary.nil?
        Puppet.err(
          "Invalid certificate request: could not retrieve server name"
        )
        return "invalid"
      end

      hostname = nameary[1]

      unless @ca
        Puppet.notice "Host #{hostname} asked for signing from non-CA master"
        return ""
      end

      # We used to save the public key, but it's basically unnecessary
      # and it mucks with the permissions requirements.
      # save_pk(hostname, csr.public_key)

      certfile = File.join(Puppet[:certdir], [hostname, "pem"].join("."))

      # first check to see if we already have a signed cert for the host
      cert, cacert = ca.getclientcert(hostname)
      if cert and cacert
        Puppet.info "Retrieving existing certificate for #{hostname}"
        unless csr.public_key.to_s == cert.public_key.to_s
          raise Puppet::Error, "Certificate request does not match existing certificate; run 'puppetca --clean #{hostname}'."
        end
        return [cert.to_pem, cacert.to_pem]
      elsif @ca
        if self.autosign?(hostname) or client.nil?
          Puppet.info "Signing certificate for CA server" if client.nil?
          # okay, we don't have a signed cert
          # if we're a CA and autosign is turned on, then go ahead and sign
          # the csr and return the results
          Puppet.info "Signing certificate for #{hostname}"
          cert, cacert = @ca.sign(csr)
          #Puppet.info "Cert: #{cert.class}; Cacert: #{cacert.class}"
          return [cert.to_pem, cacert.to_pem]
        else # just write out the csr for later signing
          if @ca.getclientcsr(hostname)
            Puppet.info "Not replacing existing request from #{hostname}"
          else
            Puppet.notice "Host #{hostname} has a waiting certificate request"
            @ca.storeclientcsr(csr)
          end
          return ["", ""]
        end
      else
        raise "huh?"
      end
    end

    private

    # Save the public key.
    def save_pk(hostname, public_key)
      pkeyfile = File.join(Puppet[:publickeydir], [hostname, "pem"].join('.'))

      if FileTest.exists?(pkeyfile)
        currentkey = File.open(pkeyfile) { |k| k.read }
        unless currentkey == public_key.to_s
          raise Puppet::Error, "public keys for #{hostname} differ"
        end
      else
        File.open(pkeyfile, "w", 0644) { |f|
          f.print public_key.to_s
        }
      end
    end
  end
end

