]> git.proxmox.com Git - pve-common.git/blob - src/PVE/Subscription.pm
PVE::Subscription - new class to simplify subscription management
[pve-common.git] / src / PVE / Subscription.pm
1 package PVE::Subscription;
2
3 use strict;
4 use warnings;
5 use Digest::MD5 qw(md5_hex md5_base64);
6 use MIME::Base64;
7 use HTTP::Request;
8 use URI;
9 use LWP::UserAgent;
10 use JSON;
11
12 use PVE::Tools;
13 use PVE::INotify;
14
15 # How long the local key is valid for in between remote checks
16 our $localkeydays = 15;
17 # How many days to allow after local key expiry before blocking
18 # access if connection cannot be made
19 my $allowcheckfaildays = 5;
20
21 my $shared_key_data = "kjfdlskfhiuewhfk947368";
22
23 my $saved_fields = {
24 key => 1,
25 checktime => 1,
26 status => 1,
27 message => 0,
28 validdirectory => 1,
29 productname => 1,
30 regdate => 1,
31 nextduedate => 1,
32 };
33
34 sub check_fields {
35 my ($info, $server_id) = @_;
36
37 foreach my $f (qw(status checktime key)) {
38 if (!$info->{$f}) {
39 die "Missing field '$f'\n";
40 }
41 }
42
43 if ($info->{checktime} > time()) {
44 die "Last check time in future.\n";
45 }
46
47 return undef if $info->{status} ne 'Active';
48
49 foreach my $f (keys %$saved_fields) {
50 next if !$saved_fields->{$f};
51 if (!$info->{$f}) {
52 die "Missing field '$f'\n";
53 }
54 }
55
56 my $found;
57 foreach my $hwid (split(/,/, $info->{validdirectory})) {
58 if ($hwid eq $server_id) {
59 $found = 1;
60 last;
61 }
62 }
63 die "Server ID does not match\n" if !$found;
64
65 return undef;
66 }
67
68 sub check_subscription {
69 my ($key, $server_id, $proxy) = @_;
70
71 my $whmcsurl = "https://shop.maurer-it.com";
72
73 my $uri = "$whmcsurl/modules/servers/licensing/verify.php";
74
75 my $check_token = time() . md5_hex(rand(8999999999) + 1000000000) . $key;
76
77 my $params = {
78 licensekey => $key,
79 dir => $server_id,
80 domain => 'www.proxmox.com',
81 ip => 'localhost',
82 check_token => $check_token,
83 };
84
85 my $req = HTTP::Request->new('POST' => $uri);
86 $req->header('Content-Type' => 'application/x-www-form-urlencoded');
87 # We use a temporary URI object to format
88 # the application/x-www-form-urlencoded content.
89 my $url = URI->new('http:');
90 $url->query_form(%$params);
91 my $content = $url->query;
92 $req->header('Content-Length' => length($content));
93 $req->content($content);
94
95 my $ua = LWP::UserAgent->new(protocols_allowed => ['https'], timeout => 30);
96
97 if ($proxy) {
98 $ua->proxy(['https'], $proxy);
99 } else {
100 $ua->env_proxy;
101 }
102
103 my $response = $ua->request($req);
104 my $code = $response->code;
105
106 if ($code != 200) {
107 my $msg = $response->message || 'unknown';
108 die "Invalid response from server: $code $msg\n";
109 }
110
111 my $raw = $response->decoded_content;
112
113 my $subinfo = {};
114 while ($raw =~ m/<(.*?)>([^<]+)<\/\1>/g) {
115 my ($k, $v) = ($1, $2);
116 next if !($k eq 'md5hash' || defined($saved_fields->{$k}));
117 $subinfo->{$k} = $v;
118 }
119 $subinfo->{checktime} = time();
120 $subinfo->{key} = $key;
121
122 if ($subinfo->{message}) {
123 $subinfo->{message} =~ s/^Directory Invalid$/Invalid Server ID/;
124 }
125
126 my $emd5sum = md5_hex($shared_key_data . $check_token);
127 if ($subinfo->{status} && $subinfo->{status} eq 'Active') {
128 if (!$subinfo->{md5hash} || ($subinfo->{md5hash} ne $emd5sum)) {
129 die "MD5 Checksum Verification Failed\n";
130 }
131 }
132
133 delete $subinfo->{md5hash};
134
135 check_fields($subinfo, $server_id);
136
137 return $subinfo;
138 }
139
140 sub read_subscription {
141 my ($server_id, $filename, $fh) = @_;
142
143 my $info = { status => 'Invalid' };
144
145 my $key = <$fh>; # first line is the key
146 chomp $key;
147
148 $info->{key} = $key;
149
150 my $csum = <$fh>; # second line is a checksum
151
152 my $data = '';
153 while (defined(my $line = <$fh>)) {
154 $data .= $line;
155 }
156
157 if ($csum && $data) {
158
159 chomp $csum;
160
161 my $localinfo = {};
162
163 eval {
164 my $json_text = decode_base64($data);
165 $localinfo = decode_json($json_text);
166 my $newcsum = md5_base64($localinfo->{checktime} . $data . $shared_key_data);
167 die "checksum failure\n" if $csum ne $newcsum;
168
169 check_fields($localinfo, $server_id);
170
171 my $age = time() - $localinfo->{checktime};
172
173 my $maxage = ($localkeydays + $allowcheckfaildays)*60*60*24;
174 if ($localinfo->{status} eq 'Active' && $age > $maxage) {
175 $localinfo->{status} = 'Invalid';
176 $localinfo->{message} = "subscription info too old";
177 }
178 };
179 if (my $err = $@) {
180 warn $err;
181 } else {
182 $info = $localinfo;
183 }
184 }
185
186 return $info;
187 }
188
189 sub update_apt_auth {
190 my ($key, $server_id) = @_;
191
192 my $auth = { 'enterprise.proxmox.com' => { login => $key, password => $server_id } };
193 PVE::INotify::update_file('apt-auth', $auth);
194 }
195
196 sub write_subscription {
197 my ($server_id, $filename, $fh, $info) = @_;
198
199 if ($info->{status} eq 'New') {
200 PVE::Tools::safe_print($filename, $fh, "$info->{key}\n");
201 } else {
202 my $json = encode_json($info);
203 my $data = encode_base64($json);
204 my $csum = md5_base64($info->{checktime} . $data . $shared_key_data);
205
206 my $raw = "$info->{key}\n$csum\n$data";
207
208 PVE::Tools::safe_print($filename, $fh, $raw);
209 }
210
211 update_apt_auth($info->{key}, $server_id);
212 }
213
214 1;