From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 94EEE6A7A9 for ; Mon, 15 Mar 2021 12:09:10 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8B5741FF38 for ; Mon, 15 Mar 2021 12:08:40 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 8AE7E1FF2E for ; Mon, 15 Mar 2021 12:08:38 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 52A7642077 for ; Mon, 15 Mar 2021 12:08:38 +0100 (CET) Date: Mon, 15 Mar 2021 12:08:28 +0100 From: Fabian =?iso-8859-1?q?Gr=FCnbichler?= To: pmg-devel@lists.proxmox.com, Wolfgang Bumiller References: <20210312152421.30114-1-w.bumiller@proxmox.com> <20210312152421.30114-7-w.bumiller@proxmox.com> In-Reply-To: <20210312152421.30114-7-w.bumiller@proxmox.com> MIME-Version: 1.0 User-Agent: astroid/0.15.0 (https://github.com/astroidmail/astroid) Message-Id: <1615805797.47mo6gu7e0.astroid@nora.none> Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable X-SPAM-LEVEL: Spam detection results: 0 AWL 0.027 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pmg-devel] [PATCH v2 api 6/8] add certificates api endpoint X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 15 Mar 2021 11:09:10 -0000 same nit as for patch 5 marked in-line On March 12, 2021 4:23 pm, Wolfgang Bumiller wrote: > This adds /nodes/{nodename}/certificates endpoint > containing: >=20 > /custom/{type} - update smtp or api certificates manually > /acme/{type} - update via acme >=20 > Signed-off-by: Wolfgang Bumiller > --- > Changes since v1: > * certificate regex simplification > * added $restart parameter to update_cert > * added set_smtp() helper to enable/disable tls & reload postfix > * dedup update_cert/set_smtp code copies >=20 > src/Makefile | 1 + > src/PMG/API2/Certificates.pm | 682 +++++++++++++++++++++++++++++++++++ > src/PMG/API2/Nodes.pm | 7 + > 3 files changed, 690 insertions(+) > create mode 100644 src/PMG/API2/Certificates.pm >=20 > diff --git a/src/Makefile b/src/Makefile > index ebc6bd8..e0629b2 100644 > --- a/src/Makefile > +++ b/src/Makefile > @@ -155,6 +155,7 @@ LIBSOURCES =3D \ > PMG/API2/When.pm \ > PMG/API2/What.pm \ > PMG/API2/Action.pm \ > + PMG/API2/Certificates.pm \ > PMG/API2/ACME.pm \ > PMG/API2/ACMEPlugin.pm \ > PMG/API2.pm \ > diff --git a/src/PMG/API2/Certificates.pm b/src/PMG/API2/Certificates.pm > new file mode 100644 > index 0000000..ca8b75b > --- /dev/null > +++ b/src/PMG/API2/Certificates.pm > @@ -0,0 +1,682 @@ > +package PMG::API2::Certificates; > + > +use strict; > +use warnings; > + > +use PVE::Certificate; > +use PVE::Exception qw(raise raise_param_exc); > +use PVE::JSONSchema qw(get_standard_option); > +use PVE::Tools qw(extract_param file_get_contents file_set_contents); > + > +use PMG::CertHelpers; > +use PMG::NodeConfig; > +use PMG::RS::CSR; > + > +use PMG::API2::ACMEPlugin; > + > +use base qw(PVE::RESTHandler); > + > +my $acme_account_dir =3D PMG::CertHelpers::acme_account_dir(); > + > +sub first_typed_pem_entry : prototype($$) { > + my ($label, $data) =3D @_; > + > + if ($data =3D~ /^(-----BEGIN \Q$label\E-----\n.*?\n-----END \Q$label= \E-----)$/ms) { > + return $1; > + } > + return undef; > +} > + > +sub pem_private_key : prototype($) { > + my ($data) =3D @_; > + return first_typed_pem_entry('PRIVATE KEY', $data); > +} > + > +sub pem_certificate : prototype($) { > + my ($data) =3D @_; > + return first_typed_pem_entry('CERTIFICATE', $data); > +} > + > +my sub restart_after_cert_update : prototype($) { > + my ($type) =3D @_; > + > + if ($type eq 'api') { > + print "Restarting pmgproxy\n"; > + PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pmgproxy'])= ; > + } > +}; > + > +my sub update_cert : prototype($$$$$) { > + my ($type, $cert_path, $certificate, $force, $restart) =3D @_; > + my $code =3D sub { > + print "Setting custom certificate file $cert_path\n"; > + PMG::CertHelpers::set_cert_file($certificate, $cert_path, $force); > + > + restart_after_cert_update($type) if $restart; > + }; > + PMG::CertHelpers::cert_lock(10, $code); > +}; > + > +my sub set_smtp : prototype($$) { > + my ($on, $reload) =3D @_; > + > + my $code =3D sub { > + my $cfg =3D PMG::Config->new(); > + > + print "Rewriting postfix config\n"; > + $cfg->set('mail', 'tls', $on); > + my $changed =3D $cfg->rewrite_config_postfix(); > + > + if ($changed && $reload) { > + print "Reloading postfix\n"; > + PMG::Utils::service_cmd('postfix', 'reload'); > + } > + }; > + PMG::Config::lock_config($code, "failed to reload postfix"); > +} > + > +__PACKAGE__->register_method ({ > + name =3D> 'index', > + path =3D> '', > + method =3D> 'GET', > + permissions =3D> { user =3D> 'all' }, > + description =3D> "Node index.", > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> "object", > + properties =3D> {}, > + }, > + links =3D> [ { rel =3D> 'child', href =3D> "{name}" } ], > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + return [ > + { name =3D> 'acme' }, > + { name =3D> 'custom' }, > + { name =3D> 'info' }, > + { name =3D> 'config' }, > + ]; > + }, > +}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'info', > + path =3D> 'info', > + method =3D> 'GET', > + permissions =3D> { user =3D> 'all' }, > + proxyto =3D> 'node', > + protected =3D> 1, > + description =3D> "Get information about the node's certificates.", > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> get_standard_option('pve-certificate-info'), > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $res =3D []; > + for my $path (&PMG::CertHelpers::API_CERT, &PMG::CertHelpers::SMTP_CERT= ) { > + eval { > + my $info =3D PVE::Certificate::get_certificate_info($path); > + push @$res, $info if $info; > + }; > + } > + return $res; > + }, > +}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'custom_cert_index', > + path =3D> 'custom', > + method =3D> 'GET', > + permissions =3D> { user =3D> 'all' }, > + description =3D> "Certificate index.", > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> "object", > + properties =3D> {}, > + }, > + links =3D> [ { rel =3D> 'child', href =3D> "{type}" } ], > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + return [ > + { type =3D> 'api' }, > + { type =3D> 'smtp' }, > + ]; > + }, > +}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'upload_custom_cert', > + path =3D> 'custom/{type}', > + method =3D> 'POST', > + permissions =3D> { check =3D> [ 'admin' ] }, > + description =3D> 'Upload or update custom certificate chain and key.= ', > + protected =3D> 1, > + proxyto =3D> 'node', > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + certificates =3D> { > + type =3D> 'string', > + format =3D> 'pem-certificate-chain', > + description =3D> 'PEM encoded certificate (chain).', > + }, > + key =3D> { > + type =3D> 'string', > + description =3D> 'PEM encoded private key.', > + format =3D> 'pem-string', > + optional =3D> 0, > + }, > + type =3D> get_standard_option('pmg-certificate-type'), > + force =3D> { > + type =3D> 'boolean', > + description =3D> 'Overwrite existing custom or ACME certificate files.= ', > + optional =3D> 1, > + default =3D> 0, > + }, > + restart =3D> { > + type =3D> 'boolean', > + description =3D> 'Restart services.', > + optional =3D> 1, > + default =3D> 0, > + }, > + }, > + }, > + returns =3D> get_standard_option('pve-certificate-info'), > + code =3D> sub { > + my ($param) =3D @_; > + > + my $type =3D extract_param($param, 'type'); # also used to know which s= ervice to restart > + my $cert_path =3D PMG::CertHelpers::cert_path($type); > + > + my $certs =3D extract_param($param, 'certificates'); > + $certs =3D PVE::Certificate::strip_leading_text($certs); > + > + my $key =3D extract_param($param, 'key'); > + if ($key) { > + $key =3D PVE::Certificate::strip_leading_text($key); > + $certs =3D "$key\n$certs"; > + } else { > + my $private_key =3D pem_private_key($certs); > + if (!defined($private_key)) { > + my $old =3D file_get_contents($cert_path); > + $private_key =3D pem_private_key($old); > + if (!defined($private_key)) { > + raise_param_exc({ > + 'key' =3D> "Attempted to upload custom certificate without (existing)= key." > + }) > + } > + > + # copy the old certificate's key: > + $certs =3D "$key\n$certs"; > + } > + } > + > + my $info; > + > + PMG::CertHelpers::cert_lock(10, sub { > + update_cert($type, $cert_path, $certs, $param->{force}, $param->{re= start}); > + }); > + > + if ($type eq 'smtp') { > + set_smtp(1, $param->{restart}); > + } > + > + return $info; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'remove_custom_cert', > + path =3D> 'custom/{type}', > + method =3D> 'DELETE', > + permissions =3D> { check =3D> [ 'admin' ] }, > + description =3D> 'DELETE custom certificate chain and key.', > + protected =3D> 1, > + proxyto =3D> 'node', > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + type =3D> get_standard_option('pmg-certificate-type'), > + restart =3D> { > + type =3D> 'boolean', > + description =3D> 'Restart pmgproxy.', > + optional =3D> 1, > + default =3D> 0, > + }, > + }, > + }, > + returns =3D> { > + type =3D> 'null', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $type =3D extract_param($param, 'type'); > + my $cert_path =3D PMG::CertHelpers::cert_path($type); > + > + my $code =3D sub { > + print "Deleting custom certificate files\n"; > + unlink $cert_path; > + > + if ($param->{restart}) { > + restart_after_cert_update($type); > + } > + }; > + > + PMG::CertHelpers::cert_lock(10, $code); > + > + if ($type eq 'smtp') { > + set_smtp(0, $param->{restart}); > + } > + > + return undef; > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'acme_cert_index', > + path =3D> 'acme', > + method =3D> 'GET', > + permissions =3D> { user =3D> 'all' }, > + description =3D> "ACME Certificate index.", > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + }, > + }, > + returns =3D> { > + type =3D> 'array', > + items =3D> { > + type =3D> "object", > + properties =3D> {}, > + }, > + links =3D> [ { rel =3D> 'child', href =3D> "{type}" } ], > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + return [ > + { type =3D> 'api' }, > + { type =3D> 'smtp' }, > + ]; > + }, > +}); > + > +my $order_certificate =3D sub { > + my ($acme, $acme_node_config) =3D @_; > + > + my $plugins =3D PMG::API2::ACMEPlugin::load_config(); > + > + print "Placing ACME order\n"; > + my ($order_url, $order) =3D $acme->new_order([ keys %{$acme_node_con= fig->{domains}} ]); > + print "Order URL: $order_url\n"; > + for my $auth_url (@{$order->{authorizations}}) { > + print "\nGetting authorization details from '$auth_url'\n"; > + my $auth =3D $acme->get_authorization($auth_url); > + > + # force lower case, like get_acme_conf does > + my $domain =3D lc($auth->{identifier}->{value}); > + if ($auth->{status} eq 'valid') { > + print "$domain is already validated!\n"; > + } else { > + print "The validation for $domain is pending!\n"; > + > + my $domain_config =3D $acme_node_config->{domains}->{$domain}; > + die "no config for domain '$domain'\n" if !$domain_config; > + > + my $plugin_id =3D $domain_config->{plugin}; > + > + my $plugin_cfg =3D $plugins->{ids}->{$plugin_id}; > + die "plugin '$plugin_id' for domain '$domain' not found!\n" > + if !$plugin_cfg; > + > + my $data =3D { > + plugin =3D> $plugin_cfg, > + alias =3D> $domain_config->{alias}, > + }; > + > + my $plugin =3D PVE::ACME::Challenge->lookup($plugin_cfg->{type}); > + $plugin->setup($acme, $auth, $data); > + > + print "Triggering validation\n"; > + eval { > + die "no validation URL returned by plugin '$plugin_id' for domain '$do= main'\n" > + if !defined($data->{url}); > + > + $acme->request_challenge_validation($data->{url}); > + print "Sleeping for 5 seconds\n"; > + sleep 5; > + while (1) { > + $auth =3D $acme->get_authorization($auth_url); > + if ($auth->{status} eq 'pending') { > + print "Status is still 'pending', trying again in 10 seconds\n"; > + sleep 10; > + next; > + } elsif ($auth->{status} eq 'valid') { > + print "Status is 'valid', domain '$domain' OK!\n"; > + last; > + } > + die "validating challenge '$auth_url' failed - status: $auth->{sta= tus}\n"; > + } > + }; > + my $err =3D $@; > + eval { $plugin->teardown($acme, $auth, $data) }; > + warn "$@\n" if $@; > + die $err if $err; > + } > + } > + print "\nAll domains validated!\n"; > + print "\nCreating CSR\n"; > + # Currently we only support dns entries, so extract those from the o= rder: > + my $san =3D [ > + map { > + $_->{value} > + } grep { > + $_->{type} eq 'dns' > + } $order->{identifiers}->@* > + ]; > + die "DNS identifiers are required to generate a CSR.\n" if !scalar @= $san; > + my ($csr_der, $key) =3D PMG::RS::CSR::generate_csr($san, {}); > + > + my $finalize_error_cnt =3D 0; > + print "Checking order status\n"; > + while (1) { > + $order =3D $acme->get_order($order_url); > + if ($order->{status} eq 'pending') { > + print "still pending, trying to finalize order\n"; > + # FIXME > + # to be compatible with and without the order ready state we try to > + # finalize even at the 'pending' state and give up after 5 > + # unsuccessful tries this can be removed when the letsencrypt api > + # definitely has implemented the 'ready' state > + eval { > + $acme->finalize_order($order->{finalize}, $csr_der); > + }; > + if (my $err =3D $@) { > + die $err if $finalize_error_cnt >=3D 5; > + > + $finalize_error_cnt++; > + warn $err; > + } > + sleep 5; > + next; > + } elsif ($order->{status} eq 'ready') { > + print "Order is ready, finalizing order\n"; > + $acme->finalize_order($order->{finalize}, $csr_der); > + sleep 5; > + next; > + } elsif ($order->{status} eq 'processing') { > + print "still processing, trying again in 30 seconds\n"; > + sleep 30; > + next; > + } elsif ($order->{status} eq 'valid') { > + print "valid!\n"; > + last; > + } > + die "order status: $order->{status}\n"; > + } > + > + print "\nDownloading certificate\n"; > + my $cert =3D $acme->get_certificate($order->{certificate}); > + > + return ($cert, $key); > +}; > + > +# Filter domains and raise an error if the list becomes empty. > +my $filter_domains =3D sub { > + my ($acme_config, $type) =3D @_; > + > + my $domains =3D $acme_config->{domains}; > + foreach my $domain (keys %$domains) { > + my $entry =3D $domains->{$domain}; > + if (!(grep { $_ eq $type } PVE::Tools::split_list($entry->{usage}))) { > + delete $domains->{$domain}; > + } > + } > + > + if (!%$domains) { > + raise("No domains configured for type '$type'\n", 400); > + } > +}; > + > +__PACKAGE__->register_method ({ > + name =3D> 'new_acme_cert', > + path =3D> 'acme/{type}', > + method =3D> 'POST', > + permissions =3D> { check =3D> [ 'admin' ] }, > + description =3D> 'Order a new certificate from ACME-compatible CA.', > + protected =3D> 1, > + proxyto =3D> 'node', > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + type =3D> get_standard_option('pmg-certificate-type'), > + force =3D> { > + type =3D> 'boolean', > + description =3D> 'Overwrite existing custom certificate.', > + optional =3D> 1, > + default =3D> 0, > + }, > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $type =3D extract_param($param, 'type'); # also used to know which s= ervice to restart > + my $cert_path =3D PMG::CertHelpers::cert_path($type); > + raise_param_exc({'force' =3D> "Custom certificate exists but 'force' is= not set."}) > + if !$param->{force} && -e $cert_path; > + > + my $node_config =3D PMG::NodeConfig::load_config(); > + my $acme_config =3D PMG::NodeConfig::get_acme_conf($node_config); > + raise("ACME domain list in configuration is missing!", 400) > + if !$acme_config || !$acme_config->{domains}->%*; > + > + $filter_domains->($acme_config, $type); > + > + my $rpcenv =3D PMG::RESTEnvironment->get(); > + my $authuser =3D $rpcenv->get_user(); > + > + my $realcmd =3D sub { > + STDOUT->autoflush(1); > + my $account =3D $acme_config->{account}; > + my $account_file =3D "${acme_account_dir}/${account}"; > + die "ACME account config file '$account' does not exist.\n" > + if ! -e $account_file; could be skipped > + > + print "Loading ACME account details\n"; > + my $acme =3D PMG::RS::Acme->load($account_file); since this should error with a nice message anyway? > + > + my ($cert, $key) =3D $order_certificate->($acme, $acme_config); > + my $certificate =3D "$key\n$cert"; > + > + update_cert($type, $cert_path, $certificate, $param->{force}, 1); > + > + if ($type eq 'smtp') { > + set_smtp(1, 1); > + } > + > + die "$@\n" if $@; > + }; > + > + return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd); > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'renew_acme_cert', > + path =3D> 'acme/{type}', > + method =3D> 'PUT', > + permissions =3D> { check =3D> [ 'admin' ] }, > + description =3D> "Renew existing certificate from CA.", > + protected =3D> 1, > + proxyto =3D> 'node', > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + type =3D> get_standard_option('pmg-certificate-type'), > + force =3D> { > + type =3D> 'boolean', > + description =3D> 'Force renewal even if expiry is more than 30 days aw= ay.', > + optional =3D> 1, > + default =3D> 0, > + }, > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $type =3D extract_param($param, 'type'); # also used to know which s= ervice to restart > + my $cert_path =3D PMG::CertHelpers::cert_path($type); > + > + raise("No current (custom) certificate found, please order a new certif= icate!\n") > + if ! -e $cert_path; > + > + my $expires_soon =3D PVE::Certificate::check_expiry($cert_path, time() = + 30*24*60*60); > + raise_param_exc({'force' =3D> "Certificate does not expire within the n= ext 30 days, and 'force' is not set."}) > + if !$expires_soon && !$param->{force}; > + > + my $node_config =3D PMG::NodeConfig::load_config(); > + my $acme_config =3D PMG::NodeConfig::get_acme_conf($node_config); > + raise("ACME domain list in configuration is missing!", 400) > + if !$acme_config || !$acme_config->{domains}->%*; > + > + $filter_domains->($acme_config, $type); > + > + my $rpcenv =3D PMG::RESTEnvironment->get(); > + my $authuser =3D $rpcenv->get_user(); > + > + my $old_cert =3D PVE::Tools::file_get_contents($cert_path); > + > + my $realcmd =3D sub { > + STDOUT->autoflush(1); > + my $account =3D $acme_config->{account}; > + my $account_file =3D "${acme_account_dir}/${account}"; > + die "ACME account config file '$account' does not exist.\n" > + if ! -e $account_file; same > + > + print "Loading ACME account details\n"; > + my $acme =3D PMG::RS::Acme->load($account_file); same > + > + my ($cert, $key) =3D $order_certificate->($acme, $acme_config); > + my $certificate =3D "$key\n$cert"; > + > + update_cert($type, $cert_path, $certificate, 1, 1); > + > + if (defined($old_cert)) { > + print "Revoking old certificate\n"; > + eval { $acme->revoke_certificate($old_cert, undef) }; > + warn "Revoke request to CA failed: $@" if $@; > + } > + }; > + > + return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd); > + }}); > + > +__PACKAGE__->register_method ({ > + name =3D> 'revoke_acme_cert', > + path =3D> 'acme/{type}', > + method =3D> 'DELETE', > + permissions =3D> { check =3D> [ 'admin' ] }, > + description =3D> "Revoke existing certificate from CA.", > + protected =3D> 1, > + proxyto =3D> 'node', > + parameters =3D> { > + additionalProperties =3D> 0, > + properties =3D> { > + node =3D> get_standard_option('pve-node'), > + type =3D> get_standard_option('pmg-certificate-type'), > + }, > + }, > + returns =3D> { > + type =3D> 'string', > + }, > + code =3D> sub { > + my ($param) =3D @_; > + > + my $type =3D extract_param($param, 'type'); # also used to know which s= ervice to restart > + my $cert_path =3D PMG::CertHelpers::cert_path($type); > + > + my $node_config =3D PMG::NodeConfig::load_config(); > + my $acme_config =3D PMG::NodeConfig::get_acme_conf($node_config); > + raise("ACME domain list in configuration is missing!", 400) > + if !$acme_config || !$acme_config->{domains}->%*; > + > + $filter_domains->($acme_config, $type); > + > + my $rpcenv =3D PMG::RESTEnvironment->get(); > + my $authuser =3D $rpcenv->get_user(); > + > + my $cert =3D PVE::Tools::file_get_contents($cert_path); > + $cert =3D pem_certificate($cert) > + or die "no certificate section found in '$cert_path'\n"; > + > + my $realcmd =3D sub { > + STDOUT->autoflush(1); > + my $account =3D $acme_config->{account}; > + my $account_file =3D "${acme_account_dir}/${account}"; > + die "ACME account config file '$account' does not exist.\n" > + if ! -e $account_file; same > + > + print "Loading ACME account details\n"; > + my $acme =3D PMG::RS::Acme->load($account_file); same > + > + print "Revoking old certificate\n"; > + eval { $acme->revoke_certificate($cert, undef) }; > + if (my $err =3D $@) { > + # is there a better check? > + die "Revoke request to CA failed: $err" if $err !~ /"Certificate is ex= pired"/; > + } > + > + my $code =3D sub { > + print "Deleting certificate files\n"; > + unlink $cert_path; > + > + # FIXME: Regenerate self-signed `api` certificate. > + restart_after_cert_update($type); > + }; > + > + PMG::CertHelpers::cert_lock(10, $code); > + > + if ($type eq 'smtp') { > + set_smtp(0, 1); > + } > + }; > + > + return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd); > + }}); > + > +1; > diff --git a/src/PMG/API2/Nodes.pm b/src/PMG/API2/Nodes.pm > index c0f5963..b6f0cd5 100644 > --- a/src/PMG/API2/Nodes.pm > +++ b/src/PMG/API2/Nodes.pm > @@ -27,6 +27,7 @@ use PMG::API2::Postfix; > use PMG::API2::MailTracker; > use PMG::API2::Backup; > use PMG::API2::PBS::Job; > +use PMG::API2::Certificates; > =20 > use base qw(PVE::RESTHandler); > =20 > @@ -85,6 +86,11 @@ __PACKAGE__->register_method ({ > path =3D> 'pbs', > }); > =20 > +__PACKAGE__->register_method ({ > + subclass =3D> "PMG::API2::Certificates", > + path =3D> 'certificates', > +}); > + > __PACKAGE__->register_method ({ > name =3D> 'index', > path =3D> '', > @@ -126,6 +132,7 @@ __PACKAGE__->register_method ({ > { name =3D> 'subscription' }, > { name =3D> 'termproxy' }, > { name =3D> 'rrddata' }, > + { name =3D> 'certificates' }, > ]; > =20 > return $result; > --=20 > 2.20.1 >=20 >=20 >=20 > _______________________________________________ > pmg-devel mailing list > pmg-devel@lists.proxmox.com > https://lists.proxmox.com/cgi-bin/mailman/listinfo/pmg-devel >=20 >=20 >=20 =