]>
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' },
70 { name
=> 'directories' },
71 { name
=> 'plugins' },
72 { name
=> 'challenge-schema' },
76 __PACKAGE__-
>register_method ({
77 name
=> 'account_index',
80 permissions
=> { check
=> [ 'admin', 'audit' ] },
81 description
=> "ACME account index.",
84 additionalProperties
=> 0,
94 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
99 my $accounts = PMG
::CertHelpers
::list_acme_accounts
();
100 return [ map { { name
=> $_ } } @$accounts ];
103 # extract the optional account name and fill in the default, also return the file name
104 my sub extract_account_name
: prototype($) {
107 my $account_name = extract_param
($param, 'name') // 'default';
108 my $account_file = "${acme_account_dir}/${account_name}";
110 return ($account_name, $account_file);
113 __PACKAGE__-
>register_method ({
114 name
=> 'register_account',
117 description
=> "Register a new ACME account with CA.",
119 permissions
=> { check
=> [ 'admin' ] },
122 additionalProperties
=> 0,
124 name
=> get_standard_option
('pmg-acme-account-name'),
125 contact
=> get_standard_option
('pmg-acme-account-contact'),
128 description
=> 'URL of CA TermsOfService - setting this indicates agreement.',
131 directory
=> get_standard_option
('pmg-acme-directory-url', {
132 default => $acme_default_directory_url,
143 my $rpcenv = PMG
::RESTEnvironment-
>get();
144 my $authuser = $rpcenv->get_user();
146 my ($account_name, $account_file) = extract_account_name
($param);
147 File
::Path
::make_path
($acme_account_dir) if ! -e
$acme_account_dir;
149 raise_param_exc
({'name' => "ACME account config file '${account_name}' already exists."})
152 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
153 my $contact = $account_contact_from_param->($param);
156 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
157 die "ACME account config file '${account_name}' already exists.\n"
160 print "Registering new ACME account..\n";
161 my $acme = PMG
::RS
::Acme-
>new($directory);
163 $acme->new_account($account_file, defined($param->{tos_url
}), $contact, undef);
166 unlink $account_file;
167 die "Registration failed: $err\n";
169 my $location = $acme->location();
170 print "Registration successful, account URL: '$location'\n";
174 return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
177 my $update_account = sub {
178 my ($param, $msg, $force_deactivate, %info) = @_;
180 my ($account_name, $account_file) = extract_account_name
($param);
182 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
183 if ! -e
$account_file;
186 my $rpcenv = PMG
::RESTEnvironment-
>get();
187 my $authuser = $rpcenv->get_user();
190 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
191 die "ACME account config file '${account_name}' does not exist.\n"
192 if ! -e
$account_file;
194 my $acme = PMG
::RS
::Acme-
>load($account_file);
196 $acme->update_account(\
%info);
199 die $err if !$force_deactivate;
200 warn "got error, but forced to continue - $err\n";
202 if ($info{status
} && $info{status
} eq 'deactivated') {
203 my $deactivated_name;
205 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
206 if (! -e
$candidate) {
207 $deactivated_name = $candidate;
211 if ($deactivated_name) {
212 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
213 rename($account_file, $deactivated_name) or
214 warn ".. failed - $!\n";
216 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
222 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
225 __PACKAGE__-
>register_method ({
226 name
=> 'update_account',
227 path
=> 'account/{name}',
229 description
=> "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
231 permissions
=> { check
=> [ 'admin' ] },
234 additionalProperties
=> 0,
236 name
=> get_standard_option
('pmg-acme-account-name'),
237 contact
=> get_standard_option
('pmg-acme-account-contact', {
248 my $contact = $account_contact_from_param->($param);
249 if (scalar @$contact) {
250 return $update_account->($param, 'update', 0, contact
=> $contact);
252 return $update_account->($param, 'refresh', 0);
256 __PACKAGE__-
>register_method ({
257 name
=> 'get_account',
258 path
=> 'account/{name}',
260 description
=> "Return existing ACME account information.",
264 additionalProperties
=> 0,
266 name
=> get_standard_option
('pmg-acme-account-name'),
271 additionalProperties
=> 0,
278 directory
=> get_standard_option
('pmg-acme-directory-url', {
294 my ($account_name, $account_file) = extract_account_name
($param);
296 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
297 if ! -e
$account_file;
299 my $acme = PMG
::RS
::Acme-
>load($account_file);
300 my $data = $acme->account();
303 account
=> $data->{account
},
305 location
=> $data->{location
},
306 directory
=> $data->{directoryUrl
},
310 __PACKAGE__-
>register_method ({
311 name
=> 'deactivate_account',
312 path
=> 'account/{name}',
314 description
=> "Deactivate existing ACME account at CA.",
317 permissions
=> { check
=> [ 'admin' ] },
319 additionalProperties
=> 0,
321 name
=> get_standard_option
('pmg-acme-account-name'),
325 'Delete account data even if the server refuses to deactivate the account.',
337 my $force_deactivate = extract_param
($param, 'force');
339 return $update_account->($param, 'deactivate', $force_deactivate, status
=> 'deactivated');
342 __PACKAGE__-
>register_method ({
346 description
=> "Retrieve ACME TermsOfService URL from CA.",
347 permissions
=> { user
=> 'all' },
349 additionalProperties
=> 0,
351 directory
=> get_standard_option
('pmg-acme-directory-url', {
352 default => $acme_default_directory_url,
360 description
=> 'ACME TermsOfService URL.',
365 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
367 my $acme = PMG
::RS
::Acme-
>new($directory);
368 my $meta = $acme->get_meta();
370 return $meta ?
$meta->{termsOfService
} : undef;
373 __PACKAGE__-
>register_method ({
374 name
=> 'get_directories',
375 path
=> 'directories',
377 description
=> "Get named known ACME directory endpoints.",
378 permissions
=> { user
=> 'all' },
380 additionalProperties
=> 0,
387 additionalProperties
=> 0,
392 url
=> get_standard_option
('pmg-acme-directory-url'),
399 return $acme_directories;
402 __PACKAGE__-
>register_method ({
403 name
=> 'challenge-schema',
404 path
=> 'challenge-schema',
406 description
=> "Get schema of ACME challenge types.",
407 permissions
=> { user
=> 'all' },
409 additionalProperties
=> 0,
416 additionalProperties
=> 0,
422 description
=> 'Human readable name, falls back to id',
437 my $plugin_type_enum = PVE
::ACME
::Challenge-
>lookup_types();
441 for my $type (@$plugin_type_enum) {
442 my $plugin = PVE
::ACME
::Challenge-
>lookup($type);
443 next if !$plugin->can('get_supported_plugins');
445 my $plugin_type = $plugin->type();
446 my $plugins = $plugin->get_supported_plugins();
447 for my $id (sort keys %$plugins) {
448 my $schema = $plugins->{$id};
451 name
=> $schema->{name
} // $id,
452 type
=> $plugin_type,