]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/ACME.pm
9e3eb8d5e14057344acba7bf2705380628d85559
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,
137 description
=> 'Key Identifier for External Account Binding.',
138 requires
=> 'eab-hmac-key',
143 description
=> 'HMAC key for External Account Binding.',
144 requires
=> 'eab-kid',
155 my $rpcenv = PMG
::RESTEnvironment-
>get();
156 my $authuser = $rpcenv->get_user();
158 my ($account_name, $account_file) = extract_account_name
($param);
159 mkpath
$acme_account_dir if ! -e
$acme_account_dir;
161 raise_param_exc
({'name' => "ACME account config file '${account_name}' already exists."})
164 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
165 my $contact = $account_contact_from_param->($param);
166 my $eab_kid = extract_param
($param, 'eab-kid');
167 my $eab_hmac_key = extract_param
($param, 'eab-hmac-key');
170 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
171 die "ACME account config file '${account_name}' already exists.\n"
174 print "Registering new ACME account..\n";
175 my $acme = PMG
::RS
::Acme-
>new($directory);
177 $acme->new_account($account_file, defined($param->{tos_url
}), $contact, undef, $eab_kid, $eab_hmac_key);
180 unlink $account_file;
181 die "Registration failed: $err\n";
183 my $location = $acme->location();
184 print "Registration successful, account URL: '$location'\n";
188 return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
191 my $update_account = sub {
192 my ($param, $msg, $force_deactivate, %info) = @_;
194 my ($account_name, $account_file) = extract_account_name
($param);
196 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
197 if ! -e
$account_file;
200 my $rpcenv = PMG
::RESTEnvironment-
>get();
201 my $authuser = $rpcenv->get_user();
204 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
205 die "ACME account config file '${account_name}' does not exist.\n"
206 if ! -e
$account_file;
208 my $acme = PMG
::RS
::Acme-
>load($account_file);
210 $acme->update_account(\
%info);
213 die $err if !$force_deactivate;
214 warn "got error, but forced to continue - $err\n";
216 if ($info{status
} && $info{status
} eq 'deactivated') {
217 my $deactivated_name;
219 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
220 if (! -e
$candidate) {
221 $deactivated_name = $candidate;
225 if ($deactivated_name) {
226 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
227 rename($account_file, $deactivated_name) or
228 warn ".. failed - $!\n";
230 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
236 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
239 __PACKAGE__-
>register_method ({
240 name
=> 'update_account',
241 path
=> 'account/{name}',
243 description
=> "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
245 permissions
=> { check
=> [ 'admin' ] },
248 additionalProperties
=> 0,
250 name
=> get_standard_option
('pmg-acme-account-name'),
251 contact
=> get_standard_option
('pmg-acme-account-contact', {
262 my $contact = $account_contact_from_param->($param);
263 if (scalar @$contact) {
264 return $update_account->($param, 'update', 0, contact
=> $contact);
266 return $update_account->($param, 'refresh', 0);
270 __PACKAGE__-
>register_method ({
271 name
=> 'get_account',
272 path
=> 'account/{name}',
274 description
=> "Return existing ACME account information.",
278 additionalProperties
=> 0,
280 name
=> get_standard_option
('pmg-acme-account-name'),
285 additionalProperties
=> 0,
292 directory
=> get_standard_option
('pmg-acme-directory-url', {
308 my ($account_name, $account_file) = extract_account_name
($param);
310 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
311 if ! -e
$account_file;
313 my $acme = PMG
::RS
::Acme-
>load($account_file);
314 my $data = $acme->account();
317 account
=> $data->{account
},
319 location
=> $data->{location
},
320 directory
=> $data->{directoryUrl
},
324 __PACKAGE__-
>register_method ({
325 name
=> 'deactivate_account',
326 path
=> 'account/{name}',
328 description
=> "Deactivate existing ACME account at CA.",
331 permissions
=> { check
=> [ 'admin' ] },
333 additionalProperties
=> 0,
335 name
=> get_standard_option
('pmg-acme-account-name'),
339 'Delete account data even if the server refuses to deactivate the account.',
351 my $force_deactivate = extract_param
($param, 'force');
353 return $update_account->($param, 'deactivate', $force_deactivate, status
=> 'deactivated');
356 __PACKAGE__-
>register_method ({
360 description
=> "Retrieve ACME TermsOfService URL from CA.",
361 permissions
=> { user
=> 'all' },
363 additionalProperties
=> 0,
365 directory
=> get_standard_option
('pmg-acme-directory-url', {
366 default => $acme_default_directory_url,
374 description
=> 'ACME TermsOfService URL.',
379 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
381 my $acme = PMG
::RS
::Acme-
>new($directory);
382 my $meta = $acme->get_meta();
384 return $meta ?
$meta->{termsOfService
} : undef;
387 __PACKAGE__-
>register_method ({
388 name
=> 'get_directories',
389 path
=> 'directories',
391 description
=> "Get named known ACME directory endpoints.",
392 permissions
=> { user
=> 'all' },
394 additionalProperties
=> 0,
401 additionalProperties
=> 0,
406 url
=> get_standard_option
('pmg-acme-directory-url'),
413 return $acme_directories;
416 __PACKAGE__-
>register_method ({
417 name
=> 'challenge-schema',
418 path
=> 'challenge-schema',
420 description
=> "Get schema of ACME challenge types.",
421 permissions
=> { user
=> 'all' },
423 additionalProperties
=> 0,
430 additionalProperties
=> 0,
436 description
=> 'Human readable name, falls back to id',
451 my $plugin_type_enum = PVE
::ACME
::Challenge-
>lookup_types();
455 for my $type (@$plugin_type_enum) {
456 my $plugin = PVE
::ACME
::Challenge-
>lookup($type);
457 next if !$plugin->can('get_supported_plugins');
459 my $plugin_type = $plugin->type();
460 my $plugins = $plugin->get_supported_plugins();
461 for my $id (sort keys %$plugins) {
462 my $schema = $plugins->{$id};
465 name
=> $schema->{name
} // $id,
466 type
=> $plugin_type,