From 2f1cb7ef3b3368347aaea0583b806c6eedd5b2a2 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Fri, 29 Sep 2017 09:17:01 +0200 Subject: [PATCH] PVE::Subscription - new class to simplify subscription management --- src/Makefile | 1 + src/PVE/Subscription.pm | 214 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 src/PVE/Subscription.pm diff --git a/src/Makefile b/src/Makefile index 044d23c..3871e59 100644 --- a/src/Makefile +++ b/src/Makefile @@ -7,6 +7,7 @@ MAN1DIR=${MANDIR}/man1/ PERLDIR=${PREFIX}/share/perl5 LIB_SOURCES= \ + Subscription.pm \ CalendarEvent.pm \ OTP.pm \ Ticket.pm \ diff --git a/src/PVE/Subscription.pm b/src/PVE/Subscription.pm new file mode 100644 index 0000000..8de1c02 --- /dev/null +++ b/src/PVE/Subscription.pm @@ -0,0 +1,214 @@ +package PVE::Subscription; + +use strict; +use warnings; +use Digest::MD5 qw(md5_hex md5_base64); +use MIME::Base64; +use HTTP::Request; +use URI; +use LWP::UserAgent; +use JSON; + +use PVE::Tools; +use PVE::INotify; + +# How long the local key is valid for in between remote checks +our $localkeydays = 15; +# How many days to allow after local key expiry before blocking +# access if connection cannot be made +my $allowcheckfaildays = 5; + +my $shared_key_data = "kjfdlskfhiuewhfk947368"; + +my $saved_fields = { + key => 1, + checktime => 1, + status => 1, + message => 0, + validdirectory => 1, + productname => 1, + regdate => 1, + nextduedate => 1, +}; + +sub check_fields { + my ($info, $server_id) = @_; + + foreach my $f (qw(status checktime key)) { + if (!$info->{$f}) { + die "Missing field '$f'\n"; + } + } + + if ($info->{checktime} > time()) { + die "Last check time in future.\n"; + } + + return undef if $info->{status} ne 'Active'; + + foreach my $f (keys %$saved_fields) { + next if !$saved_fields->{$f}; + if (!$info->{$f}) { + die "Missing field '$f'\n"; + } + } + + my $found; + foreach my $hwid (split(/,/, $info->{validdirectory})) { + if ($hwid eq $server_id) { + $found = 1; + last; + } + } + die "Server ID does not match\n" if !$found; + + return undef; +} + +sub check_subscription { + my ($key, $server_id, $proxy) = @_; + + my $whmcsurl = "https://shop.maurer-it.com"; + + my $uri = "$whmcsurl/modules/servers/licensing/verify.php"; + + my $check_token = time() . md5_hex(rand(8999999999) + 1000000000) . $key; + + my $params = { + licensekey => $key, + dir => $server_id, + domain => 'www.proxmox.com', + ip => 'localhost', + check_token => $check_token, + }; + + my $req = HTTP::Request->new('POST' => $uri); + $req->header('Content-Type' => 'application/x-www-form-urlencoded'); + # We use a temporary URI object to format + # the application/x-www-form-urlencoded content. + my $url = URI->new('http:'); + $url->query_form(%$params); + my $content = $url->query; + $req->header('Content-Length' => length($content)); + $req->content($content); + + my $ua = LWP::UserAgent->new(protocols_allowed => ['https'], timeout => 30); + + if ($proxy) { + $ua->proxy(['https'], $proxy); + } else { + $ua->env_proxy; + } + + my $response = $ua->request($req); + my $code = $response->code; + + if ($code != 200) { + my $msg = $response->message || 'unknown'; + die "Invalid response from server: $code $msg\n"; + } + + my $raw = $response->decoded_content; + + my $subinfo = {}; + while ($raw =~ m/<(.*?)>([^<]+)<\/\1>/g) { + my ($k, $v) = ($1, $2); + next if !($k eq 'md5hash' || defined($saved_fields->{$k})); + $subinfo->{$k} = $v; + } + $subinfo->{checktime} = time(); + $subinfo->{key} = $key; + + if ($subinfo->{message}) { + $subinfo->{message} =~ s/^Directory Invalid$/Invalid Server ID/; + } + + my $emd5sum = md5_hex($shared_key_data . $check_token); + if ($subinfo->{status} && $subinfo->{status} eq 'Active') { + if (!$subinfo->{md5hash} || ($subinfo->{md5hash} ne $emd5sum)) { + die "MD5 Checksum Verification Failed\n"; + } + } + + delete $subinfo->{md5hash}; + + check_fields($subinfo, $server_id); + + return $subinfo; +} + +sub read_subscription { + my ($server_id, $filename, $fh) = @_; + + my $info = { status => 'Invalid' }; + + my $key = <$fh>; # first line is the key + chomp $key; + + $info->{key} = $key; + + my $csum = <$fh>; # second line is a checksum + + my $data = ''; + while (defined(my $line = <$fh>)) { + $data .= $line; + } + + if ($csum && $data) { + + chomp $csum; + + my $localinfo = {}; + + eval { + my $json_text = decode_base64($data); + $localinfo = decode_json($json_text); + my $newcsum = md5_base64($localinfo->{checktime} . $data . $shared_key_data); + die "checksum failure\n" if $csum ne $newcsum; + + check_fields($localinfo, $server_id); + + my $age = time() - $localinfo->{checktime}; + + my $maxage = ($localkeydays + $allowcheckfaildays)*60*60*24; + if ($localinfo->{status} eq 'Active' && $age > $maxage) { + $localinfo->{status} = 'Invalid'; + $localinfo->{message} = "subscription info too old"; + } + }; + if (my $err = $@) { + warn $err; + } else { + $info = $localinfo; + } + } + + return $info; +} + +sub update_apt_auth { + my ($key, $server_id) = @_; + + my $auth = { 'enterprise.proxmox.com' => { login => $key, password => $server_id } }; + PVE::INotify::update_file('apt-auth', $auth); +} + +sub write_subscription { + my ($server_id, $filename, $fh, $info) = @_; + + if ($info->{status} eq 'New') { + PVE::Tools::safe_print($filename, $fh, "$info->{key}\n"); + } else { + my $json = encode_json($info); + my $data = encode_base64($json); + my $csum = md5_base64($info->{checktime} . $data . $shared_key_data); + + my $raw = "$info->{key}\n$csum\n$data"; + + PVE::Tools::safe_print($filename, $fh, $raw); + } + + update_apt_auth($info->{key}, $server_id); +} + +1; -- 2.39.2