#! /usr/bin/perl

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2016 - 2017 Raphaël Gertz <acme@rapsys.eu>

# Best practice
use strict;
use warnings;

# Fix use of acl
use filetest 'access';

# Load dependancies
use Carp qw(carp confess);
use DateTime;
use File::Path qw(make_path);
use File::stat qw(stat);
use File::Spec;
use File::Slurp qw(read_file write_file);
use JSON qw(decode_json);
use IPC::System::Simple qw(capturex $EXITVAL);
use Acme qw(CERT_DIR CONFIG DS KEY_DIR SERVER_CRT SERVER_KEY ACCOUNT_KEY);

# Load POSIX
use POSIX qw(strftime EXIT_SUCCESS EXIT_FAILURE);

# Init debug
my $debug = 0;

# Init config
my $config = undef;

# Strip and enable debug
@ARGV = map { if ($_ eq '-d') { $debug = 1; (); } else { $_; } } @ARGV;

# Check config file
if (! -f CONFIG) {
	print 'Config file '.CONFIG.' do not exists'."\n";
	exit EXIT_FAILURE;
}

# Load config
unless (
	#XXX: use eval to workaround a fatal in decode_json
	eval {
		# Read it
		($config = read_file(CONFIG)) &&
		# Decode it
		($config = decode_json($config)) &&
		# Check hash validity
		defined($config->{certificates}) &&
		# Check not empty
		scalar($config->{certificates}) &&
		# Check hash validity
		defined($config->{thumbprint}) &&
		# Check certificates array
		! scalar map {unless(defined($_->{cert}) && defined($_->{key}) && defined($_->{mail}) && defined($_->{domain}) && defined($_->{domains})) {1;} else {();}} @{$config->{certificates}}
	}
) {
	print 'Config file '.CONFIG.' is not readable or invalid'."\n";
	exit EXIT_FAILURE;
}

# Deal with certificates
foreach (@{$config->{certificates}}) {
	# Init variables
	my ($mtime, $dt, $suffix) = undef;

	# Skip certificate without 60 days
	if (-f $_->{cert} && ($mtime = stat($_->{cert})->mtime) >= (time() - 60*24*3600)) {
		next;
	}

	# Extract cert directory and filename
	my (undef, $certDir) = File::Spec->splitpath($_->{cert});

	# Check that certificate is writable
	unless (-w $certDir || -w $_->{cert}) {
		carp('directory '.$certDir.' or file '.$_->{cert}.' must be writable: '.$!);
		next;
	}

	# Check that key directory exists
	if (! -d KEY_DIR) {
		# Create all paths
		make_path(KEY_DIR, {error => \my $err});
		if (@$err) {
			map {
				my ($file, $msg) = %$_;
				carp ($file eq '' ? '' : $file.': ').$msg if ($debug);
			} @$err;
			confess 'make_path failed';
		}
	}

	# Unlink if is a symlink
	if (-l KEY_DIR.DS.SERVER_KEY) {
		unless(unlink(KEY_DIR.DS.SERVER_KEY)) {
			carp('unlink '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!);
			next;
		}
	}

	# Symlink to key
	unless(symlink($_->{key}, KEY_DIR.DS.SERVER_KEY)) {
		carp('symlink '.$_->{key}.' to '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!);
		next;
	}

	# Unlink if is a symlink
	if (-l KEY_DIR.DS.ACCOUNT_KEY) {
		unless(unlink(KEY_DIR.DS.ACCOUNT_KEY)) {
			carp('unlink '.KEY_DIR.DS.ACCOUNT_KEY.' failed: '.$!);
			next;
		}
	}

	# Symlink to key
	unless(symlink($_->{account}, KEY_DIR.DS.ACCOUNT_KEY)) {
		carp('symlink '.$_->{account}.' to '.KEY_DIR.DS.ACCOUNT_KEY.' failed: '.$!);
		next;
	}

	# Init args
	my @args = @{$_->{domains}};

	# Prepend mail and domain to other args
	unshift(@args, $_->{mail}, $_->{domain});

	# Preprend prod
	if (defined $_->{prod} && $_->{prod}) {
		unshift(@args, '-p');
	}

	# Preprend debug
	if ($debug) {
		unshift(@args, '-d');
	}

	# Run acmecert with args
	my @out = capturex([0..1], 'acmecert', @args);

	# Deal with error
	if ($EXITVAL != 0) {
		print join("\n", @out) if ($debug);
		carp('acmecert '.join(', ', @args).' failed: '.$!);
		next;
	}

	# Read cert
	my $content;
	unless($content = read_file(CERT_DIR.DS.SERVER_CRT)) {
		carp('read_file '.CERT_DIR.DS.SERVER_CRT.' failed: '.$!);
		next;
	}

	# Handle old certificate
	if (-w $certDir && -f $_->{cert}) {
		# Extract datetime suffix
		$suffix = ($dt = DateTime->from_epoch(epoch => $mtime))->ymd('').$dt->hms('');

		# Rename old certificate
		unless(rename($_->{cert}, $_->{cert}.'.'.$suffix)) {
			carp('rename '.$_->{cert}.' to '.$_->{cert}.'.'.$suffix.' failed: '.$!);
			next;
		}
	}

	# Save cert
	unless(write_file($_->{cert}, $content)) {
		carp('write_file '.$_->{cert}.' failed: '.$!);
		next;
	}
}

# Exit with success
exit EXIT_SUCCESS;