mirror of
				https://git.proxmox.com/git/pve-manager
				synced 2025-11-04 03:35:39 +00:00 
			
		
		
		
	People normally use cut&paste, and that often adds unexpected white space characters. So we simply remove them for convenience.
		
			
				
	
	
		
			401 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			401 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
package PVE::API2::Subscription;
 | 
						|
 | 
						|
use strict;
 | 
						|
use warnings;
 | 
						|
use Digest::MD5 qw(md5_hex md5_base64);
 | 
						|
use MIME::Base64;
 | 
						|
use Net::SSL;
 | 
						|
use HTTP::Request;
 | 
						|
use LWP::UserAgent;
 | 
						|
use JSON; 
 | 
						|
 | 
						|
use PVE::Tools;
 | 
						|
use PVE::ProcFSTools;
 | 
						|
use PVE::Exception qw(raise_param_exc);
 | 
						|
use PVE::INotify;
 | 
						|
use PVE::Cluster qw (cfs_read_file cfs_write_file);
 | 
						|
use PVE::AccessControl;
 | 
						|
use PVE::Storage;
 | 
						|
use PVE::JSONSchema qw(get_standard_option);
 | 
						|
 | 
						|
use PVE::SafeSyslog;
 | 
						|
 | 
						|
use PVE::API2Tools;
 | 
						|
use PVE::RESTHandler;
 | 
						|
 | 
						|
use base qw(PVE::RESTHandler);
 | 
						|
 | 
						|
PVE::INotify::register_file('subscription', "/etc/subscription",
 | 
						|
			    \&read_etc_pve_subscription,
 | 
						|
			    \&write_etc_pve_subscription);
 | 
						|
 | 
						|
# How long the local key is valid for in between remote checks
 | 
						|
my $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";
 | 
						|
 | 
						|
sub get_sockets {
 | 
						|
    my $info = PVE::ProcFSTools::read_cpuinfo();
 | 
						|
    return $info->{sockets};
 | 
						|
}
 | 
						|
 | 
						|
sub parse_key {
 | 
						|
    my ($key) = @_;
 | 
						|
 | 
						|
    if ($key =~ m/^pve([124])([cbsp])-[0-9a-f]{10}$/) {
 | 
						|
	return wantarray ? ($1, $2) : $1; # number of sockets, level
 | 
						|
    }
 | 
						|
    return undef;
 | 
						|
}
 | 
						|
 | 
						|
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, $req_sockets) = @_;
 | 
						|
 | 
						|
    foreach my $f (qw(status checktime key)) {
 | 
						|
	if (!$info->{$f}) {
 | 
						|
	    die "Missing field '$f'\n";
 | 
						|
	}
 | 
						|
    }
 | 
						|
 | 
						|
    my $sockets = parse_key($info->{key});
 | 
						|
    if (!$sockets) {
 | 
						|
	die "Wrong subscription key format\n";
 | 
						|
    }
 | 
						|
    if ($sockets < $req_sockets) {
 | 
						|
	die "wrong number of sockets ($sockets < $req_sockets)\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 read_etc_pve_subscription {
 | 
						|
    my ($filename, $fh) = @_;
 | 
						|
 | 
						|
    my $info = { status => 'Invalid' };
 | 
						|
 | 
						|
    my $key = <$fh>; # first line is the key
 | 
						|
    chomp $key;
 | 
						|
    my ($sockets, $level) = parse_key($key);
 | 
						|
    die "Wrong subscription key format\n" if !$sockets;
 | 
						|
 | 
						|
    my $csum = <$fh>; # second line is a checksum
 | 
						|
 | 
						|
    $info->{key} = $key;
 | 
						|
 | 
						|
    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;
 | 
						|
 | 
						|
	    my $req_sockets = get_sockets();
 | 
						|
	    my $server_id = PVE::API2Tools::get_hwaddress();
 | 
						|
 | 
						|
	    check_fields($localinfo, $server_id, $req_sockets);
 | 
						|
 | 
						|
	    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;
 | 
						|
	}
 | 
						|
    }
 | 
						|
 | 
						|
    if ($info->{status} eq 'Active') {
 | 
						|
	$info->{level} = $level;
 | 
						|
    }
 | 
						|
 | 
						|
    return $info;
 | 
						|
}
 | 
						|
 | 
						|
sub write_apt_auth {
 | 
						|
    my $key = shift;
 | 
						|
 | 
						|
    my $server_id = PVE::API2Tools::get_hwaddress();
 | 
						|
    my $auth = { 'enterprise.proxmox.com' => { login => $key, password => $server_id } };
 | 
						|
    PVE::INotify::update_file('apt-auth', $auth);
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
sub write_etc_pve_subscription {
 | 
						|
    my ($filename, $fh, $info) = @_;
 | 
						|
 | 
						|
    if ($info->{status} eq 'New') {
 | 
						|
	PVE::Tools::safe_print($filename, $fh, "$info->{key}\n");
 | 
						|
	return;
 | 
						|
    }
 | 
						|
 | 
						|
    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);
 | 
						|
 | 
						|
    write_apt_auth($info->{key});
 | 
						|
}
 | 
						|
 | 
						|
sub check_subscription {
 | 
						|
    my ($key) = @_;
 | 
						|
 | 
						|
    my $whmcsurl = "https://shop.maurer-it.com";
 | 
						|
 | 
						|
    my $uri = "$whmcsurl/modules/servers/licensing/verify.php";
 | 
						|
 
 | 
						|
    my $server_id = PVE::API2Tools::get_hwaddress();
 | 
						|
 | 
						|
    my $req_sockets = get_sockets();
 | 
						|
 | 
						|
    my $check_token = time() . md5_hex(rand(8999999999) + 1000000000) . $key;
 | 
						|
 | 
						|
    my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
 | 
						|
    my $proxy = $dccfg->{http_proxy};
 | 
						|
 | 
						|
    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);
 | 
						|
    $ua->ssl_opts(verify_hostname => 0); # don't care
 | 
						|
 | 
						|
    # HACK: LWP does not use proxy 'CONNECT' for https
 | 
						|
    local $ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS} = "Net::SSL";
 | 
						|
    local ($ENV{HTTPS_PROXY}, $ENV{HTTPS_PROXY_USERNAME}, $ENV{HTTPS_PROXY_PASSWORD});
 | 
						|
 | 
						|
    if ($proxy) {
 | 
						|
	# some proxies reject connection if UserAgent header is not set
 | 
						|
	Net::SSL::send_useragent_to_proxy(1);
 | 
						|
	($ENV{HTTPS_PROXY}, $ENV{HTTPS_PROXY_USERNAME}, $ENV{HTTPS_PROXY_PASSWORD}) =
 | 
						|
	    PVE::API2Tools::parse_http_proxy($proxy);
 | 
						|
	$ua->proxy(['http'], $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, $req_sockets);
 | 
						|
 
 | 
						|
    return $subinfo;
 | 
						|
}
 | 
						|
 | 
						|
__PACKAGE__->register_method ({
 | 
						|
    name => 'get', 
 | 
						|
    path => '', 
 | 
						|
    method => 'GET',
 | 
						|
    description => "Read subscription info.",
 | 
						|
    proxyto => 'node',
 | 
						|
    permissions => { user => 'all' },
 | 
						|
    parameters => {
 | 
						|
    	additionalProperties => 0,
 | 
						|
	properties => {
 | 
						|
	    node => get_standard_option('pve-node'),
 | 
						|
	},
 | 
						|
    },
 | 
						|
    returns => { type => 'object'},
 | 
						|
    code => sub {
 | 
						|
	my ($param) = @_;
 | 
						|
 | 
						|
	my $server_id = PVE::API2Tools::get_hwaddress();
 | 
						|
 | 
						|
	my $info = PVE::INotify::read_file('subscription');
 | 
						|
	if (!$info) {
 | 
						|
	    return {
 | 
						|
		status => "NotFound",
 | 
						|
		message => "There is no subscription key",
 | 
						|
		serverid => $server_id,
 | 
						|
	    }
 | 
						|
	}
 | 
						|
 | 
						|
	$info->{serverid} = $server_id;
 | 
						|
	$info->{sockets} = get_sockets();
 | 
						|
 | 
						|
	return $info
 | 
						|
    }});
 | 
						|
 | 
						|
__PACKAGE__->register_method ({
 | 
						|
    name => 'update', 
 | 
						|
    path => '', 
 | 
						|
    method => 'POST',
 | 
						|
    description => "Update subscription info.",
 | 
						|
    proxyto => 'node',
 | 
						|
    protected => 1,
 | 
						|
    parameters => {
 | 
						|
    	additionalProperties => 0,
 | 
						|
	properties => {
 | 
						|
	    node => get_standard_option('pve-node'),
 | 
						|
	    force => {
 | 
						|
		description => "Always connect to server, even if we have up to date info inside local cache.",
 | 
						|
		type => 'boolean',
 | 
						|
		optional => 1,
 | 
						|
		default => 0
 | 
						|
	    }
 | 
						|
	},
 | 
						|
    },
 | 
						|
    returns => { type => 'null'},
 | 
						|
    code => sub {
 | 
						|
	my ($param) = @_;
 | 
						|
 | 
						|
	my $info = PVE::INotify::read_file('subscription');
 | 
						|
	return undef if !$info;
 | 
						|
 | 
						|
	write_apt_auth($info->{key}) if $info->{key};
 | 
						|
 | 
						|
	if (!$param->{force} && $info->{status} eq 'Active') {
 | 
						|
	    my $age = time() -  $info->{checktime};
 | 
						|
	    return undef if $age < $localkeydays*60*60*24;
 | 
						|
	}
 | 
						|
	
 | 
						|
	my $key = $info->{key};
 | 
						|
 | 
						|
	$info = check_subscription($key);
 | 
						|
 | 
						|
	PVE::INotify::write_file('subscription', $info);
 | 
						|
 | 
						|
	return undef;
 | 
						|
    }});
 | 
						|
 | 
						|
__PACKAGE__->register_method ({
 | 
						|
    name => 'set', 
 | 
						|
    path => '', 
 | 
						|
    method => 'PUT',
 | 
						|
    description => "Set subscription key.",
 | 
						|
    proxyto => 'node',
 | 
						|
    protected => 1,
 | 
						|
    parameters => {
 | 
						|
    	additionalProperties => 0,
 | 
						|
	properties => {
 | 
						|
	    node => get_standard_option('pve-node'),
 | 
						|
	    key => {
 | 
						|
		description => "Proxmox VE subscription key",
 | 
						|
		type => 'string',
 | 
						|
	    },
 | 
						|
	},
 | 
						|
    },
 | 
						|
    returns => { type => 'null'},
 | 
						|
    code => sub {
 | 
						|
	my ($param) = @_;
 | 
						|
 | 
						|
	$param->{key} = PVE::Tools::trim($param->{key});
 | 
						|
 | 
						|
	my $info = {
 | 
						|
	    status => 'New',
 | 
						|
	    key => $param->{key},
 | 
						|
	    checktime => time(),
 | 
						|
	};
 | 
						|
 | 
						|
	my $req_sockets = get_sockets();
 | 
						|
	my $server_id = PVE::API2Tools::get_hwaddress();
 | 
						|
 | 
						|
	check_fields($info, $server_id, $req_sockets);
 | 
						|
 | 
						|
	PVE::INotify::write_file('subscription', $info);
 | 
						|
 | 
						|
	$info = check_subscription($param->{key});
 | 
						|
 | 
						|
	PVE::INotify::write_file('subscription', $info);
 | 
						|
 | 
						|
	return undef;
 | 
						|
    }});
 | 
						|
 | 
						|
1;
 |