]> Raphaël G. Git Repositories - acme/blob - acmecron
Add man page documentation
[acme] / acmecron
1 #! /usr/bin/perl
2
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 #
16 # Copyright (C) 2016 - 2017 Raphaël Gertz <acme@rapsys.eu>
17
18 # Best practice
19 use strict;
20 use warnings;
21
22 # Fix use of acl
23 use filetest 'access';
24
25 # Load dependancies
26 use Carp qw(carp confess);
27 use DateTime;
28 use File::Path qw(make_path);
29 use File::stat qw(stat);
30 use File::Spec;
31 use File::Slurp qw(read_file write_file);
32 use JSON qw(decode_json);
33 use IPC::System::Simple qw(capturex $EXITVAL);
34 use Acme qw(CERT_DIR CONFIG DS KEY_DIR SERVER_CRT SERVER_KEY ACCOUNT_KEY);
35
36 # Load POSIX
37 use POSIX qw(strftime EXIT_SUCCESS EXIT_FAILURE);
38
39 # Init debug
40 my $debug = 0;
41
42 # Init config
43 my $config = undef;
44
45 # Strip and enable debug
46 @ARGV = map { if ($_ eq '-d') { $debug = 1; (); } else { $_; } } @ARGV;
47
48 # Check config file
49 if (! -f CONFIG) {
50 print 'Config file '.CONFIG.' do not exists'."\n";
51 exit EXIT_FAILURE;
52 }
53
54 # Load config
55 unless (
56 #XXX: use eval to workaround a fatal in decode_json
57 eval {
58 # Read it
59 ($config = read_file(CONFIG)) &&
60 # Decode it
61 ($config = decode_json($config)) &&
62 # Check hash validity
63 defined($config->{certificates}) &&
64 # Check not empty
65 scalar($config->{certificates}) &&
66 # Check hash validity
67 defined($config->{thumbprint}) &&
68 # Check certificates array
69 ! scalar map {unless(defined($_->{cert}) && defined($_->{key}) && defined($_->{mail}) && defined($_->{domain}) && defined($_->{domains})) {1;} else {();}} @{$config->{certificates}}
70 }
71 ) {
72 print 'Config file '.CONFIG.' is not readable or invalid'."\n";
73 exit EXIT_FAILURE;
74 }
75
76 # Deal with certificates
77 foreach (@{$config->{certificates}}) {
78 # Init variables
79 my ($mtime, $dt, $suffix) = undef;
80
81 # Skip certificate without 60 days
82 if (-f $_->{cert} && ($mtime = stat($_->{cert})->mtime) >= (time() - 60*24*3600)) {
83 next;
84 }
85
86 # Extract cert directory and filename
87 my (undef, $certDir) = File::Spec->splitpath($_->{cert});
88
89 # Check that certificate is writable
90 unless (-w $certDir || -w $_->{cert}) {
91 carp('directory '.$certDir.' or file '.$_->{cert}.' must be writable: '.$!);
92 next;
93 }
94
95 # Check that key directory exists
96 if (! -d KEY_DIR) {
97 # Create all paths
98 make_path(KEY_DIR, {error => \my $err});
99 if (@$err) {
100 map {
101 my ($file, $msg) = %$_;
102 carp ($file eq '' ? '' : $file.': ').$msg if ($debug);
103 } @$err;
104 confess 'make_path failed';
105 }
106 }
107
108 # Unlink if is a symlink
109 if (-l KEY_DIR.DS.SERVER_KEY) {
110 unless(unlink(KEY_DIR.DS.SERVER_KEY)) {
111 carp('unlink '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!);
112 next;
113 }
114 }
115
116 # Symlink to key
117 unless(symlink($_->{key}, KEY_DIR.DS.SERVER_KEY)) {
118 carp('symlink '.$_->{key}.' to '.KEY_DIR.DS.SERVER_KEY.' failed: '.$!);
119 next;
120 }
121
122 # Unlink if is a symlink
123 if (-l KEY_DIR.DS.ACCOUNT_KEY) {
124 unless(unlink(KEY_DIR.DS.ACCOUNT_KEY)) {
125 carp('unlink '.KEY_DIR.DS.ACCOUNT_KEY.' failed: '.$!);
126 next;
127 }
128 }
129
130 # Symlink to key
131 unless(symlink($_->{account}, KEY_DIR.DS.ACCOUNT_KEY)) {
132 carp('symlink '.$_->{account}.' to '.KEY_DIR.DS.ACCOUNT_KEY.' failed: '.$!);
133 next;
134 }
135
136 # Init args
137 my @args = @{$_->{domains}};
138
139 # Prepend mail and domain to other args
140 unshift(@args, $_->{mail}, $_->{domain});
141
142 # Preprend prod
143 if (defined $_->{prod} && $_->{prod}) {
144 unshift(@args, '-p');
145 }
146
147 # Preprend debug
148 if ($debug) {
149 unshift(@args, '-d');
150 }
151
152 # Run acmecert with args
153 my @out = capturex([0..1], 'acmecert', @args);
154
155 # Deal with error
156 if ($EXITVAL != 0) {
157 print join("\n", @out) if ($debug);
158 carp('acmecert '.join(', ', @args).' failed: '.$!);
159 next;
160 }
161
162 # Read cert
163 my $content;
164 unless($content = read_file(CERT_DIR.DS.SERVER_CRT)) {
165 carp('read_file '.CERT_DIR.DS.SERVER_CRT.' failed: '.$!);
166 next;
167 }
168
169 # Handle old certificate
170 if (-w $certDir && -f $_->{cert}) {
171 # Extract datetime suffix
172 $suffix = ($dt = DateTime->from_epoch(epoch => $mtime))->ymd('').$dt->hms('');
173
174 # Rename old certificate
175 unless(rename($_->{cert}, $_->{cert}.'.'.$suffix)) {
176 carp('rename '.$_->{cert}.' to '.$_->{cert}.'.'.$suffix.' failed: '.$!);
177 next;
178 }
179 }
180
181 # Save cert
182 unless(write_file($_->{cert}, $content)) {
183 carp('write_file '.$_->{cert}.' failed: '.$!);
184 next;
185 }
186 }
187
188 # Exit with success
189 exit EXIT_SUCCESS;