Fix config generation
[acme] / letscron
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 <acmepl@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);
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 # Init args
123 my @args = @{$_->{domains}};
124
125 # Prepend mail and domain to other args
126 unshift(@args, $_->{mail}, $_->{domain});
127
128 # Preprend prod
129 if (defined $_->{prod} && $_->{prod}) {
130 unshift(@args, '-p');
131 }
132
133 # Preprend debug
134 if ($debug) {
135 unshift(@args, '-d');
136 }
137
138 # Run letscert with args
139 my @out = capturex([0..1], 'letscert', @args);
140
141 # Deal with error
142 if ($EXITVAL != 0) {
143 print join("\n", @out) if ($debug);
144 carp('letscert '.join(', ', @args).' failed: '.$!);
145 next;
146 }
147
148 # Read cert
149 my $content;
150 unless($content = read_file(CERT_DIR.DS.SERVER_CRT)) {
151 carp('read_file '.CERT_DIR.DS.SERVER_CRT.' failed: '.$!);
152 next;
153 }
154
155 # Handle old certificate
156 if (-w $certDir && -f $_->{cert}) {
157 # Extract datetime suffix
158 $suffix = ($dt = DateTime->from_epoch(epoch => $mtime))->ymd('').$dt->hms('');
159
160 # Rename old certificate
161 unless(rename($_->{cert}, $_->{cert}.'.'.$suffix)) {
162 carp('rename '.$_->{cert}.' to '.$_->{cert}.'.'.$suffix.' failed: '.$!);
163 next;
164 }
165 }
166
167 # Save cert
168 unless(write_file($_->{cert}, $content)) {
169 carp('write_file '.$_->{cert}.' failed: '.$!);
170 next;
171 }
172 }
173
174 # Exit with success
175 exit EXIT_SUCCESS;