]> git.proxmox.com Git - pve-manager.git/blob - PVE/API2/Subscription.pm
do not use Net::SSL
[pve-manager.git] / PVE / API2 / Subscription.pm
1 package PVE::API2::Subscription;
2
3 use strict;
4 use warnings;
5 use Digest::MD5 qw(md5_hex md5_base64);
6 use MIME::Base64;
7 use HTTP::Request;
8 use LWP::UserAgent;
9 use JSON;
10
11 use PVE::Tools;
12 use PVE::ProcFSTools;
13 use PVE::Exception qw(raise_param_exc);
14 use PVE::INotify;
15 use PVE::Cluster qw (cfs_read_file cfs_write_file);
16 use PVE::AccessControl;
17 use PVE::Storage;
18 use PVE::JSONSchema qw(get_standard_option);
19
20 use PVE::SafeSyslog;
21
22 use PVE::API2Tools;
23 use PVE::RESTHandler;
24
25 use base qw(PVE::RESTHandler);
26
27 PVE::INotify::register_file('subscription', "/etc/subscription",
28 \&read_etc_pve_subscription,
29 \&write_etc_pve_subscription);
30
31 # How long the local key is valid for in between remote checks
32 my $localkeydays = 15;
33 # How many days to allow after local key expiry before blocking
34 # access if connection cannot be made
35 my $allowcheckfaildays = 5;
36
37 my $shared_key_data = "kjfdlskfhiuewhfk947368";
38
39 sub get_sockets {
40 my $info = PVE::ProcFSTools::read_cpuinfo();
41 return $info->{sockets};
42 }
43
44 sub parse_key {
45 my ($key) = @_;
46
47 if ($key =~ m/^pve([124])([cbsp])-[0-9a-f]{10}$/) {
48 return wantarray ? ($1, $2) : $1; # number of sockets, level
49 }
50 return undef;
51 }
52
53 my $saved_fields = {
54 key => 1,
55 checktime => 1,
56 status => 1,
57 message => 0,
58 validdirectory => 1,
59 productname => 1,
60 regdate => 1,
61 nextduedate => 1,
62 };
63
64 sub check_fields {
65 my ($info, $server_id, $req_sockets) = @_;
66
67 foreach my $f (qw(status checktime key)) {
68 if (!$info->{$f}) {
69 die "Missing field '$f'\n";
70 }
71 }
72
73 my $sockets = parse_key($info->{key});
74 if (!$sockets) {
75 die "Wrong subscription key format\n";
76 }
77 if ($sockets < $req_sockets) {
78 die "wrong number of sockets ($sockets < $req_sockets)\n";
79 }
80
81 if ($info->{checktime} > time()) {
82 die "Last check time in future.\n";
83 }
84
85 return undef if $info->{status} ne 'Active';
86
87 foreach my $f (keys %$saved_fields) {
88 next if !$saved_fields->{$f};
89 if (!$info->{$f}) {
90 die "Missing field '$f'\n";
91 }
92 }
93
94 my $found;
95 foreach my $hwid (split(/,/, $info->{validdirectory})) {
96 if ($hwid eq $server_id) {
97 $found = 1;
98 last;
99 }
100 }
101 die "Server ID does not match\n" if !$found;
102
103 return undef;
104 }
105
106 sub read_etc_pve_subscription {
107 my ($filename, $fh) = @_;
108
109 my $info = { status => 'Invalid' };
110
111 my $key = <$fh>; # first line is the key
112 chomp $key;
113 my ($sockets, $level) = parse_key($key);
114 die "Wrong subscription key format\n" if !$sockets;
115
116 my $csum = <$fh>; # second line is a checksum
117
118 $info->{key} = $key;
119
120 my $data = '';
121 while (defined(my $line = <$fh>)) {
122 $data .= $line;
123 }
124
125 if ($csum && $data) {
126
127 chomp $csum;
128
129 my $localinfo = {};
130
131 eval {
132 my $json_text = decode_base64($data);
133 $localinfo = decode_json($json_text);
134 my $newcsum = md5_base64($localinfo->{checktime} . $data . $shared_key_data);
135 die "checksum failure\n" if $csum ne $newcsum;
136
137 my $req_sockets = get_sockets();
138 my $server_id = PVE::API2Tools::get_hwaddress();
139
140 check_fields($localinfo, $server_id, $req_sockets);
141
142 my $age = time() - $localinfo->{checktime};
143
144 my $maxage = ($localkeydays + $allowcheckfaildays)*60*60*24;
145 if ($localinfo->{status} eq 'Active' && $age > $maxage) {
146 $localinfo->{status} = 'Invalid';
147 $localinfo->{message} = "subscription info too old";
148 }
149 };
150 if (my $err = $@) {
151 warn $err;
152 } else {
153 $info = $localinfo;
154 }
155 }
156
157 if ($info->{status} eq 'Active') {
158 $info->{level} = $level;
159 }
160
161 return $info;
162 }
163
164 sub write_apt_auth {
165 my $key = shift;
166
167 my $server_id = PVE::API2Tools::get_hwaddress();
168 my $auth = { 'enterprise.proxmox.com' => { login => $key, password => $server_id } };
169 PVE::INotify::update_file('apt-auth', $auth);
170
171 }
172
173 sub write_etc_pve_subscription {
174 my ($filename, $fh, $info) = @_;
175
176 if ($info->{status} eq 'New') {
177 PVE::Tools::safe_print($filename, $fh, "$info->{key}\n");
178 return;
179 }
180
181 my $json = encode_json($info);
182 my $data = encode_base64($json);
183 my $csum = md5_base64($info->{checktime} . $data . $shared_key_data);
184
185 my $raw = "$info->{key}\n$csum\n$data";
186
187 PVE::Tools::safe_print($filename, $fh, $raw);
188
189 write_apt_auth($info->{key});
190 }
191
192 sub check_subscription {
193 my ($key) = @_;
194
195 my $whmcsurl = "https://shop.maurer-it.com";
196
197 my $uri = "$whmcsurl/modules/servers/licensing/verify.php";
198
199 my $server_id = PVE::API2Tools::get_hwaddress();
200
201 my $req_sockets = get_sockets();
202
203 my $check_token = time() . md5_hex(rand(8999999999) + 1000000000) . $key;
204
205 my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
206 my $proxy = $dccfg->{http_proxy};
207
208 my $params = {
209 licensekey => $key,
210 dir => $server_id,
211 domain => 'www.proxmox.com',
212 ip => 'localhost',
213 check_token => $check_token,
214 };
215
216 my $req = HTTP::Request->new('POST' => $uri);
217 $req->header('Content-Type' => 'application/x-www-form-urlencoded');
218 # We use a temporary URI object to format
219 # the application/x-www-form-urlencoded content.
220 my $url = URI->new('http:');
221 $url->query_form(%$params);
222 my $content = $url->query;
223 $req->header('Content-Length' => length($content));
224 $req->content($content);
225
226 my $ua = LWP::UserAgent->new(protocols_allowed => ['https'], timeout => 30);
227
228 if ($proxy) {
229 $ua->proxy(['https'], $proxy);
230 } else {
231 $ua->env_proxy;
232 }
233
234 my $response = $ua->request($req);
235 my $code = $response->code;
236
237 if ($code != 200) {
238 my $msg = $response->message || 'unknown';
239 die "Invalid response from server: $code $msg\n";
240 }
241
242 my $raw = $response->decoded_content;
243
244 my $subinfo = {};
245 while ($raw =~ m/<(.*?)>([^<]+)<\/\1>/g) {
246 my ($k, $v) = ($1, $2);
247 next if !($k eq 'md5hash' || defined($saved_fields->{$k}));
248 $subinfo->{$k} = $v;
249 }
250 $subinfo->{checktime} = time();
251 $subinfo->{key} = $key;
252
253 if ($subinfo->{message}) {
254 $subinfo->{message} =~ s/^Directory Invalid$/Invalid Server ID/;
255 }
256
257 my $emd5sum = md5_hex($shared_key_data . $check_token);
258 if ($subinfo->{status} && $subinfo->{status} eq 'Active') {
259 if (!$subinfo->{md5hash} || ($subinfo->{md5hash} ne $emd5sum)) {
260 die "MD5 Checksum Verification Failed\n";
261 }
262 }
263
264 delete $subinfo->{md5hash};
265
266 check_fields($subinfo, $server_id, $req_sockets);
267
268 return $subinfo;
269 }
270
271 __PACKAGE__->register_method ({
272 name => 'get',
273 path => '',
274 method => 'GET',
275 description => "Read subscription info.",
276 proxyto => 'node',
277 permissions => { user => 'all' },
278 parameters => {
279 additionalProperties => 0,
280 properties => {
281 node => get_standard_option('pve-node'),
282 },
283 },
284 returns => { type => 'object'},
285 code => sub {
286 my ($param) = @_;
287
288 my $server_id = PVE::API2Tools::get_hwaddress();
289
290 my $info = PVE::INotify::read_file('subscription');
291 if (!$info) {
292 return {
293 status => "NotFound",
294 message => "There is no subscription key",
295 serverid => $server_id,
296 }
297 }
298
299 $info->{serverid} = $server_id;
300 $info->{sockets} = get_sockets();
301
302 return $info
303 }});
304
305 __PACKAGE__->register_method ({
306 name => 'update',
307 path => '',
308 method => 'POST',
309 description => "Update subscription info.",
310 proxyto => 'node',
311 protected => 1,
312 parameters => {
313 additionalProperties => 0,
314 properties => {
315 node => get_standard_option('pve-node'),
316 force => {
317 description => "Always connect to server, even if we have up to date info inside local cache.",
318 type => 'boolean',
319 optional => 1,
320 default => 0
321 }
322 },
323 },
324 returns => { type => 'null'},
325 code => sub {
326 my ($param) = @_;
327
328 my $info = PVE::INotify::read_file('subscription');
329 return undef if !$info;
330
331 write_apt_auth($info->{key}) if $info->{key};
332
333 if (!$param->{force} && $info->{status} eq 'Active') {
334 my $age = time() - $info->{checktime};
335 return undef if $age < $localkeydays*60*60*24;
336 }
337
338 my $key = $info->{key};
339
340 $info = check_subscription($key);
341
342 PVE::INotify::write_file('subscription', $info);
343
344 return undef;
345 }});
346
347 __PACKAGE__->register_method ({
348 name => 'set',
349 path => '',
350 method => 'PUT',
351 description => "Set subscription key.",
352 proxyto => 'node',
353 protected => 1,
354 parameters => {
355 additionalProperties => 0,
356 properties => {
357 node => get_standard_option('pve-node'),
358 key => {
359 description => "Proxmox VE subscription key",
360 type => 'string',
361 },
362 },
363 },
364 returns => { type => 'null'},
365 code => sub {
366 my ($param) = @_;
367
368 $param->{key} = PVE::Tools::trim($param->{key});
369
370 my $info = {
371 status => 'New',
372 key => $param->{key},
373 checktime => time(),
374 };
375
376 my $req_sockets = get_sockets();
377 my $server_id = PVE::API2Tools::get_hwaddress();
378
379 check_fields($info, $server_id, $req_sockets);
380
381 PVE::INotify::write_file('subscription', $info);
382
383 $info = check_subscription($param->{key});
384
385 PVE::INotify::write_file('subscription', $info);
386
387 return undef;
388 }});
389
390 1;