X-Git-Url: https://git.rapsys.eu/acme/blobdiff_plain/e714f0566d12b8ad35c9e3768ed6a6f0f5236a9f..707fca91ce64b098f3bc55721f630b6822d48bd8:/acmecron?ds=inline
diff --git a/acmecron b/acmecron
new file mode 100755
index 0000000..0ffeaad
--- /dev/null
+++ b/acmecron
@@ -0,0 +1,189 @@
+#! /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 .
+#
+# Copyright (C) 2016 - 2017 Raphaël Gertz 
+
+# 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;