]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/ACME.pm
1b355111d9fed7d9b51f886c26704da9d7bc2d19
1 package PMG
::API2
::ACME
;
6 use PVE
::Exception
qw(raise_param_exc);
7 use PVE
::JSONSchema
qw(get_standard_option);
8 use PVE
::Tools
qw(extract_param);
10 use PVE
::ACME
::Challenge
;
12 use PMG
::RESTEnvironment
;
16 use PMG
::API2
::ACMEPlugin
;
18 use base
qw(PVE::RESTHandler);
20 __PACKAGE__-
>register_method ({
21 subclass
=> "PMG::API2::ACMEPlugin",
25 # FIXME: Put this list in pve-common or proxmox-acme{,-rs}?
26 my $acme_directories = [
28 name
=> 'Let\'s Encrypt V2',
29 url
=> 'https://acme-v02.api.letsencrypt.org/directory',
32 name
=> 'Let\'s Encrypt V2 Staging',
33 url
=> 'https://acme-staging-v02.api.letsencrypt.org/directory',
36 my $acme_default_directory_url = $acme_directories->[0]->{url
};
37 my $account_contact_from_param = sub {
38 my @addresses = PVE
::Tools
::split_list
(extract_param
($_[0], 'contact'));
39 return [ map { "mailto:$_" } @addresses ];
41 my $acme_account_dir = PMG
::CertHelpers
::acme_account_dir
();
43 __PACKAGE__-
>register_method ({
47 permissions
=> { user
=> 'all' },
48 description
=> "ACME index.",
50 additionalProperties
=> 0,
60 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
66 { name
=> 'account' },
68 { name
=> 'directories' },
69 { name
=> 'plugins' },
70 { name
=> 'challenge-schema' },
74 __PACKAGE__-
>register_method ({
75 name
=> 'account_index',
78 permissions
=> { check
=> [ 'admin', 'audit' ] },
79 description
=> "ACME account index.",
82 additionalProperties
=> 0,
92 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
97 my $accounts = PMG
::CertHelpers
::list_acme_accounts
();
98 return [ map { { name
=> $_ } } @$accounts ];
101 # extract the optional account name and fill in the default, also return the file name
102 my sub extract_account_name
: prototype($) {
105 my $account_name = extract_param
($param, 'name') // 'default';
106 my $account_file = "${acme_account_dir}/${account_name}";
108 return ($account_name, $account_file);
111 __PACKAGE__-
>register_method ({
112 name
=> 'register_account',
115 description
=> "Register a new ACME account with CA.",
117 permissions
=> { check
=> [ 'admin' ] },
120 additionalProperties
=> 0,
122 name
=> get_standard_option
('pmg-acme-account-name'),
123 contact
=> get_standard_option
('pmg-acme-account-contact'),
126 description
=> 'URL of CA TermsOfService - setting this indicates agreement.',
129 directory
=> get_standard_option
('pmg-acme-directory-url', {
130 default => $acme_default_directory_url,
141 my $rpcenv = PMG
::RESTEnvironment-
>get();
142 my $authuser = $rpcenv->get_user();
144 my ($account_name, $account_file) = extract_account_name
($param);
145 mkdir $acme_account_dir if ! -e
$acme_account_dir;
147 raise_param_exc
({'name' => "ACME account config file '${account_name}' already exists."})
150 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
151 my $contact = $account_contact_from_param->($param);
154 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
155 die "ACME account config file '${account_name}' already exists.\n"
158 print "Registering new ACME account..\n";
159 my $acme = PMG
::RS
::Acme-
>new($directory);
161 $acme->new_account($account_file, defined($param->{tos_url
}), $contact, undef);
164 unlink $account_file;
165 die "Registration failed: $err\n";
167 my $location = $acme->location();
168 print "Registration successful, account URL: '$location'\n";
172 return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
175 my $update_account = sub {
176 my ($param, $msg, $force_deactivate, %info) = @_;
178 my ($account_name, $account_file) = extract_account_name
($param);
180 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
181 if ! -e
$account_file;
184 my $rpcenv = PMG
::RESTEnvironment-
>get();
185 my $authuser = $rpcenv->get_user();
188 PMG
::CertHelpers
::lock_acme
($account_name, 10, sub {
189 die "ACME account config file '${account_name}' does not exist.\n"
190 if ! -e
$account_file;
192 my $acme = PMG
::RS
::Acme-
>load($account_file);
194 $acme->update_account(\
%info);
197 die $err if !$force_deactivate;
198 warn "got error, but forced to continue - $err\n";
200 if ($info{status
} && $info{status
} eq 'deactivated') {
201 my $deactivated_name;
203 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
204 if (! -e
$candidate) {
205 $deactivated_name = $candidate;
209 if ($deactivated_name) {
210 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
211 rename($account_file, $deactivated_name) or
212 warn ".. failed - $!\n";
214 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
220 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
223 __PACKAGE__-
>register_method ({
224 name
=> 'update_account',
225 path
=> 'account/{name}',
227 description
=> "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
229 permissions
=> { check
=> [ 'admin' ] },
232 additionalProperties
=> 0,
234 name
=> get_standard_option
('pmg-acme-account-name'),
235 contact
=> get_standard_option
('pmg-acme-account-contact', {
246 my $contact = $account_contact_from_param->($param);
247 if (scalar @$contact) {
248 return $update_account->($param, 'update', 0, contact
=> $contact);
250 return $update_account->($param, 'refresh', 0);
254 __PACKAGE__-
>register_method ({
255 name
=> 'get_account',
256 path
=> 'account/{name}',
258 description
=> "Return existing ACME account information.",
262 additionalProperties
=> 0,
264 name
=> get_standard_option
('pmg-acme-account-name'),
269 additionalProperties
=> 0,
276 directory
=> get_standard_option
('pmg-acme-directory-url', {
292 my ($account_name, $account_file) = extract_account_name
($param);
294 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
295 if ! -e
$account_file;
297 my $acme = PMG
::RS
::Acme-
>load($account_file);
298 my $data = $acme->account();
301 account
=> $data->{account
},
303 location
=> $data->{location
},
304 directory
=> $data->{directoryUrl
},
308 __PACKAGE__-
>register_method ({
309 name
=> 'deactivate_account',
310 path
=> 'account/{name}',
312 description
=> "Deactivate existing ACME account at CA.",
315 permissions
=> { check
=> [ 'admin' ] },
317 additionalProperties
=> 0,
319 name
=> get_standard_option
('pmg-acme-account-name'),
323 'Delete account data even if the server refuses to deactivate the account.',
335 my $force_deactivate = extract_param
($param, 'force');
337 return $update_account->($param, 'deactivate', $force_deactivate, status
=> 'deactivated');
340 __PACKAGE__-
>register_method ({
344 description
=> "Retrieve ACME TermsOfService URL from CA.",
345 permissions
=> { user
=> 'all' },
347 additionalProperties
=> 0,
349 directory
=> get_standard_option
('pmg-acme-directory-url', {
350 default => $acme_default_directory_url,
358 description
=> 'ACME TermsOfService URL.',
363 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
365 my $acme = PMG
::RS
::Acme-
>new($directory);
366 my $meta = $acme->get_meta();
368 return $meta ?
$meta->{termsOfService
} : undef;
371 __PACKAGE__-
>register_method ({
372 name
=> 'get_directories',
373 path
=> 'directories',
375 description
=> "Get named known ACME directory endpoints.",
376 permissions
=> { user
=> 'all' },
378 additionalProperties
=> 0,
385 additionalProperties
=> 0,
390 url
=> get_standard_option
('pmg-acme-directory-url'),
397 return $acme_directories;
400 __PACKAGE__-
>register_method ({
401 name
=> 'challenge-schema',
402 path
=> 'challenge-schema',
404 description
=> "Get schema of ACME challenge types.",
405 permissions
=> { user
=> 'all' },
407 additionalProperties
=> 0,
414 additionalProperties
=> 0,
420 description
=> 'Human readable name, falls back to id',
435 my $plugin_type_enum = PVE
::ACME
::Challenge-
>lookup_types();
439 for my $type (@$plugin_type_enum) {
440 my $plugin = PVE
::ACME
::Challenge-
>lookup($type);
441 next if !$plugin->can('get_supported_plugins');
443 my $plugin_type = $plugin->type();
444 my $plugins = $plugin->get_supported_plugins();
445 for my $id (sort keys %$plugins) {
446 my $schema = $plugins->{$id};
449 name
=> $schema->{name
} // $id,
450 type
=> $plugin_type,