]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/ACMEAccount.pm
api2: network: improve code readability
[pve-manager.git] / PVE / API2 / ACMEAccount.pm
1 package PVE::API2::ACMEAccount;
2
3 use strict;
4 use warnings;
5
6 use PVE::ACME;
7 use PVE::CertHelpers;
8 use PVE::Exception qw(raise_param_exc);
9 use PVE::JSONSchema qw(get_standard_option);
10 use PVE::RPCEnvironment;
11 use PVE::Tools qw(extract_param);
12 use PVE::ACME::Challenge;
13
14 use PVE::API2::ACMEPlugin;
15
16 use base qw(PVE::RESTHandler);
17
18 __PACKAGE__->register_method ({
19 subclass => "PVE::API2::ACMEPlugin",
20 path => 'plugins',
21 });
22
23 my $acme_directories = [
24 {
25 name => 'Let\'s Encrypt V2',
26 url => 'https://acme-v02.api.letsencrypt.org/directory',
27 },
28 {
29 name => 'Let\'s Encrypt V2 Staging',
30 url => 'https://acme-staging-v02.api.letsencrypt.org/directory',
31 },
32 ];
33 my $acme_default_directory_url = $acme_directories->[0]->{url};
34 my $account_contact_from_param = sub {
35 my @addresses = PVE::Tools::split_list(extract_param($_[0], 'contact'));
36 return [ map { "mailto:$_" } @addresses ];
37 };
38 my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
39
40 __PACKAGE__->register_method ({
41 name => 'index',
42 path => '',
43 method => 'GET',
44 permissions => { user => 'all' },
45 description => "ACMEAccount index.",
46 parameters => {
47 additionalProperties => 0,
48 properties => {
49 },
50 },
51 returns => {
52 type => 'array',
53 items => {
54 type => "object",
55 properties => {},
56 },
57 links => [ { rel => 'child', href => "{name}" } ],
58 },
59 code => sub {
60 my ($param) = @_;
61
62 return [
63 { name => 'account' },
64 { name => 'tos' },
65 { name => 'directories' },
66 { name => 'plugins' },
67 { name => 'challenge-schema' },
68 ];
69 }});
70
71 __PACKAGE__->register_method ({
72 name => 'account_index',
73 path => 'account',
74 method => 'GET',
75 permissions => { user => 'all' },
76 description => "ACMEAccount index.",
77 protected => 1,
78 parameters => {
79 additionalProperties => 0,
80 properties => {
81 },
82 },
83 returns => {
84 type => 'array',
85 items => {
86 type => "object",
87 properties => {},
88 },
89 links => [ { rel => 'child', href => "{name}" } ],
90 },
91 code => sub {
92 my ($param) = @_;
93
94 my $accounts = PVE::CertHelpers::list_acme_accounts();
95 return [ map { { name => $_ } } @$accounts ];
96 }});
97
98 __PACKAGE__->register_method ({
99 name => 'register_account',
100 path => 'account',
101 method => 'POST',
102 description => "Register a new ACME account with CA.",
103 protected => 1,
104 parameters => {
105 additionalProperties => 0,
106 properties => {
107 name => get_standard_option('pve-acme-account-name'),
108 contact => get_standard_option('pve-acme-account-contact'),
109 tos_url => {
110 type => 'string',
111 description => 'URL of CA TermsOfService - setting this indicates agreement.',
112 optional => 1,
113 },
114 directory => get_standard_option('pve-acme-directory-url', {
115 default => $acme_default_directory_url,
116 optional => 1,
117 }),
118 },
119 },
120 returns => {
121 type => 'string',
122 },
123 code => sub {
124 my ($param) = @_;
125
126 my $rpcenv = PVE::RPCEnvironment::get();
127 my $authuser = $rpcenv->get_user();
128
129 my $account_name = extract_param($param, 'name') // 'default';
130 my $account_file = "${acme_account_dir}/${account_name}";
131 mkdir $acme_account_dir if ! -e $acme_account_dir;
132
133 raise_param_exc({'name' => "ACME account config file '${account_name}' already exists."})
134 if -e $account_file;
135
136 my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
137 my $contact = $account_contact_from_param->($param);
138
139 my $realcmd = sub {
140 PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
141 die "ACME account config file '${account_name}' already exists.\n"
142 if -e $account_file;
143
144 my $acme = PVE::ACME->new($account_file, $directory);
145 print "Generating ACME account key..\n";
146 $acme->init(4096);
147 print "Registering ACME account..\n";
148 eval { $acme->new_account($param->{tos_url}, contact => $contact); };
149 if (my $err = $@) {
150 unlink $account_file;
151 die "Registration failed: $err\n";
152 }
153 print "Registration successful, account URL: '$acme->{location}'\n";
154 });
155 die $@ if $@;
156 };
157
158 return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
159 }});
160
161 my $update_account = sub {
162 my ($param, $msg, %info) = @_;
163
164 my $account_name = extract_param($param, 'name') // 'default';
165 my $account_file = "${acme_account_dir}/${account_name}";
166
167 raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
168 if ! -e $account_file;
169
170
171 my $rpcenv = PVE::RPCEnvironment::get();
172 my $authuser = $rpcenv->get_user();
173
174 my $realcmd = sub {
175 PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
176 die "ACME account config file '${account_name}' does not exist.\n"
177 if ! -e $account_file;
178
179 my $acme = PVE::ACME->new($account_file);
180 $acme->load();
181 $acme->update_account(%info);
182 if ($info{status} && $info{status} eq 'deactivated') {
183 my $deactivated_name;
184 for my $i (0..100) {
185 my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
186 if (! -e $candidate) {
187 $deactivated_name = $candidate;
188 last;
189 }
190 }
191 if ($deactivated_name) {
192 print "Renaming account file from '$account_file' to '$deactivated_name'\n";
193 rename($account_file, $deactivated_name) or
194 warn ".. failed - $!\n";
195 } else {
196 warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
197 }
198 }
199 });
200 die $@ if $@;
201 };
202
203 return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
204 };
205
206 __PACKAGE__->register_method ({
207 name => 'update_account',
208 path => 'account/{name}',
209 method => 'PUT',
210 description => "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
211 protected => 1,
212 parameters => {
213 additionalProperties => 0,
214 properties => {
215 name => get_standard_option('pve-acme-account-name'),
216 contact => get_standard_option('pve-acme-account-contact', {
217 optional => 1,
218 }),
219 },
220 },
221 returns => {
222 type => 'string',
223 },
224 code => sub {
225 my ($param) = @_;
226
227 my $contact = $account_contact_from_param->($param);
228 if (scalar @$contact) {
229 return $update_account->($param, 'update', contact => $contact);
230 } else {
231 return $update_account->($param, 'refresh');
232 }
233 }});
234
235 __PACKAGE__->register_method ({
236 name => 'get_account',
237 path => 'account/{name}',
238 method => 'GET',
239 description => "Return existing ACME account information.",
240 protected => 1,
241 parameters => {
242 additionalProperties => 0,
243 properties => {
244 name => get_standard_option('pve-acme-account-name'),
245 },
246 },
247 returns => {
248 type => 'object',
249 additionalProperties => 0,
250 properties => {
251 account => {
252 type => 'object',
253 optional => 1,
254 renderer => 'yaml',
255 },
256 directory => get_standard_option('pve-acme-directory-url', {
257 optional => 1,
258 }),
259 location => {
260 type => 'string',
261 optional => 1,
262 },
263 tos => {
264 type => 'string',
265 optional => 1,
266 },
267 },
268 },
269 code => sub {
270 my ($param) = @_;
271
272 my $account_name = extract_param($param, 'name') // 'default';
273 my $account_file = "${acme_account_dir}/${account_name}";
274
275 raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
276 if ! -e $account_file;
277
278 my $acme = PVE::ACME->new($account_file);
279 $acme->load();
280
281 my $res = {};
282 $res->{account} = $acme->{account};
283 $res->{directory} = $acme->{directory};
284 $res->{location} = $acme->{location};
285 $res->{tos} = $acme->{tos};
286
287 return $res;
288 }});
289
290 __PACKAGE__->register_method ({
291 name => 'deactivate_account',
292 path => 'account/{name}',
293 method => 'DELETE',
294 description => "Deactivate existing ACME account at CA.",
295 protected => 1,
296 parameters => {
297 additionalProperties => 0,
298 properties => {
299 name => get_standard_option('pve-acme-account-name'),
300 },
301 },
302 returns => {
303 type => 'string',
304 },
305 code => sub {
306 my ($param) = @_;
307
308 return $update_account->($param, 'deactivate', status => 'deactivated');
309 }});
310
311 __PACKAGE__->register_method ({
312 name => 'get_tos',
313 path => 'tos',
314 method => 'GET',
315 description => "Retrieve ACME TermsOfService URL from CA.",
316 permissions => { user => 'all' },
317 parameters => {
318 additionalProperties => 0,
319 properties => {
320 directory => get_standard_option('pve-acme-directory-url', {
321 default => $acme_default_directory_url,
322 optional => 1,
323 }),
324 },
325 },
326 returns => {
327 type => 'string',
328 optional => 1,
329 description => 'ACME TermsOfService URL.',
330 },
331 code => sub {
332 my ($param) = @_;
333
334 my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
335
336 my $acme = PVE::ACME->new(undef, $directory);
337 my $meta = $acme->get_meta();
338
339 return $meta ? $meta->{termsOfService} : undef;
340 }});
341
342 __PACKAGE__->register_method ({
343 name => 'get_directories',
344 path => 'directories',
345 method => 'GET',
346 description => "Get named known ACME directory endpoints.",
347 permissions => { user => 'all' },
348 parameters => {
349 additionalProperties => 0,
350 properties => {},
351 },
352 returns => {
353 type => 'array',
354 items => {
355 type => 'object',
356 additionalProperties => 0,
357 properties => {
358 name => {
359 type => 'string',
360 },
361 url => get_standard_option('pve-acme-directory-url'),
362 },
363 },
364 },
365 code => sub {
366 my ($param) = @_;
367
368 return $acme_directories;
369 }});
370
371 __PACKAGE__->register_method ({
372 name => 'challengeschema',
373 path => 'challenge-schema',
374 method => 'GET',
375 description => "Get schema of ACME challenge types.",
376 permissions => { user => 'all' },
377 parameters => {
378 additionalProperties => 0,
379 properties => {},
380 },
381 returns => {
382 type => 'array',
383 items => {
384 type => 'object',
385 additionalProperties => 0,
386 properties => {
387 id => {
388 type => 'string',
389 },
390 name => {
391 description => 'Human readable name, falls back to id',
392 type => 'string',
393 },
394 type => {
395 type => 'string',
396 },
397 schema => {
398 type => 'object',
399 },
400 },
401 },
402 },
403 code => sub {
404 my ($param) = @_;
405
406 my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();
407
408 my $res = [];
409
410 for my $type (@$plugin_type_enum) {
411 my $plugin = PVE::ACME::Challenge->lookup($type);
412 next if !$plugin->can('get_supported_plugins');
413
414 my $plugin_type = $plugin->type();
415 my $plugins = $plugin->get_supported_plugins();
416 for my $id (sort keys %$plugins) {
417 my $schema = $plugins->{$id};
418 push @$res, {
419 id => $id,
420 name => $schema->{name} // $id,
421 type => $plugin_type,
422 schema => $schema,
423 };
424 }
425 }
426
427 return $res;
428 }});
429
430 1;