]>
Commit | Line | Data |
---|---|---|
ac78733c WB |
1 | package PMG::API2::Certificates; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Certificate; | |
7 | use PVE::Exception qw(raise raise_param_exc); | |
8 | use PVE::JSONSchema qw(get_standard_option); | |
9 | use PVE::Tools qw(extract_param file_get_contents file_set_contents); | |
10 | ||
11 | use PMG::CertHelpers; | |
12 | use PMG::NodeConfig; | |
a0822f48 | 13 | use PMG::RS::Acme; |
ac78733c WB |
14 | use PMG::RS::CSR; |
15 | ||
16 | use PMG::API2::ACMEPlugin; | |
17 | ||
18 | use base qw(PVE::RESTHandler); | |
19 | ||
20 | my $acme_account_dir = PMG::CertHelpers::acme_account_dir(); | |
21 | ||
22 | sub first_typed_pem_entry : prototype($$) { | |
23 | my ($label, $data) = @_; | |
24 | ||
25 | if ($data =~ /^(-----BEGIN \Q$label\E-----\n.*?\n-----END \Q$label\E-----)$/ms) { | |
26 | return $1; | |
27 | } | |
28 | return undef; | |
29 | } | |
30 | ||
31 | sub pem_private_key : prototype($) { | |
32 | my ($data) = @_; | |
33 | return first_typed_pem_entry('PRIVATE KEY', $data); | |
34 | } | |
35 | ||
36 | sub pem_certificate : prototype($) { | |
37 | my ($data) = @_; | |
38 | return first_typed_pem_entry('CERTIFICATE', $data); | |
39 | } | |
40 | ||
41 | my sub restart_after_cert_update : prototype($) { | |
42 | my ($type) = @_; | |
43 | ||
44 | if ($type eq 'api') { | |
45 | print "Restarting pmgproxy\n"; | |
46 | PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pmgproxy']); | |
d7ce72c8 SI |
47 | |
48 | my $cinfo = PMG::ClusterConfig->new(); | |
49 | if (scalar(keys %{$cinfo->{ids}})) { | |
50 | print "Notify cluster about new fingerprint\n"; | |
51 | PMG::Cluster::trigger_update_fingerprints($cinfo); | |
52 | } | |
ac78733c WB |
53 | } |
54 | }; | |
55 | ||
56 | my sub update_cert : prototype($$$$$) { | |
57 | my ($type, $cert_path, $certificate, $force, $restart) = @_; | |
58 | my $code = sub { | |
59 | print "Setting custom certificate file $cert_path\n"; | |
52872fce | 60 | my $info = PMG::CertHelpers::set_cert_file($certificate, $cert_path, $force); |
ac78733c WB |
61 | |
62 | restart_after_cert_update($type) if $restart; | |
52872fce FE |
63 | |
64 | return $info; | |
ac78733c | 65 | }; |
52872fce | 66 | return PMG::CertHelpers::cert_lock(10, $code); |
ac78733c WB |
67 | }; |
68 | ||
69 | my sub set_smtp : prototype($$) { | |
70 | my ($on, $reload) = @_; | |
71 | ||
72 | my $code = sub { | |
73 | my $cfg = PMG::Config->new(); | |
6694a159 | 74 | # check if value actually would change |
1ecf138e SI |
75 | if (!$cfg->get('mail', 'tls') != !$on) { |
76 | print "Rewriting postfix config\n"; | |
77 | $cfg->set('mail', 'tls', $on); | |
78 | $cfg->write(); | |
6694a159 | 79 | $cfg->rewrite_config_postfix(); |
ac78733c WB |
80 | } |
81 | ||
1ecf138e | 82 | if ($reload) { |
ac78733c WB |
83 | print "Reloading postfix\n"; |
84 | PMG::Utils::service_cmd('postfix', 'reload'); | |
85 | } | |
86 | }; | |
87 | PMG::Config::lock_config($code, "failed to reload postfix"); | |
88 | } | |
89 | ||
90 | __PACKAGE__->register_method ({ | |
91 | name => 'index', | |
92 | path => '', | |
93 | method => 'GET', | |
94 | permissions => { user => 'all' }, | |
95 | description => "Node index.", | |
96 | parameters => { | |
97 | additionalProperties => 0, | |
98 | properties => { | |
99 | node => get_standard_option('pve-node'), | |
100 | }, | |
101 | }, | |
102 | returns => { | |
103 | type => 'array', | |
104 | items => { | |
105 | type => "object", | |
106 | properties => {}, | |
107 | }, | |
108 | links => [ { rel => 'child', href => "{name}" } ], | |
109 | }, | |
110 | code => sub { | |
111 | my ($param) = @_; | |
112 | ||
113 | return [ | |
114 | { name => 'acme' }, | |
115 | { name => 'custom' }, | |
116 | { name => 'info' }, | |
117 | { name => 'config' }, | |
118 | ]; | |
119 | }, | |
120 | }); | |
121 | ||
122 | __PACKAGE__->register_method ({ | |
123 | name => 'info', | |
124 | path => 'info', | |
125 | method => 'GET', | |
126 | permissions => { user => 'all' }, | |
127 | proxyto => 'node', | |
128 | protected => 1, | |
129 | description => "Get information about the node's certificates.", | |
130 | parameters => { | |
131 | additionalProperties => 0, | |
132 | properties => { | |
133 | node => get_standard_option('pve-node'), | |
134 | }, | |
135 | }, | |
136 | returns => { | |
137 | type => 'array', | |
138 | items => get_standard_option('pve-certificate-info'), | |
139 | }, | |
140 | code => sub { | |
141 | my ($param) = @_; | |
142 | ||
143 | my $res = []; | |
144 | for my $path (&PMG::CertHelpers::API_CERT, &PMG::CertHelpers::SMTP_CERT) { | |
145 | eval { | |
146 | my $info = PVE::Certificate::get_certificate_info($path); | |
147 | push @$res, $info if $info; | |
148 | }; | |
149 | } | |
150 | return $res; | |
151 | }, | |
152 | }); | |
153 | ||
154 | __PACKAGE__->register_method ({ | |
155 | name => 'custom_cert_index', | |
156 | path => 'custom', | |
157 | method => 'GET', | |
158 | permissions => { user => 'all' }, | |
159 | description => "Certificate index.", | |
160 | parameters => { | |
161 | additionalProperties => 0, | |
162 | properties => { | |
163 | node => get_standard_option('pve-node'), | |
164 | }, | |
165 | }, | |
166 | returns => { | |
167 | type => 'array', | |
168 | items => { | |
169 | type => "object", | |
170 | properties => {}, | |
171 | }, | |
172 | links => [ { rel => 'child', href => "{type}" } ], | |
173 | }, | |
174 | code => sub { | |
175 | my ($param) = @_; | |
176 | ||
177 | return [ | |
178 | { type => 'api' }, | |
179 | { type => 'smtp' }, | |
180 | ]; | |
181 | }, | |
182 | }); | |
183 | ||
184 | __PACKAGE__->register_method ({ | |
185 | name => 'upload_custom_cert', | |
186 | path => 'custom/{type}', | |
187 | method => 'POST', | |
188 | permissions => { check => [ 'admin' ] }, | |
189 | description => 'Upload or update custom certificate chain and key.', | |
190 | protected => 1, | |
191 | proxyto => 'node', | |
192 | parameters => { | |
193 | additionalProperties => 0, | |
194 | properties => { | |
195 | node => get_standard_option('pve-node'), | |
196 | certificates => { | |
197 | type => 'string', | |
198 | format => 'pem-certificate-chain', | |
199 | description => 'PEM encoded certificate (chain).', | |
200 | }, | |
201 | key => { | |
202 | type => 'string', | |
203 | description => 'PEM encoded private key.', | |
204 | format => 'pem-string', | |
205 | optional => 0, | |
206 | }, | |
207 | type => get_standard_option('pmg-certificate-type'), | |
208 | force => { | |
209 | type => 'boolean', | |
210 | description => 'Overwrite existing custom or ACME certificate files.', | |
211 | optional => 1, | |
212 | default => 0, | |
213 | }, | |
214 | restart => { | |
215 | type => 'boolean', | |
216 | description => 'Restart services.', | |
217 | optional => 1, | |
218 | default => 0, | |
219 | }, | |
220 | }, | |
221 | }, | |
222 | returns => get_standard_option('pve-certificate-info'), | |
223 | code => sub { | |
224 | my ($param) = @_; | |
225 | ||
226 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
227 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
228 | ||
229 | my $certs = extract_param($param, 'certificates'); | |
230 | $certs = PVE::Certificate::strip_leading_text($certs); | |
231 | ||
232 | my $key = extract_param($param, 'key'); | |
233 | if ($key) { | |
234 | $key = PVE::Certificate::strip_leading_text($key); | |
235 | $certs = "$key\n$certs"; | |
236 | } else { | |
237 | my $private_key = pem_private_key($certs); | |
238 | if (!defined($private_key)) { | |
239 | my $old = file_get_contents($cert_path); | |
240 | $private_key = pem_private_key($old); | |
241 | if (!defined($private_key)) { | |
242 | raise_param_exc({ | |
243 | 'key' => "Attempted to upload custom certificate without (existing) key." | |
244 | }) | |
245 | } | |
246 | ||
247 | # copy the old certificate's key: | |
248 | $certs = "$key\n$certs"; | |
249 | } | |
250 | } | |
251 | ||
252 | my $info; | |
253 | ||
254 | PMG::CertHelpers::cert_lock(10, sub { | |
52872fce | 255 | $info = update_cert($type, $cert_path, $certs, $param->{force}, $param->{restart}); |
ac78733c WB |
256 | }); |
257 | ||
258 | if ($type eq 'smtp') { | |
259 | set_smtp(1, $param->{restart}); | |
260 | } | |
261 | ||
262 | return $info; | |
263 | }}); | |
264 | ||
265 | __PACKAGE__->register_method ({ | |
266 | name => 'remove_custom_cert', | |
267 | path => 'custom/{type}', | |
268 | method => 'DELETE', | |
269 | permissions => { check => [ 'admin' ] }, | |
270 | description => 'DELETE custom certificate chain and key.', | |
271 | protected => 1, | |
272 | proxyto => 'node', | |
273 | parameters => { | |
274 | additionalProperties => 0, | |
275 | properties => { | |
276 | node => get_standard_option('pve-node'), | |
277 | type => get_standard_option('pmg-certificate-type'), | |
278 | restart => { | |
279 | type => 'boolean', | |
280 | description => 'Restart pmgproxy.', | |
281 | optional => 1, | |
282 | default => 0, | |
283 | }, | |
284 | }, | |
285 | }, | |
286 | returns => { | |
287 | type => 'null', | |
288 | }, | |
289 | code => sub { | |
290 | my ($param) = @_; | |
291 | ||
292 | my $type = extract_param($param, 'type'); | |
293 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
294 | ||
295 | my $code = sub { | |
296 | print "Deleting custom certificate files\n"; | |
297 | unlink $cert_path; | |
298 | PMG::Ticket::generate_api_cert(0) if $type eq 'api'; | |
299 | ||
300 | if ($param->{restart}) { | |
301 | restart_after_cert_update($type); | |
302 | } | |
303 | }; | |
304 | ||
305 | PMG::CertHelpers::cert_lock(10, $code); | |
306 | ||
307 | if ($type eq 'smtp') { | |
308 | set_smtp(0, $param->{restart}); | |
309 | } | |
310 | ||
311 | return undef; | |
312 | }}); | |
313 | ||
314 | __PACKAGE__->register_method ({ | |
315 | name => 'acme_cert_index', | |
316 | path => 'acme', | |
317 | method => 'GET', | |
318 | permissions => { user => 'all' }, | |
319 | description => "ACME Certificate index.", | |
320 | parameters => { | |
321 | additionalProperties => 0, | |
322 | properties => { | |
323 | node => get_standard_option('pve-node'), | |
324 | }, | |
325 | }, | |
326 | returns => { | |
327 | type => 'array', | |
328 | items => { | |
329 | type => "object", | |
330 | properties => {}, | |
331 | }, | |
332 | links => [ { rel => 'child', href => "{type}" } ], | |
333 | }, | |
334 | code => sub { | |
335 | my ($param) = @_; | |
336 | ||
337 | return [ | |
338 | { type => 'api' }, | |
339 | { type => 'smtp' }, | |
340 | ]; | |
341 | }, | |
342 | }); | |
343 | ||
344 | my $order_certificate = sub { | |
345 | my ($acme, $acme_node_config) = @_; | |
346 | ||
347 | my $plugins = PMG::API2::ACMEPlugin::load_config(); | |
348 | ||
349 | print "Placing ACME order\n"; | |
af6b569a | 350 | my ($order_url, $order) = $acme->new_order([ sort keys %{$acme_node_config->{domains}} ]); |
ac78733c WB |
351 | print "Order URL: $order_url\n"; |
352 | for my $auth_url (@{$order->{authorizations}}) { | |
353 | print "\nGetting authorization details from '$auth_url'\n"; | |
354 | my $auth = $acme->get_authorization($auth_url); | |
355 | ||
356 | # force lower case, like get_acme_conf does | |
357 | my $domain = lc($auth->{identifier}->{value}); | |
358 | if ($auth->{status} eq 'valid') { | |
359 | print "$domain is already validated!\n"; | |
360 | } else { | |
361 | print "The validation for $domain is pending!\n"; | |
362 | ||
363 | my $domain_config = $acme_node_config->{domains}->{$domain}; | |
364 | die "no config for domain '$domain'\n" if !$domain_config; | |
365 | ||
366 | my $plugin_id = $domain_config->{plugin}; | |
367 | ||
368 | my $plugin_cfg = $plugins->{ids}->{$plugin_id}; | |
369 | die "plugin '$plugin_id' for domain '$domain' not found!\n" | |
370 | if !$plugin_cfg; | |
371 | ||
372 | my $data = { | |
373 | plugin => $plugin_cfg, | |
374 | alias => $domain_config->{alias}, | |
375 | }; | |
376 | ||
377 | my $plugin = PVE::ACME::Challenge->lookup($plugin_cfg->{type}); | |
378 | $plugin->setup($acme, $auth, $data); | |
379 | ||
380 | print "Triggering validation\n"; | |
381 | eval { | |
382 | die "no validation URL returned by plugin '$plugin_id' for domain '$domain'\n" | |
383 | if !defined($data->{url}); | |
384 | ||
385 | $acme->request_challenge_validation($data->{url}); | |
386 | print "Sleeping for 5 seconds\n"; | |
387 | sleep 5; | |
388 | while (1) { | |
389 | $auth = $acme->get_authorization($auth_url); | |
390 | if ($auth->{status} eq 'pending') { | |
391 | print "Status is still 'pending', trying again in 10 seconds\n"; | |
392 | sleep 10; | |
393 | next; | |
394 | } elsif ($auth->{status} eq 'valid') { | |
395 | print "Status is 'valid', domain '$domain' OK!\n"; | |
396 | last; | |
397 | } | |
c58e1065 TL |
398 | my $error = "validating challenge '$auth_url' failed - status: $auth->{status}"; |
399 | for (@{$auth->{challenges}}) { | |
400 | $error .= ", $_->{error}->{detail}" if $_->{error}->{detail}; | |
401 | } | |
402 | die "$error\n"; | |
ac78733c WB |
403 | } |
404 | }; | |
405 | my $err = $@; | |
406 | eval { $plugin->teardown($acme, $auth, $data) }; | |
407 | warn "$@\n" if $@; | |
408 | die $err if $err; | |
409 | } | |
410 | } | |
411 | print "\nAll domains validated!\n"; | |
412 | print "\nCreating CSR\n"; | |
413 | # Currently we only support dns entries, so extract those from the order: | |
414 | my $san = [ | |
415 | map { | |
416 | $_->{value} | |
417 | } grep { | |
418 | $_->{type} eq 'dns' | |
419 | } $order->{identifiers}->@* | |
420 | ]; | |
421 | die "DNS identifiers are required to generate a CSR.\n" if !scalar @$san; | |
422 | my ($csr_der, $key) = PMG::RS::CSR::generate_csr($san, {}); | |
423 | ||
424 | my $finalize_error_cnt = 0; | |
425 | print "Checking order status\n"; | |
426 | while (1) { | |
427 | $order = $acme->get_order($order_url); | |
428 | if ($order->{status} eq 'pending') { | |
429 | print "still pending, trying to finalize order\n"; | |
430 | # FIXME | |
431 | # to be compatible with and without the order ready state we try to | |
432 | # finalize even at the 'pending' state and give up after 5 | |
433 | # unsuccessful tries this can be removed when the letsencrypt api | |
434 | # definitely has implemented the 'ready' state | |
435 | eval { | |
436 | $acme->finalize_order($order->{finalize}, $csr_der); | |
437 | }; | |
438 | if (my $err = $@) { | |
439 | die $err if $finalize_error_cnt >= 5; | |
440 | ||
441 | $finalize_error_cnt++; | |
442 | warn $err; | |
443 | } | |
444 | sleep 5; | |
445 | next; | |
446 | } elsif ($order->{status} eq 'ready') { | |
447 | print "Order is ready, finalizing order\n"; | |
448 | $acme->finalize_order($order->{finalize}, $csr_der); | |
449 | sleep 5; | |
450 | next; | |
451 | } elsif ($order->{status} eq 'processing') { | |
452 | print "still processing, trying again in 30 seconds\n"; | |
453 | sleep 30; | |
454 | next; | |
455 | } elsif ($order->{status} eq 'valid') { | |
456 | print "valid!\n"; | |
457 | last; | |
458 | } | |
459 | die "order status: $order->{status}\n"; | |
460 | } | |
461 | ||
462 | print "\nDownloading certificate\n"; | |
463 | my $cert = $acme->get_certificate($order->{certificate}); | |
464 | ||
465 | return ($cert, $key); | |
466 | }; | |
467 | ||
468 | # Filter domains and raise an error if the list becomes empty. | |
469 | my $filter_domains = sub { | |
470 | my ($acme_config, $type) = @_; | |
471 | ||
4b01992f | 472 | my $domains = PMG::NodeConfig::filter_domains_by_type($acme_config->{domains}, $type); |
ac78733c | 473 | |
4b01992f | 474 | if (!$domains) { |
ac78733c WB |
475 | raise("No domains configured for type '$type'\n", 400); |
476 | } | |
4b01992f WB |
477 | |
478 | $acme_config->{domains} = $domains; | |
ac78733c WB |
479 | }; |
480 | ||
481 | __PACKAGE__->register_method ({ | |
482 | name => 'new_acme_cert', | |
483 | path => 'acme/{type}', | |
484 | method => 'POST', | |
485 | permissions => { check => [ 'admin' ] }, | |
486 | description => 'Order a new certificate from ACME-compatible CA.', | |
487 | protected => 1, | |
488 | proxyto => 'node', | |
489 | parameters => { | |
490 | additionalProperties => 0, | |
491 | properties => { | |
492 | node => get_standard_option('pve-node'), | |
493 | type => get_standard_option('pmg-certificate-type'), | |
494 | force => { | |
495 | type => 'boolean', | |
496 | description => 'Overwrite existing custom certificate.', | |
497 | optional => 1, | |
498 | default => 0, | |
499 | }, | |
500 | }, | |
501 | }, | |
502 | returns => { | |
503 | type => 'string', | |
504 | }, | |
505 | code => sub { | |
506 | my ($param) = @_; | |
507 | ||
508 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
509 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
510 | raise_param_exc({'force' => "Custom certificate exists but 'force' is not set."}) | |
511 | if !$param->{force} && -e $cert_path; | |
512 | ||
513 | my $node_config = PMG::NodeConfig::load_config(); | |
514 | my $acme_config = PMG::NodeConfig::get_acme_conf($node_config); | |
515 | raise("ACME domain list in configuration is missing!", 400) | |
259f8a41 | 516 | if !($acme_config && $acme_config->{domains} && $acme_config->{domains}->%*); |
ac78733c WB |
517 | |
518 | $filter_domains->($acme_config, $type); | |
519 | ||
520 | my $rpcenv = PMG::RESTEnvironment->get(); | |
521 | my $authuser = $rpcenv->get_user(); | |
522 | ||
523 | my $realcmd = sub { | |
524 | STDOUT->autoflush(1); | |
525 | my $account = $acme_config->{account}; | |
526 | my $account_file = "${acme_account_dir}/${account}"; | |
527 | die "ACME account config file '$account' does not exist.\n" | |
528 | if ! -e $account_file; | |
529 | ||
530 | print "Loading ACME account details\n"; | |
531 | my $acme = PMG::RS::Acme->load($account_file); | |
532 | ||
533 | my ($cert, $key) = $order_certificate->($acme, $acme_config); | |
534 | my $certificate = "$key\n$cert"; | |
535 | ||
536 | update_cert($type, $cert_path, $certificate, $param->{force}, 1); | |
537 | ||
538 | if ($type eq 'smtp') { | |
539 | set_smtp(1, 1); | |
540 | } | |
541 | ||
542 | die "$@\n" if $@; | |
543 | }; | |
544 | ||
545 | return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd); | |
546 | }}); | |
547 | ||
548 | __PACKAGE__->register_method ({ | |
549 | name => 'renew_acme_cert', | |
550 | path => 'acme/{type}', | |
551 | method => 'PUT', | |
552 | permissions => { check => [ 'admin' ] }, | |
553 | description => "Renew existing certificate from CA.", | |
554 | protected => 1, | |
555 | proxyto => 'node', | |
556 | parameters => { | |
557 | additionalProperties => 0, | |
558 | properties => { | |
559 | node => get_standard_option('pve-node'), | |
560 | type => get_standard_option('pmg-certificate-type'), | |
561 | force => { | |
562 | type => 'boolean', | |
563 | description => 'Force renewal even if expiry is more than 30 days away.', | |
564 | optional => 1, | |
565 | default => 0, | |
566 | }, | |
567 | }, | |
568 | }, | |
569 | returns => { | |
570 | type => 'string', | |
571 | }, | |
572 | code => sub { | |
573 | my ($param) = @_; | |
574 | ||
575 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
576 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
577 | ||
578 | raise("No current (custom) certificate found, please order a new certificate!\n") | |
579 | if ! -e $cert_path; | |
580 | ||
581 | my $expires_soon = PVE::Certificate::check_expiry($cert_path, time() + 30*24*60*60); | |
582 | raise_param_exc({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."}) | |
583 | if !$expires_soon && !$param->{force}; | |
584 | ||
585 | my $node_config = PMG::NodeConfig::load_config(); | |
586 | my $acme_config = PMG::NodeConfig::get_acme_conf($node_config); | |
587 | raise("ACME domain list in configuration is missing!", 400) | |
588 | if !$acme_config || !$acme_config->{domains}->%*; | |
589 | ||
590 | $filter_domains->($acme_config, $type); | |
591 | ||
592 | my $rpcenv = PMG::RESTEnvironment->get(); | |
593 | my $authuser = $rpcenv->get_user(); | |
594 | ||
595 | my $old_cert = PVE::Tools::file_get_contents($cert_path); | |
596 | ||
597 | my $realcmd = sub { | |
598 | STDOUT->autoflush(1); | |
599 | my $account = $acme_config->{account}; | |
600 | my $account_file = "${acme_account_dir}/${account}"; | |
601 | die "ACME account config file '$account' does not exist.\n" | |
602 | if ! -e $account_file; | |
603 | ||
604 | print "Loading ACME account details\n"; | |
605 | my $acme = PMG::RS::Acme->load($account_file); | |
606 | ||
607 | my ($cert, $key) = $order_certificate->($acme, $acme_config); | |
608 | my $certificate = "$key\n$cert"; | |
609 | ||
610 | update_cert($type, $cert_path, $certificate, 1, 1); | |
611 | ||
612 | if (defined($old_cert)) { | |
613 | print "Revoking old certificate\n"; | |
58f5c8f1 WB |
614 | eval { |
615 | $old_cert = pem_certificate($old_cert) | |
616 | or die "no certificate section found in '$cert_path'\n"; | |
617 | $acme->revoke_certificate($old_cert, undef); | |
618 | }; | |
ac78733c WB |
619 | warn "Revoke request to CA failed: $@" if $@; |
620 | } | |
621 | }; | |
622 | ||
623 | return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd); | |
624 | }}); | |
625 | ||
626 | __PACKAGE__->register_method ({ | |
627 | name => 'revoke_acme_cert', | |
628 | path => 'acme/{type}', | |
629 | method => 'DELETE', | |
630 | permissions => { check => [ 'admin' ] }, | |
631 | description => "Revoke existing certificate from CA.", | |
632 | protected => 1, | |
633 | proxyto => 'node', | |
634 | parameters => { | |
635 | additionalProperties => 0, | |
636 | properties => { | |
637 | node => get_standard_option('pve-node'), | |
638 | type => get_standard_option('pmg-certificate-type'), | |
639 | }, | |
640 | }, | |
641 | returns => { | |
642 | type => 'string', | |
643 | }, | |
644 | code => sub { | |
645 | my ($param) = @_; | |
646 | ||
647 | my $type = extract_param($param, 'type'); # also used to know which service to restart | |
648 | my $cert_path = PMG::CertHelpers::cert_path($type); | |
649 | ||
650 | my $node_config = PMG::NodeConfig::load_config(); | |
651 | my $acme_config = PMG::NodeConfig::get_acme_conf($node_config); | |
652 | raise("ACME domain list in configuration is missing!", 400) | |
653 | if !$acme_config || !$acme_config->{domains}->%*; | |
654 | ||
655 | $filter_domains->($acme_config, $type); | |
656 | ||
657 | my $rpcenv = PMG::RESTEnvironment->get(); | |
658 | my $authuser = $rpcenv->get_user(); | |
659 | ||
660 | my $cert = PVE::Tools::file_get_contents($cert_path); | |
661 | $cert = pem_certificate($cert) | |
662 | or die "no certificate section found in '$cert_path'\n"; | |
663 | ||
664 | my $realcmd = sub { | |
665 | STDOUT->autoflush(1); | |
666 | my $account = $acme_config->{account}; | |
667 | my $account_file = "${acme_account_dir}/${account}"; | |
668 | die "ACME account config file '$account' does not exist.\n" | |
669 | if ! -e $account_file; | |
670 | ||
671 | print "Loading ACME account details\n"; | |
672 | my $acme = PMG::RS::Acme->load($account_file); | |
673 | ||
674 | print "Revoking old certificate\n"; | |
675 | eval { $acme->revoke_certificate($cert, undef) }; | |
676 | if (my $err = $@) { | |
677 | # is there a better check? | |
678 | die "Revoke request to CA failed: $err" if $err !~ /"Certificate is expired"/; | |
679 | } | |
680 | ||
681 | my $code = sub { | |
682 | print "Deleting certificate files\n"; | |
683 | unlink $cert_path; | |
684 | PMG::Ticket::generate_api_cert(0) if $type eq 'api'; | |
685 | ||
686 | restart_after_cert_update($type); | |
687 | }; | |
688 | ||
689 | PMG::CertHelpers::cert_lock(10, $code); | |
690 | ||
691 | if ($type eq 'smtp') { | |
692 | set_smtp(0, 1); | |
693 | } | |
694 | }; | |
695 | ||
696 | return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd); | |
697 | }}); | |
698 | ||
699 | 1; |