]>
Commit | Line | Data |
---|---|---|
66f35a7e WB |
1 | package PMG::CertHelpers; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Certificate; | |
7 | use PVE::JSONSchema; | |
8 | use PVE::Tools; | |
9 | ||
10 | use constant { | |
11 | API_CERT => '/etc/pmg/pmg-api.pem', | |
12 | SMTP_CERT => '/etc/pmg/pmg-tls.pem', | |
13 | }; | |
14 | ||
abbb3940 | 15 | my $account_prefix = '/etc/pmg/acme/accounts'; |
66f35a7e WB |
16 | |
17 | # TODO: Move `pve-acme-account-name` to common and reuse instead of this. | |
18 | PVE::JSONSchema::register_standard_option('pmg-acme-account-name', { | |
19 | description => 'ACME account config file name.', | |
20 | type => 'string', | |
21 | format => 'pve-configid', | |
22 | format_description => 'name', | |
23 | optional => 1, | |
24 | default => 'default', | |
25 | }); | |
26 | ||
27 | PVE::JSONSchema::register_standard_option('pmg-acme-account-contact', { | |
28 | type => 'string', | |
29 | format => 'email-list', | |
30 | description => 'Contact email addresses.', | |
31 | }); | |
32 | ||
33 | PVE::JSONSchema::register_standard_option('pmg-acme-directory-url', { | |
34 | type => 'string', | |
35 | description => 'URL of ACME CA directory endpoint.', | |
36 | pattern => '^https?://.*', | |
37 | }); | |
38 | ||
39 | PVE::JSONSchema::register_format('pmg-certificate-type', sub { | |
40 | my ($type, $noerr) = @_; | |
41 | ||
42 | if ($type =~ /^(?: api | smtp )$/x) { | |
43 | return $type; | |
44 | } | |
45 | return undef if $noerr; | |
46 | die "value '$type' does not look like a valid certificate type\n"; | |
47 | }); | |
48 | ||
49 | PVE::JSONSchema::register_standard_option('pmg-certificate-type', { | |
50 | type => 'string', | |
51 | description => 'The TLS certificate type (API or SMTP certificate).', | |
52 | enum => ['api', 'smtp'], | |
53 | }); | |
54 | ||
55 | PVE::JSONSchema::register_format('pmg-acme-domain', sub { | |
56 | my ($domain, $noerr) = @_; | |
57 | ||
58 | my $label = qr/[a-z0-9][a-z0-9_-]*/i; | |
59 | ||
0e543af7 | 60 | return $domain if $domain =~ /^(?:\*\.)?$label(?:\.$label)+$/; |
66f35a7e WB |
61 | return undef if $noerr; |
62 | die "value '$domain' does not look like a valid domain name!\n"; | |
63 | }); | |
64 | ||
65 | PVE::JSONSchema::register_format('pmg-acme-alias', sub { | |
66 | my ($alias, $noerr) = @_; | |
67 | ||
68 | my $label = qr/[a-z0-9_][a-z0-9_-]*/i; | |
69 | ||
70 | return $alias if $alias =~ /^$label(?:\.$label)+$/; | |
71 | return undef if $noerr; | |
72 | die "value '$alias' does not look like a valid alias name!\n"; | |
73 | }); | |
74 | ||
75 | my $local_cert_lock = '/var/lock/pmg-certs.lock'; | |
76 | my $local_acme_lock = '/var/lock/pmg-acme.lock'; | |
77 | ||
78 | sub cert_path : prototype($) { | |
79 | my ($type) = @_; | |
80 | if ($type eq 'api') { | |
81 | return API_CERT; | |
82 | } elsif ($type eq 'smtp') { | |
83 | return SMTP_CERT; | |
84 | } else { | |
85 | die "unknown certificate type '$type'\n"; | |
86 | } | |
87 | } | |
88 | ||
89 | sub cert_lock { | |
90 | my ($timeout, $code, @param) = @_; | |
91 | ||
92 | my $res = PVE::Tools::lock_file($local_cert_lock, $timeout, $code, @param); | |
93 | die $@ if $@; | |
94 | return $res; | |
95 | } | |
96 | ||
97 | sub set_cert_file { | |
98 | my ($cert, $cert_path, $force) = @_; | |
99 | ||
100 | my ($old_cert, $info); | |
101 | ||
102 | my $cert_path_old = "${cert_path}.old"; | |
103 | ||
104 | die "Custom certificate file exists but force flag is not set.\n" | |
105 | if !$force && -e $cert_path; | |
106 | ||
107 | PVE::Tools::file_copy($cert_path, $cert_path_old) if -e $cert_path; | |
108 | ||
109 | eval { | |
110 | my $gid = undef; | |
111 | if ($cert_path eq &API_CERT) { | |
112 | $gid = getgrnam('www-data') || | |
113 | die "user www-data not in group file\n"; | |
114 | } | |
115 | ||
116 | if (defined($gid)) { | |
117 | my $cert_path_tmp = "${cert_path}.tmp"; | |
118 | PVE::Tools::file_set_contents($cert_path_tmp, $cert, 0640); | |
119 | if (!chown(-1, $gid, $cert_path_tmp)) { | |
120 | my $msg = | |
121 | "failed to change group ownership of '$cert_path_tmp' to www-data ($gid): $!\n"; | |
122 | unlink($cert_path_tmp); | |
123 | die $msg; | |
124 | } | |
125 | if (!rename($cert_path_tmp, $cert_path)) { | |
126 | my $msg = | |
127 | "failed to rename '$cert_path_tmp' to '$cert_path': $!\n"; | |
128 | unlink($cert_path_tmp); | |
129 | die $msg; | |
130 | } | |
131 | } else { | |
132 | PVE::Tools::file_set_contents($cert_path, $cert, 0600); | |
133 | } | |
134 | ||
135 | $info = PVE::Certificate::get_certificate_info($cert_path); | |
136 | }; | |
137 | my $err = $@; | |
138 | ||
139 | if ($err) { | |
140 | if (-e $cert_path_old) { | |
141 | eval { | |
142 | warn "Attempting to restore old certificate file..\n"; | |
143 | PVE::Tools::file_copy($cert_path_old, $cert_path); | |
144 | }; | |
145 | warn "$@\n" if $@; | |
146 | } | |
147 | die "Setting certificate files failed - $err\n" | |
148 | } | |
149 | ||
150 | unlink $cert_path_old; | |
151 | ||
152 | return $info; | |
153 | } | |
154 | ||
155 | sub lock_acme { | |
156 | my ($account_name, $timeout, $code, @param) = @_; | |
157 | ||
158 | my $file = "$local_acme_lock.$account_name"; | |
159 | ||
160 | my $res = PVE::Tools::lock_file($file, $timeout, $code, @param); | |
161 | die $@ if $@; | |
162 | return $res; | |
163 | } | |
164 | ||
165 | sub acme_account_dir { | |
166 | return $account_prefix; | |
167 | } | |
168 | ||
169 | sub list_acme_accounts { | |
170 | my $accounts = []; | |
171 | ||
172 | return $accounts if ! -d $account_prefix; | |
173 | ||
174 | PVE::Tools::dir_glob_foreach($account_prefix, qr/[^.]+.*/, sub { | |
175 | my ($name) = @_; | |
176 | ||
177 | push @$accounts, $name | |
178 | if PVE::JSONSchema::pve_verify_configid($name, 1); | |
179 | }); | |
180 | ||
181 | return $accounts; | |
182 | } |