]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/ACME.pm
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, %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);
193 $acme->update_account(\
%info);
194 if ($info{status
} && $info{status
} eq 'deactivated') {
195 my $deactivated_name;
197 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
198 if (! -e
$candidate) {
199 $deactivated_name = $candidate;
203 if ($deactivated_name) {
204 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
205 rename($account_file, $deactivated_name) or
206 warn ".. failed - $!\n";
208 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
214 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
217 __PACKAGE__-
>register_method ({
218 name
=> 'update_account',
219 path
=> 'account/{name}',
221 description
=> "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
223 permissions
=> { check
=> [ 'admin' ] },
226 additionalProperties
=> 0,
228 name
=> get_standard_option
('pmg-acme-account-name'),
229 contact
=> get_standard_option
('pmg-acme-account-contact', {
240 my $contact = $account_contact_from_param->($param);
241 if (scalar @$contact) {
242 return $update_account->($param, 'update', contact
=> $contact);
244 return $update_account->($param, 'refresh');
248 __PACKAGE__-
>register_method ({
249 name
=> 'get_account',
250 path
=> 'account/{name}',
252 description
=> "Return existing ACME account information.",
256 additionalProperties
=> 0,
258 name
=> get_standard_option
('pmg-acme-account-name'),
263 additionalProperties
=> 0,
270 directory
=> get_standard_option
('pmg-acme-directory-url', {
286 my ($account_name, $account_file) = extract_account_name
($param);
288 raise_param_exc
({'name' => "ACME account config file '${account_name}' does not exist."})
289 if ! -e
$account_file;
291 my $acme = PMG
::RS
::Acme-
>load($account_file);
292 my $data = $acme->account();
295 account
=> $data->{account
},
297 location
=> $data->{location
},
298 directory
=> $data->{directoryUrl
},
302 __PACKAGE__-
>register_method ({
303 name
=> 'deactivate_account',
304 path
=> 'account/{name}',
306 description
=> "Deactivate existing ACME account at CA.",
309 permissions
=> { check
=> [ 'admin' ] },
311 additionalProperties
=> 0,
313 name
=> get_standard_option
('pmg-acme-account-name'),
322 return $update_account->($param, 'deactivate', status
=> 'deactivated');
325 __PACKAGE__-
>register_method ({
329 description
=> "Retrieve ACME TermsOfService URL from CA.",
330 permissions
=> { user
=> 'all' },
332 additionalProperties
=> 0,
334 directory
=> get_standard_option
('pmg-acme-directory-url', {
335 default => $acme_default_directory_url,
343 description
=> 'ACME TermsOfService URL.',
348 my $directory = extract_param
($param, 'directory') // $acme_default_directory_url;
350 my $acme = PMG
::RS
::Acme-
>new($directory);
351 my $meta = $acme->get_meta();
353 return $meta ?
$meta->{termsOfService
} : undef;
356 __PACKAGE__-
>register_method ({
357 name
=> 'get_directories',
358 path
=> 'directories',
360 description
=> "Get named known ACME directory endpoints.",
361 permissions
=> { user
=> 'all' },
363 additionalProperties
=> 0,
370 additionalProperties
=> 0,
375 url
=> get_standard_option
('pmg-acme-directory-url'),
382 return $acme_directories;
385 __PACKAGE__-
>register_method ({
386 name
=> 'challenge-schema',
387 path
=> 'challenge-schema',
389 description
=> "Get schema of ACME challenge types.",
390 permissions
=> { user
=> 'all' },
392 additionalProperties
=> 0,
399 additionalProperties
=> 0,
405 description
=> 'Human readable name, falls back to id',
420 my $plugin_type_enum = PVE
::ACME
::Challenge-
>lookup_types();
424 for my $type (@$plugin_type_enum) {
425 my $plugin = PVE
::ACME
::Challenge-
>lookup($type);
426 next if !$plugin->can('get_supported_plugins');
428 my $plugin_type = $plugin->type();
429 my $plugins = $plugin->get_supported_plugins();
430 for my $id (sort keys %$plugins) {
431 my $schema = $plugins->{$id};
434 name
=> $schema->{name
} // $id,
435 type
=> $plugin_type,