]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/ACME.pm
1 package PMG
::API2
::ACME
;
8 use PVE
::Exception
qw(raise_param_exc);
9 use PVE
::JSONSchema
qw(get_standard_option);
10 use PVE
::Tools
qw(extract_param);
12 use PVE
::ACME
::Challenge
;
14 use PMG
::RESTEnvironment
;
18 use PMG
::API2
::ACMEPlugin
;
20 use base
qw(PVE::RESTHandler);
22 __PACKAGE__-
>register_method ({
23 subclass
=> "PMG::API2::ACMEPlugin",
27 # FIXME: Put this list in pve-common or proxmox-acme{,-rs}?
28 my $acme_directories = [
30 name
=> 'Let\'s Encrypt V2',
31 url
=> 'https://acme-v02.api.letsencrypt.org/directory',
34 name
=> 'Let\'s Encrypt V2 Staging',
35 url
=> 'https://acme-staging-v02.api.letsencrypt.org/directory',
38 my $acme_default_directory_url = $acme_directories->[0]->{url
};
39 my $account_contact_from_param = sub {
40 my @addresses = PVE
::Tools
::split_list
(extract_param
($_[0], 'contact'));
41 return [ map { "mailto:$_" } @addresses ];
43 my $acme_account_dir = PMG
::CertHelpers
::acme_account_dir
();
45 __PACKAGE__-
>register_method ({
49 permissions
=> { user
=> 'all' },
50 description
=> "ACME index.",
52 additionalProperties
=> 0,
62 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
68 { name
=> 'account' },
71 { name
=> 'directories' },
72 { name
=> 'plugins' },
73 { name
=> 'challenge-schema' },
77 __PACKAGE__-
>register_method ({
78 name
=> 'account_index',
81 permissions
=> { check
=> [ 'admin', 'audit' ] },
82 description
=> "ACME account index.",
85 additionalProperties
=> 0,
95 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
100 my $accounts = PMG
::CertHelpers
::list_acme_accounts
();
101 return [ map { { name
=> $_ } } @$accounts ];
104 # extract the optional account name and fill in the default, also return the file name
105 my sub extract_account_name
: prototype($) {
108 my $account_name = extract_param
($param, 'name') // 'default';
109 my $account_file = "${acme_account_dir}/${account_name}";
111 return ($account_name, $account_file);
114 __PACKAGE__-
>register_method ({
115 name
=> 'register_account',
118 description
=> "Register a new ACME account with CA.",
120 permissions
=> { check
=> [ 'admin' ] },
123 additionalProperties
=> 0,
125 name
=> get_standard_option
('pmg-acme-account-name'),
126 contact
=> get_standard_option
('pmg-acme-account-contact'),
129 description
=> 'URL of CA TermsOfService - setting this indicates agreement.',
132 directory
=> get_standard_option
('pmg-acme-directory-url', {
133 default => $acme_default_directory_url,
138 description
=> 'Key Identifier for External Account Binding.',
139 requires
=> 'eab-hmac-key',
144 description
=> 'HMAC key for External Account Binding.',
145 requires
=> 'eab-kid',
156 my $rpcenv = PMG
::RESTEnvironment-
>get();
157 my $authuser = $rpcenv->get_user();
159 my ($account_name, $account_file) = extract_account_name
($param);
160 mkpath
$acme_account_dir if ! -e
$acme_account_dir;
162 raise_param_exc
({'name' => "ACME account config file '${account_name}' already exists."})
165 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
166 my $contact = $account_contact_from_param->($param);
167 my $eab_kid = extract_param
($param, 'eab-kid');
168 my $eab_hmac_key = extract_param
($param, 'eab-hmac-key');
171 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
172 die "ACME account config file '${account_name}' already exists.\n"
175 print "Registering new ACME account..\n";
176 my $acme = PMG
::RS
::Acme-
>new($directory);
178 $acme->new_account($account_file, defined($param->{tos_url
}), $contact, undef, $eab_kid, $eab_hmac_key);
181 unlink $account_file;
182 die "Registration failed: $err\n";
184 my $location = $acme->location();
185 print "Registration successful, account URL: '$location'\n";
189 return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
192 my $update_account = sub {
193 my ($param, $msg, $force_deactivate, %info) = @_;
195 my ($account_name, $account_file) = extract_account_name
($param);
197 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
198 if ! -e
$account_file;
201 my $rpcenv = PMG
::RESTEnvironment-
>get();
202 my $authuser = $rpcenv->get_user();
205 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
206 die "ACME account config file '${account_name}' does not exist.\n"
207 if ! -e
$account_file;
209 my $acme = PMG
::RS
::Acme-
>load($account_file);
211 $acme->update_account(\
%info);
214 die $err if !$force_deactivate;
215 warn "got error, but forced to continue - $err\n";
217 if ($info{status
} && $info{status
} eq 'deactivated') {
218 my $deactivated_name;
220 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
221 if (! -e
$candidate) {
222 $deactivated_name = $candidate;
226 if ($deactivated_name) {
227 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
228 rename($account_file, $deactivated_name) or
229 warn ".. failed - $!\n";
231 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
237 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
240 __PACKAGE__-
>register_method ({
241 name
=> 'update_account',
242 path
=> 'account/{name}',
244 description
=> "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
246 permissions
=> { check
=> [ 'admin' ] },
249 additionalProperties
=> 0,
251 name
=> get_standard_option
('pmg-acme-account-name'),
252 contact
=> get_standard_option
('pmg-acme-account-contact', {
263 my $contact = $account_contact_from_param->($param);
264 if (scalar @$contact) {
265 return $update_account->($param, 'update', 0, contact
=> $contact);
267 return $update_account->($param, 'refresh', 0);
271 __PACKAGE__-
>register_method ({
272 name
=> 'get_account',
273 path
=> 'account/{name}',
275 description
=> "Return existing ACME account information.",
279 additionalProperties
=> 0,
281 name
=> get_standard_option
('pmg-acme-account-name'),
286 additionalProperties
=> 0,
293 directory
=> get_standard_option
('pmg-acme-directory-url', {
309 my ($account_name, $account_file) = extract_account_name
($param);
311 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
312 if ! -e
$account_file;
314 my $acme = PMG
::RS
::Acme-
>load($account_file);
315 my $data = $acme->account();
318 account
=> $data->{account
},
320 location
=> $data->{location
},
321 directory
=> $data->{directoryUrl
},
325 __PACKAGE__-
>register_method ({
326 name
=> 'deactivate_account',
327 path
=> 'account/{name}',
329 description
=> "Deactivate existing ACME account at CA.",
332 permissions
=> { check
=> [ 'admin' ] },
334 additionalProperties
=> 0,
336 name
=> get_standard_option
('pmg-acme-account-name'),
340 'Delete account data even if the server refuses to deactivate the account.',
352 my $force_deactivate = extract_param
($param, 'force');
354 return $update_account->($param, 'deactivate', $force_deactivate, status
=> 'deactivated');
357 # TODO: deprecated, remove with pmg 9
358 __PACKAGE__-
>register_method ({
362 description
=> "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /config/acme/meta.",
363 permissions
=> { user
=> 'all' },
365 additionalProperties
=> 0,
367 directory
=> get_standard_option
('pmg-acme-directory-url', {
368 default => $acme_default_directory_url,
376 description
=> 'ACME TermsOfService URL.',
381 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
383 my $acme = PMG
::RS
::Acme-
>new($directory);
384 my $meta = $acme->get_meta();
386 return $meta ?
$meta->{termsOfService
} : undef;
389 __PACKAGE__-
>register_method ({
393 description
=> "Retrieve ACME Directory Meta Information",
394 permissions
=> { user
=> 'all' },
396 additionalProperties
=> 0,
398 directory
=> get_standard_option
('pmg-acme-directory-url', {
399 default => $acme_default_directory_url,
406 additionalProperties
=> 1,
409 description
=> 'ACME TermsOfService URL.',
413 externalAccountRequired
=> {
414 description
=> 'EAB Required',
419 description
=> 'URL to more information about the ACME server.',
424 description
=> 'Hostnames referring to the ACME servers.',
436 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
438 my $acme = PVE
::ACME-
>new(undef, $directory);
439 my $meta = $acme->get_meta();
444 __PACKAGE__-
>register_method ({
445 name
=> 'get_directories',
446 path
=> 'directories',
448 description
=> "Get named known ACME directory endpoints.",
449 permissions
=> { user
=> 'all' },
451 additionalProperties
=> 0,
458 additionalProperties
=> 0,
463 url
=> get_standard_option
('pmg-acme-directory-url'),
470 return $acme_directories;
473 __PACKAGE__-
>register_method ({
474 name
=> 'challenge-schema',
475 path
=> 'challenge-schema',
477 description
=> "Get schema of ACME challenge types.",
478 permissions
=> { user
=> 'all' },
480 additionalProperties
=> 0,
487 additionalProperties
=> 0,
493 description
=> 'Human readable name, falls back to id',
508 my $plugin_type_enum = PVE
::ACME
::Challenge-
>lookup_types();
512 for my $type (@$plugin_type_enum) {
513 my $plugin = PVE
::ACME
::Challenge-
>lookup($type);
514 next if !$plugin->can('get_supported_plugins');
516 my $plugin_type = $plugin->type();
517 my $plugins = $plugin->get_supported_plugins();
518 for my $id (sort keys %$plugins) {
519 my $schema = $plugins->{$id};
522 name
=> $schema->{name
} // $id,
523 type
=> $plugin_type,