PVE::Subscription - new class to simplify subscription management
authorDietmar Maurer <dietmar@proxmox.com>
Fri, 29 Sep 2017 07:17:01 +0000 (09:17 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Fri, 29 Sep 2017 07:17:01 +0000 (09:17 +0200)
src/Makefile
src/PVE/Subscription.pm [new file with mode: 0644]

index 044d23c..3871e59 100644 (file)
@@ -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 (file)
index 0000000..8de1c02
--- /dev/null
@@ -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;