1 package PVE
::API2
::Subscription
;
5 use Digest
::MD5
qw(md5_hex md5_base64);
13 use PVE
::Exception
qw(raise_param_exc);
15 use PVE
::Cluster qw
(cfs_read_file cfs_write_file
);
16 use PVE
::AccessControl
;
18 use PVE
::JSONSchema
qw(get_standard_option);
25 use base
qw(PVE::RESTHandler);
27 PVE
::INotify
::register_file
('subscription', "/etc/subscription",
28 \
&read_etc_pve_subscription
,
29 \
&write_etc_pve_subscription
);
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;
37 my $shared_key_data = "kjfdlskfhiuewhfk947368";
40 my $info = PVE
::ProcFSTools
::read_cpuinfo
();
41 return $info->{sockets
};
47 if ($key =~ m/^pve([124])([cbsp])-[0-9a-f]{10}$/) {
48 return wantarray ?
($1, $2) : $1; # number of sockets, level
65 my ($info, $server_id, $req_sockets) = @_;
67 foreach my $f (qw(status checktime key)) {
69 die "Missing field '$f'\n";
73 my $sockets = parse_key
($info->{key
});
75 die "Wrong subscription key format\n";
77 if ($sockets < $req_sockets) {
78 die "wrong number of sockets ($sockets < $req_sockets)\n";
81 if ($info->{checktime
} > time()) {
82 die "Last check time in future.\n";
85 return undef if $info->{status
} ne 'Active';
87 foreach my $f (keys %$saved_fields) {
88 next if !$saved_fields->{$f};
90 die "Missing field '$f'\n";
95 foreach my $hwid (split(/,/, $info->{validdirectory
})) {
96 if ($hwid eq $server_id) {
101 die "Server ID does not match\n" if !$found;
106 sub read_etc_pve_subscription
{
107 my ($filename, $fh) = @_;
109 my $info = { status
=> 'Invalid' };
111 my $key = <$fh>; # first line is the key
113 my ($sockets, $level) = parse_key
($key);
114 die "Wrong subscription key format\n" if !$sockets;
116 my $csum = <$fh>; # second line is a checksum
121 while (defined(my $line = <$fh>)) {
125 if ($csum && $data) {
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;
137 my $req_sockets = get_sockets
();
138 my $server_id = PVE
::API2Tools
::get_hwaddress
();
140 check_fields
($localinfo, $server_id, $req_sockets);
142 my $age = time() - $localinfo->{checktime
};
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";
157 if ($info->{status
} eq 'Active') {
158 $info->{level
} = $level;
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);
173 sub write_etc_pve_subscription
{
174 my ($filename, $fh, $info) = @_;
176 if ($info->{status
} eq 'New') {
177 PVE
::Tools
::safe_print
($filename, $fh, "$info->{key}\n");
181 my $json = encode_json
($info);
182 my $data = encode_base64
($json);
183 my $csum = md5_base64
($info->{checktime
} . $data . $shared_key_data);
185 my $raw = "$info->{key}\n$csum\n$data";
187 PVE
::Tools
::safe_print
($filename, $fh, $raw);
189 write_apt_auth
($info->{key
});
192 sub check_subscription
{
195 my $whmcsurl = "https://shop.maurer-it.com";
197 my $uri = "$whmcsurl/modules/servers/licensing/verify.php";
199 my $server_id = PVE
::API2Tools
::get_hwaddress
();
201 my $req_sockets = get_sockets
();
203 my $check_token = time() . md5_hex
(rand(8999999999) + 1000000000) . $key;
205 my $dccfg = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
206 my $proxy = $dccfg->{http_proxy
};
211 domain
=> 'www.proxmox.com',
213 check_token
=> $check_token,
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);
226 my $ua = LWP
::UserAgent-
>new(protocols_allowed
=> ['https'], timeout
=> 30);
229 $ua->proxy(['https'], $proxy);
234 my $response = $ua->request($req);
235 my $code = $response->code;
238 my $msg = $response->message || 'unknown';
239 die "Invalid response from server: $code $msg\n";
242 my $raw = $response->decoded_content;
245 while ($raw =~ m/<(.*?)>([^<]+)<\/\
1>/g
) {
246 my ($k, $v) = ($1, $2);
247 next if !($k eq 'md5hash' || defined($saved_fields->{$k}));
250 $subinfo->{checktime
} = time();
251 $subinfo->{key
} = $key;
253 if ($subinfo->{message
}) {
254 $subinfo->{message
} =~ s/^Directory Invalid$/Invalid Server ID/;
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";
264 delete $subinfo->{md5hash
};
266 check_fields
($subinfo, $server_id, $req_sockets);
271 __PACKAGE__-
>register_method ({
275 description
=> "Read subscription info.",
277 permissions
=> { user
=> 'all' },
279 additionalProperties
=> 0,
281 node
=> get_standard_option
('pve-node'),
284 returns
=> { type
=> 'object'},
288 my $server_id = PVE
::API2Tools
::get_hwaddress
();
290 my $info = PVE
::INotify
::read_file
('subscription');
293 status
=> "NotFound",
294 message
=> "There is no subscription key",
295 serverid
=> $server_id,
299 $info->{serverid
} = $server_id;
300 $info->{sockets
} = get_sockets
();
305 __PACKAGE__-
>register_method ({
309 description
=> "Update subscription info.",
313 additionalProperties
=> 0,
315 node
=> get_standard_option
('pve-node'),
317 description
=> "Always connect to server, even if we have up to date info inside local cache.",
324 returns
=> { type
=> 'null'},
328 my $info = PVE
::INotify
::read_file
('subscription');
329 return undef if !$info;
331 write_apt_auth
($info->{key
}) if $info->{key
};
333 if (!$param->{force
} && $info->{status
} eq 'Active') {
334 my $age = time() - $info->{checktime
};
335 return undef if $age < $localkeydays*60*60*24;
338 my $key = $info->{key
};
340 $info = check_subscription
($key);
342 PVE
::INotify
::write_file
('subscription', $info);
347 __PACKAGE__-
>register_method ({
351 description
=> "Set subscription key.",
355 additionalProperties
=> 0,
357 node
=> get_standard_option
('pve-node'),
359 description
=> "Proxmox VE subscription key",
364 returns
=> { type
=> 'null'},
368 $param->{key
} = PVE
::Tools
::trim
($param->{key
});
372 key
=> $param->{key
},
376 my $req_sockets = get_sockets
();
377 my $server_id = PVE
::API2Tools
::get_hwaddress
();
379 check_fields
($info, $server_id, $req_sockets);
381 PVE
::INotify
::write_file
('subscription', $info);
383 $info = check_subscription
($param->{key
});
385 PVE
::INotify
::write_file
('subscription', $info);