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 B76C268B37 for ; Tue, 9 Mar 2021 15:14:48 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C8E91B1AA for ; Tue, 9 Mar 2021 15:14:17 +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 E3B45B0E1 for ; Tue, 9 Mar 2021 15:14:09 +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 AC41A46011 for ; Tue, 9 Mar 2021 15:14:09 +0100 (CET) From: Wolfgang Bumiller To: pmg-devel@lists.proxmox.com Date: Tue, 9 Mar 2021 15:13:52 +0100 Message-Id: <20210309141401.19237-9-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210309141401.19237-1-w.bumiller@proxmox.com> References: <20210309141401.19237-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.040 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [pmgconfig.pm] Subject: [pmg-devel] [PATCH api 8/8] add acme and cert subcommands to pmgconfig 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: Tue, 09 Mar 2021 14:14:48 -0000 Signed-off-by: Wolfgang Bumiller --- src/PMG/CLI/pmgconfig.pm | 178 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/src/PMG/CLI/pmgconfig.pm b/src/PMG/CLI/pmgconfig.pm index 85edfa5..4f948cf 100644 --- a/src/PMG/CLI/pmgconfig.pm +++ b/src/PMG/CLI/pmgconfig.pm @@ -5,10 +5,13 @@ use warnings; use IO::File; use Data::Dumper; +use Term::ReadLine; + use PVE::SafeSyslog; use PVE::Tools qw(extract_param); use PVE::INotify; use PVE::CLIHandler; +use PVE::JSONSchema qw(get_standard_option); use PMG::RESTEnvironment; use PMG::RuleDB; @@ -18,14 +21,52 @@ use PMG::LDAPConfig; use PMG::LDAPSet; use PMG::Config; use PMG::Ticket; + +use PMG::API2::ACME; +use PMG::API2::ACMEPlugin; +use PMG::API2::Certificates; use PMG::API2::DKIMSign; use base qw(PVE::CLIHandler); +my $nodename = PVE::INotify::nodename(); + sub setup_environment { PMG::RESTEnvironment->setup_default_cli_env(); } +my $upid_exit = sub { + my $upid = shift; + my $status = PVE::Tools::upid_read_status($upid); + print "Task $status\n"; + exit($status eq 'OK' ? 0 : -1); +}; + +sub param_mapping { + my ($name) = @_; + + my $load_file_and_encode = sub { + my ($filename) = @_; + + return PVE::ACME::Challenge->encode_value('string', 'data', PVE::Tools::file_get_contents($filename)); + }; + + my $mapping = { + 'upload_custom_cert' => [ + 'certificates', + 'key', + ], + 'add_plugin' => [ + ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0], + ], + 'update_plugin' => [ + ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0], + ], + }; + + return $mapping->{$name}; +} + __PACKAGE__->register_method ({ name => 'dump', path => 'dump', @@ -184,6 +225,84 @@ __PACKAGE__->register_method ({ return undef; }}); +__PACKAGE__->register_method({ + name => 'acme_register', + path => 'acme_register', + method => 'POST', + description => "Register a new ACME account with a compatible CA.", + parameters => { + additionalProperties => 0, + properties => { + name => get_standard_option('pmg-acme-account-name'), + contact => get_standard_option('pmg-acme-account-contact'), + directory => get_standard_option('pmg-acme-directory-url', { + optional => 1, + }), + }, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + if (!$param->{directory}) { + my $directories = PMG::API2::ACME->get_directories({}); + print "Directory endpoints:\n"; + my $i = 0; + while ($i < @$directories) { + print $i, ") ", $directories->[$i]->{name}, " (", $directories->[$i]->{url}, ")\n"; + $i++; + } + print $i, ") Custom\n"; + + my $term = Term::ReadLine->new('pmgconfig'); + my $get_dir_selection = sub { + my $selection = $term->readline("Enter selection: "); + if ($selection =~ /^(\d+)$/) { + $selection = $1; + if ($selection == $i) { + $param->{directory} = $term->readline("Enter custom URL: "); + return; + } elsif ($selection < $i && $selection >= 0) { + $param->{directory} = $directories->[$selection]->{url}; + return; + } + } + print "Invalid selection.\n"; + }; + + my $attempts = 0; + while (!$param->{directory}) { + die "Aborting.\n" if $attempts > 3; + $get_dir_selection->(); + $attempts++; + } + } + print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n"; + my $tos = PMG::API2::ACME->get_tos({ directory => $param->{directory} }); + if ($tos) { + print "Terms of Service: $tos\n"; + my $term = Term::ReadLine->new('pvenode'); + my $agreed = $term->readline('Do you agree to the above terms? [y|N]: '); + die "Cannot continue without agreeing to ToS, aborting.\n" + if ($agreed !~ /^y$/i); + + $param->{tos_url} = $tos; + } else { + print "No Terms of Service found, proceeding.\n"; + } + print "\nAttempting to register account with '$param->{directory}'..\n"; + + $upid_exit->(PMG::API2::ACME->register_account($param)); + }}); + +my $print_cert_info = sub { + my ($schema, $cert, $options) = @_; + + my $order = [qw(filename fingerprint subject issuer notbefore notafter public-key-type public-key-bits san)]; + PVE::CLIFormatter::print_api_result( + $cert, $schema, $order, { %$options, noheader => 1, sort_key => 0 }); +}; + our $cmddef = { 'dump' => [ __PACKAGE__, 'dump', []], sync => [ __PACKAGE__, 'sync', []], @@ -198,6 +317,65 @@ our $cmddef = { die "no dkim_selector configured\n" if !defined($res->{record}); print "$res->{record}\n"; }], + + cert => { + info => [ 'PMG::API2::Certificates', 'info', [], { node => $nodename }, sub { + my ($res, $schema, $options) = @_; + + if (!$options->{'output-format'} || $options->{'output-format'} eq 'text') { + for my $cert (sort { $a->{filename} cmp $b->{filename} } @$res) { + $print_cert_info->($schema->{items}, $cert, $options); + } + } else { + PVE::CLIFormatter::print_api_result($res, $schema, undef, $options); + } + + }, $PVE::RESTHandler::standard_output_options], + set => [ 'PMG::API2::Certificates', 'upload_custom_cert', ['type', 'certificates', 'key'], { node => $nodename }, sub { + my ($res, $schema, $options) = @_; + $print_cert_info->($schema, $res, $options); + }, $PVE::RESTHandler::standard_output_options], + delete => [ 'PMG::API2::Certificates', 'remove_custom_cert', ['type', 'restart'], { node => $nodename } ], + }, + + acme => { + account => { + list => [ 'PMG::API2::ACME', 'account_index', [], {}, sub { + my ($res) = @_; + for my $acc (@$res) { + print "$acc->{name}\n"; + } + }], + register => [ __PACKAGE__, 'acme_register', ['name', 'contact'], {}, $upid_exit ], + deactivate => [ 'PMG::API2::ACME', 'deactivate_account', ['name'], {}, $upid_exit ], + info => [ 'PMG::API2::ACME', 'get_account', ['name'], {}, sub { + my ($data, $schema, $options) = @_; + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); + }, $PVE::RESTHandler::standard_output_options], + update => [ 'PMG::API2::ACME', 'update_account', ['name'], {}, $upid_exit ], + }, + cert => { + order => [ 'PMG::API2::Certificates', 'new_acme_cert', ['type'], { node => $nodename }, $upid_exit ], + + + renew => [ 'PMG::API2::Certificates', 'renew_acme_cert', ['type'], { node => $nodename }, $upid_exit ], + revoke => [ 'PMG::API2::Certificates', 'revoke_acme_cert', ['type'], { node => $nodename }, $upid_exit ], + }, + plugin => { + list => [ 'PMG::API2::ACMEPlugin', 'index', [], {}, sub { + my ($data, $schema, $options) = @_; + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); + }, $PVE::RESTHandler::standard_output_options ], + config => [ 'PMG::API2::ACMEPlugin', 'get_plugin_config', ['id'], {}, sub { + my ($data, $schema, $options) = @_; + PVE::CLIFormatter::print_api_result($data, $schema, undef, $options); + }, $PVE::RESTHandler::standard_output_options ], + add => [ 'PMG::API2::ACMEPlugin', 'add_plugin', ['type', 'id'] ], + set => [ 'PMG::API2::ACMEPlugin', 'update_plugin', ['id'] ], + remove => [ 'PMG::API2::ACMEPlugin', 'delete_plugin', ['id'] ], + }, + + }, }; -- 2.20.1