#!/usr/bin/ruby
#
# NAME
#   symbiosis-ssl - Manage and generate SSL certificates
#
# SYNOPSIS
#   symbiosis-ssl [ --threshold days ] [ --no-generate ] [ --no-rollover ] [ --select set ]
#     [ --list ] [ --prefix prefix ] [ --verbose ] [ --debug ] [ --manual ] [ --help ]
#     [ domain domain ... ]
#
# OPTIONS
#  --force          Re-generate certificates, and roll over to the new set even
#                   if they're not due to be renewed. Implies --verbose.
#
#  --threshold days  Number of days before expiry that certificates should be renewed. Defaults to 21.
#
#  --select set     Select a specific set for a single domain. A domain must be specified.
#
#  --list           List available SSL certificate sets for a domain.
#
#  --no-generate    Do not try and generate keys or certificates.
#
#  --no-rollover    Do not try and generate keys or certificates.
#
#  --prefix prefix  Set the directory prefix for Symbiosis. Defaults to /srv.
#
#  --help           Show the help information for this script.
#
#  --manual         Show the manual for this script
#
#  --verbose        Show verbose information.
#
#  --debug          Show debugging information.
#
# USAGE
#
# This command is used to manage certificate sets automatically for domains on
# a Symbiosis system. It can request certificates from LetsEncrypt or generate
# self-signed ones (see PROVIDERS).
#
# In addition, if any domain's certificate set was altered, hooks are run (see
# HOOKS).
#
# PROVIDERS
#
# Currently two providers are supported, namely LetsEncrypt and SelfSigned. A
# domain can be set up to use either provider by setting a file
# /srv/example.com/config/ssl-provider with the name of the desired provider in
# it.
#
# If the provider is set to something else (e.g. CertificateProviderDuJour)
# then no certificates will be generated, but it is possible to manage updating
# certificates with this program.
#
# HOOKS
#
# Hooks are executed from the /etc/symbiosis/ssl-hooks.d directory, given the
# following conditions:
#
# * The file is executable
# * The file's name is made up only of alphanumerics, underscore (_) and hyphen
# (-)
#
# At present, only one event causes the hooks to be executed. If any domain's
# certificate set is altered by symbiosis-ssl, at the end of the process all
# the hooks are called with 'live-update' passed as their only command-line
# argument and the list of domains that were altered
#
# AUTHOR
#   Patrick J. Cherry <patrick@bytemark.co.uk>
#

#
#  Modules we require
#

require 'English'
require 'getoptlong'

opts = GetoptLong.new(
  ['--help', '-h', GetoptLong::NO_ARGUMENT],
  ['--manual', '-m', GetoptLong::NO_ARGUMENT],
  ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
  ['--debug', '-d', GetoptLong::NO_ARGUMENT],
  ['--force', '-f', GetoptLong::NO_ARGUMENT],
  ['--list', '-l', GetoptLong::NO_ARGUMENT],
  ['--threshold', '-t', GetoptLong::REQUIRED_ARGUMENT],
  ['--no-generate', '-G', GetoptLong::NO_ARGUMENT],
  ['--no-rollover', '-R', GetoptLong::NO_ARGUMENT],
  ['--select', '-s', GetoptLong::REQUIRED_ARGUMENT],
  ['--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT],
  ['--etc-dir', '-r', GetoptLong::REQUIRED_ARGUMENT]
)

manual = help = false
$VERBOSE = false
$DEBUG = false
prefix = '/srv'
do_list = do_generate = do_rollover = nil
rollover_to = nil
threshold = 21
etc_dir = '/etc'

opts.each do |opt,arg|
  case opt
  when '--no-generate'
    do_generate = false
  when '--no-rollover'
    do_rollover = false
  when '--select'
    rollover_to = arg.to_s
  when '--force'
    do_generate = do_rollover = true
    $VERBOSE = true
  when '--threshold'
    begin
      threshold = Integer(arg)
    rescue ArgumentError
      warn "** Could not parse #{arg.inspect} as an integer for --threshold"
    end
  when '--help'
    help = true
  when '--manual'
    manual = true
  when '--prefix'
    prefix = arg
  when '--etc-dir'
    etc_dir = arg
  when '--list'
    do_list = true
  when '--verbose'
    $VERBOSE = true
  when '--debug'
    $DEBUG = true
  end
end


#
# Output help as required.
#
if help || manual
  require 'symbiosis/utils'
  Symbiosis::Utils.show_help(__FILE__) if help
  Symbiosis::Utils.show_manual(__FILE__) if manual
  exit 0
end

#
# The requires spawn a massive stack of warnings in verbose mode.  So let's
# hide them.
#
v = $VERBOSE
$VERBOSE = false

require 'symbiosis'
require 'symbiosis/domains'
require 'symbiosis/domain/ssl'
require 'symbiosis/ssl'
require 'symbiosis/ssl/hooks'
require 'symbiosis/ssl/letsencrypt'
require 'symbiosis/ssl/selfsigned'

#
# And unhide.  Ugh.
#
$VERBOSE = v

Symbiosis.etc = etc_dir

domains = []

ARGV.each do |arg|
  domain = Symbiosis::Domains.find(arg.to_s, prefix)

  if domain.nil?
    warn "** Unable to find/parse domain #{arg.inspect}"
    next
  end

  domains << domain
end

if rollover_to && ARGV.length != 1
  warn '** Exactly one domain must be specfied when rolling over to a specific set.'
  exit 1
end

domains = Symbiosis::Domains.all(prefix) if ARGV.empty?

exit_code = 0

%w[INT TERM].each do |sig|
  trap(sig) do
    if Process.uid.zero?
      Process.euid = 0
      Process.egid = 0
    end

    exit 1
  end
end

now = Time.now

domains_altered = []

domains.sort_by(&:name).each do |domain|
  if do_list || rollover_to
    puts "Certificate sets for #{domain}:"

    if domain.ssl_available_sets.empty?
      puts "\t** No sets found\n\n"
      next
    end

    domain.ssl_available_sets.each do |this_set|
      if this_set.certificate.issuer == this_set.certificate.subject
        puts "\tSSL set #{this_set.name}: self-signed for #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}"
      else
        puts "\tSSL set #{this_set.name}: signed by #{this_set.certificate.issuer}, expires #{this_set.certificate.not_after}"
      end
    end

    current = domain.ssl_current_set
    puts "\tCurrent SSL set: #{current.name}\n" unless $VERBOSE

    next if rollover_to.nil?

    to_set = domain.ssl_available_sets.find { |s| s.name.to_s == rollover_to }

    if to_set.nil?
      puts "\tThere is no set '#{rollover_to}' available for this domain."
      next
    end

    if to_set == current
      puts "\tNo need to change to set #{to_set.name} as this is already current."
      next
    end

    puts "\tRolling over from set #{current.name} to #{to_set.name}"
    domain.ssl_rollover(to_set)
    puts "\tCurrent SSL set now: #{domain.ssl_current_set.name}\n"
    next
  end

  begin
    rollover_performed = domain.ssl_magic(threshold, do_generate, do_rollover, now)
    domains_altered.push domain.name if rollover_performed
  rescue StandardError => err
    puts "\t!! Failed: #{err.to_s.gsub($RS, '')}" if $VERBOSE
    puts err.backtrace.join("\n") if $DEBUG
    exit_code = 1
  end
end

success = Symbiosis::SSL::Hooks.run! 'live-update', domains_altered
exit_code = 2 unless success

exit exit_code
