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