1 package PVE
::API2
::Subscription
;
5 use Digest
::MD5
qw(md5_hex md5_base64);
14 use PVE
::Exception
qw(raise_param_exc);
16 use PVE
::Cluster qw
(cfs_read_file cfs_write_file
);
17 use PVE
::AccessControl
;
19 use PVE
::JSONSchema
qw(get_standard_option);
26 use base
qw(PVE::RESTHandler);
28 PVE
::INotify
::register_file
('subscription', "/etc/subscription",
29 \
&read_etc_pve_subscription
,
30 \
&write_etc_pve_subscription
);
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;
38 my $shared_key_data = "kjfdlskfhiuewhfk947368";
41 my $info = PVE
::ProcFSTools
::read_cpuinfo
();
42 return $info->{sockets
};
48 if ($key =~ m/^pve([124])([cbsp])-[0-9a-f]{10}$/) {
49 return wantarray ?
($1, $2) : $1; # number of sockets, level
66 my ($info, $server_id, $req_sockets) = @_;
68 foreach my $f (qw(status checktime key)) {
70 die "Missing field '$f'\n";
74 my $sockets = parse_key
($info->{key
});
76 die "Wrong subscription key format\n";
78 if ($sockets < $req_sockets) {
79 die "wrong number of sockets ($sockets < $req_sockets)\n";
82 if ($info->{checktime
} > time()) {
83 die "Last check time in future.\n";
86 return undef if $info->{status
} ne 'Active';
88 foreach my $f (keys %$saved_fields) {
89 next if !$saved_fields->{$f};
91 die "Missing field '$f'\n";
96 foreach my $hwid (split(/,/, $info->{validdirectory
})) {
97 if ($hwid eq $server_id) {
102 die "Server ID does not match\n" if !$found;
107 sub read_etc_pve_subscription
{
108 my ($filename, $fh) = @_;
110 my $info = { status
=> 'Invalid' };
112 my $key = <$fh>; # first line is the key
114 my ($sockets, $level) = parse_key
($key);
115 die "Wrong subscription key format\n" if !$sockets;
117 my $csum = <$fh>; # second line is a checksum
122 while (defined(my $line = <$fh>)) {
126 if ($csum && $data) {
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;
138 my $req_sockets = get_sockets
();
139 my $server_id = PVE
::API2Tools
::get_hwaddress
();
141 check_fields
($localinfo, $server_id, $req_sockets);
143 my $age = time() - $localinfo->{checktime
};
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";
158 if ($info->{status
} eq 'Active') {
159 $info->{level
} = $level;
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);
174 sub write_etc_pve_subscription
{
175 my ($filename, $fh, $info) = @_;
177 if ($info->{status
} eq 'New') {
178 PVE
::Tools
::safe_print
($filename, $fh, "$info->{key}\n");
182 my $json = encode_json
($info);
183 my $data = encode_base64
($json);
184 my $csum = md5_base64
($info->{checktime
} . $data . $shared_key_data);
186 my $raw = "$info->{key}\n$csum\n$data";
188 PVE
::Tools
::safe_print
($filename, $fh, $raw);
190 write_apt_auth
($info->{key
});
193 sub check_subscription
{
196 my $whmcsurl = "https://shop.maurer-it.com";
198 my $uri = "$whmcsurl/modules/servers/licensing/verify.php";
200 my $server_id = PVE
::API2Tools
::get_hwaddress
();
202 my $req_sockets = get_sockets
();
204 my $check_token = time() . md5_hex
(rand(8999999999) + 1000000000) . $key;
206 my $dccfg = PVE
::Cluster
::cfs_read_file
('datacenter.cfg');
207 my $proxy = $dccfg->{http_proxy
};
212 domain
=> 'www.proxmox.com',
214 check_token
=> $check_token,
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);
227 my $ua = LWP
::UserAgent-
>new(protocols_allowed
=> ['https'], timeout
=> 30);
228 $ua->ssl_opts(verify_hostname
=> 0); # don't care
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
});
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);
244 my $response = $ua->request($req);
245 my $code = $response->code;
248 my $msg = $response->message || 'unknown';
249 die "Invalid response from server: $code $msg\n";
252 my $raw = $response->decoded_content;
255 while ($raw =~ m/<(.*?)>([^<]+)<\/\
1>/g
) {
256 my ($k, $v) = ($1, $2);
257 next if !($k eq 'md5hash' || defined($saved_fields->{$k}));
260 $subinfo->{checktime
} = time();
261 $subinfo->{key
} = $key;
263 if ($subinfo->{message
}) {
264 $subinfo->{message
} =~ s/^Directory Invalid$/Invalid Server ID/;
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";
274 delete $subinfo->{md5hash
};
276 check_fields
($subinfo, $server_id, $req_sockets);
281 __PACKAGE__-
>register_method ({
285 description
=> "Read subscription info.",
287 permissions
=> { user
=> 'all' },
289 additionalProperties
=> 0,
291 node
=> get_standard_option
('pve-node'),
294 returns
=> { type
=> 'object'},
298 my $server_id = PVE
::API2Tools
::get_hwaddress
();
300 my $info = PVE
::INotify
::read_file
('subscription');
303 status
=> "NotFound",
304 message
=> "There is no subscription key",
305 serverid
=> $server_id,
309 $info->{serverid
} = $server_id;
310 $info->{sockets
} = get_sockets
();
315 __PACKAGE__-
>register_method ({
319 description
=> "Update subscription info.",
323 additionalProperties
=> 0,
325 node
=> get_standard_option
('pve-node'),
327 description
=> "Always connect to server, even if we have up to date info inside local cache.",
334 returns
=> { type
=> 'null'},
338 my $info = PVE
::INotify
::read_file
('subscription');
339 return undef if !$info;
341 write_apt_auth
($info->{key
}) if $info->{key
};
343 if (!$param->{force
} && $info->{status
} eq 'Active') {
344 my $age = time() - $info->{checktime
};
345 return undef if $age < $localkeydays*60*60*24;
348 my $key = $info->{key
};
350 $info = check_subscription
($key);
352 PVE
::INotify
::write_file
('subscription', $info);
357 __PACKAGE__-
>register_method ({
361 description
=> "Set subscription key.",
365 additionalProperties
=> 0,
367 node
=> get_standard_option
('pve-node'),
369 description
=> "Proxmox VE subscription key",
374 returns
=> { type
=> 'null'},
378 $param->{key
} = PVE
::Tools
::trim
($param->{key
});
382 key
=> $param->{key
},
386 my $req_sockets = get_sockets
();
387 my $server_id = PVE
::API2Tools
::get_hwaddress
();
389 check_fields
($info, $server_id, $req_sockets);
391 PVE
::INotify
::write_file
('subscription', $info);
393 $info = check_subscription
($param->{key
});
395 PVE
::INotify
::write_file
('subscription', $info);