]> git.proxmox.com Git - pmg-api.git/blame - PMG/Utils.pm
verify_optional_pmail: do not raise error if $pmail ne $authuser
[pmg-api.git] / PMG / Utils.pm
CommitLineData
758c7b6b
DM
1package PMG::Utils;
2
3use strict;
4use warnings;
758c7b6b 5use DBI;
b8ea5d5d 6use Net::Cmd;
758c7b6b 7use Net::SMTP;
26357b0a 8use IO::File;
cad3d400 9use File::stat;
d17c5265
DM
10use POSIX qw(strftime);
11use File::stat;
ff1c5a81 12use File::Basename;
758c7b6b
DM
13use MIME::Words;
14use MIME::Parser;
8210c7fa 15use Time::HiRes qw (gettimeofday);
2f7031b7 16use Time::Local;
26357b0a 17use Xdgmime;
d0d91cda 18use Data::Dumper;
62ebb4bc 19use Digest::SHA;
f609bf7f 20use Net::IP;
9f67f5b3 21use Socket;
dff363d9
DM
22use RRDs;
23use Filesys::Df;
8208c076 24use Encode;
9a56f771 25use HTML::Entities;
758c7b6b 26
dff363d9 27use PVE::ProcFSTools;
f609bf7f 28use PVE::Network;
5953119e 29use PVE::Tools;
758c7b6b 30use PVE::SafeSyslog;
f609bf7f 31use PVE::ProcFSTools;
d0d91cda 32use PMG::AtomicFile;
8210c7fa 33use PMG::MailQueue;
c4df1b85 34use PMG::SMTPPrinter;
758c7b6b 35
62ebb4bc
DM
36my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
37
38PVE::JSONSchema::register_format('pmg-realm', \&verify_realm);
39sub verify_realm {
40 my ($realm, $noerr) = @_;
41
42 if ($realm !~ m/^${realm_regex}$/) {
43 return undef if $noerr;
44 die "value does not look like a valid realm\n";
45 }
46 return $realm;
47}
48
49PVE::JSONSchema::register_standard_option('realm', {
50 description => "Authentication domain ID",
51 type => 'string', format => 'pmg-realm',
52 maxLength => 32,
53});
54
55PVE::JSONSchema::register_format('pmg-userid', \&verify_username);
56sub verify_username {
57 my ($username, $noerr) = @_;
58
59 $username = '' if !$username;
60 my $len = length($username);
61 if ($len < 3) {
62 die "user name '$username' is too short\n" if !$noerr;
63 return undef;
64 }
65 if ($len > 64) {
66 die "user name '$username' is too long ($len > 64)\n" if !$noerr;
67 return undef;
68 }
69
70 # we only allow a limited set of characters
71 # colon is not allowed, because we store usernames in
72 # colon separated lists)!
73 # slash is not allowed because it is used as pve API delimiter
74 # also see "man useradd"
75 if ($username =~ m!^([^\s:/]+)\@(${realm_regex})$!) {
76 return wantarray ? ($username, $1, $2) : $username;
77 }
78
79 die "value '$username' does not look like a valid user name\n" if !$noerr;
80
81 return undef;
82}
83
84PVE::JSONSchema::register_standard_option('userid', {
85 description => "User ID",
86 type => 'string', format => 'pmg-userid',
6b0180c3 87 minLength => 4,
62ebb4bc 88 maxLength => 64,
6b0180c3 89});
62ebb4bc
DM
90
91PVE::JSONSchema::register_standard_option('username', {
92 description => "Username (without realm)",
93 type => 'string',
94 pattern => '[^\s:\/\@]{3,60}',
6b0180c3 95 minLength => 4,
62ebb4bc
DM
96 maxLength => 64,
97});
98
758c7b6b
DM
99sub lastid {
100 my ($dbh, $seq) = @_;
101
102 return $dbh->last_insert_id(
103 undef, undef, undef, undef, { sequence => $seq});
104}
105
ab466078
DM
106# quote all regex operators
107sub quote_regex {
108 my $val = shift;
109
110 $val =~ s/([\(\)\[\]\/\}\+\*\?\.\|\^\$\\])/\\$1/g;
111
112 return $val;
113}
114
cad3d400
DM
115sub file_older_than {
116 my ($filename, $lasttime) = @_;
117
118 my $st = stat($filename);
119
120 return 0 if !defined($st);
121
122 return ($lasttime >= $st->ctime);
123}
124
758c7b6b
DM
125sub extract_filename {
126 my ($head) = @_;
127
128 if (my $value = $head->recommended_filename()) {
8210c7fa 129 chomp $value;
758c7b6b
DM
130 if (my $decvalue = MIME::Words::decode_mimewords($value)) {
131 $decvalue =~ s/\0/ /g;
5953119e 132 $decvalue = PVE::Tools::trim($decvalue);
758c7b6b
DM
133 return $decvalue;
134 }
135 }
136
137 return undef;
138}
139
140sub remove_marks {
141 my ($entity, $add_id, $id) = @_;
142
143 $id //= 1;
144
145 foreach my $tag (grep {/^x-proxmox-tmp/i} $entity->head->tags) {
146 $entity->head->delete ($tag);
147 }
148
149 $entity->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
150
151 foreach my $part ($entity->parts) {
152 $id = remove_marks($part, $add_id, $id + 1);
153 }
154
155 return $id;
156}
157
158sub subst_values {
159 my ($body, $dh) = @_;
160
161 return if !$body;
162
163 foreach my $k (keys %$dh) {
164 my $v = $dh->{$k};
bd98f5a1
DM
165 if (defined($v)) {
166 $body =~ s/__\Q${k}\E__/$v/gs;
758c7b6b
DM
167 }
168 }
169
170 return $body;
171}
172
173sub reinject_mail {
174 my ($entity, $sender, $targets, $xforward, $me, $nodsn) = @_;
175
176 my $smtp;
177 my $resid;
178 my $rescode;
179 my $resmess;
180
181 eval {
182 my $smtp = Net::SMTP->new('127.0.0.1', Port => 10025, Hello => $me) ||
183 die "unable to connect to localhost at port 10025";
184
185 if (defined($xforward)) {
186 my $xfwd;
8210c7fa 187
758c7b6b
DM
188 foreach my $attr (keys %{$xforward}) {
189 $xfwd .= " $attr=$xforward->{$attr}";
190 }
191
192 if ($xfwd && $smtp->command("XFORWARD", $xfwd)->response() != CMD_OK) {
193 syslog('err', "xforward error - got: %s %s", $smtp->code, scalar($smtp->message));
194 }
195 }
196
197 if (!$smtp->mail($sender)) {
198 syslog('err', "smtp error - got: %s %s", $smtp->code, scalar ($smtp->message));
199 die "smtp from: ERROR";
200 }
201
202 my $dsnopts = $nodsn ? {Notify => ['NEVER']} : {};
203
204 if (!$smtp->to (@$targets, $dsnopts)) {
205 syslog ('err', "smtp error - got: %s %s", $smtp->code, scalar($smtp->message));
206 die "smtp to: ERROR";
207 }
208
209 # Output the head:
210 #$entity->sync_headers ();
211 $smtp->data();
212
213 my $out = PMG::SMTPPrinter->new($smtp);
214 $entity->print($out);
8210c7fa
DM
215
216 # make sure we always have a newline at the end of the mail
758c7b6b
DM
217 # else dataend() fails
218 $smtp->datasend("\n");
219
220 if ($smtp->dataend()) {
221 my @msgs = $smtp->message;
8210c7fa
DM
222 $resmess = $msgs[$#msgs];
223 ($resid) = $resmess =~ m/Ok: queued as ([0-9A-Z]+)/;
758c7b6b
DM
224 $rescode = $smtp->code;
225 if (!$resid) {
226 die sprintf("unexpected SMTP result - got: %s %s : WARNING", $smtp->code, $resmess);
8210c7fa 227 }
758c7b6b
DM
228 } else {
229 my @msgs = $smtp->message;
8210c7fa 230 $resmess = $msgs[$#msgs];
758c7b6b
DM
231 $rescode = $smtp->code;
232 die sprintf("sending data failed - got: %s %s : ERROR", $smtp->code, $resmess);
233 }
234 };
235 my $err = $@;
8210c7fa 236
758c7b6b 237 $smtp->quit if $smtp;
8210c7fa 238
758c7b6b
DM
239 if ($err) {
240 syslog ('err', $err);
241 }
242
243 return wantarray ? ($resid, $rescode, $resmess) : $resid;
244}
245
8210c7fa
DM
246sub analyze_virus_clam {
247 my ($queue, $dname, $pmg_cfg) = @_;
248
249 my $timeout = 60*5;
250 my $vinfo;
251
252 my $clamdscan_opts = "--stdout";
253
254 my ($csec, $usec) = gettimeofday();
255
256 my $previous_alarm;
257
258 eval {
259
260 $previous_alarm = alarm($timeout);
261
262 $SIG{ALRM} = sub {
263 die "$queue->{logid}: Maximum time ($timeout sec) exceeded. " .
264 "virus analyze (clamav) failed: ERROR";
265 };
266
267 open(CMD, "/usr/bin/clamdscan $clamdscan_opts '$dname'|") ||
268 die "$queue->{logid}: can't exec clamdscan: $! : ERROR";
269
270 my $ifiles;
8210c7fa 271
54eaf7df
DM
272 my $response = '';
273 while (defined(my $line = <CMD>)) {
274 if ($line =~ m/^$dname.*:\s+([^ :]*)\s+FOUND$/) {
8210c7fa
DM
275 # we just use the first detected virus name
276 $vinfo = $1 if !$vinfo;
54eaf7df 277 } elsif ($line =~ m/^Infected files:\s(\d*)$/i) {
8210c7fa
DM
278 $ifiles = $1;
279 }
280
54eaf7df 281 $response .= $line;
8210c7fa
DM
282 }
283
284 close(CMD);
285
286 alarm(0); # avoid race conditions
287
288 if (!defined($ifiles)) {
289 die "$queue->{logid}: got undefined output from " .
54eaf7df 290 "virus detector: $response : ERROR";
8210c7fa
DM
291 }
292
293 if ($vinfo) {
54eaf7df 294 syslog('info', "$queue->{logid}: virus detected: $vinfo (clamav)");
8210c7fa
DM
295 }
296 };
297 my $err = $@;
298
299 alarm($previous_alarm);
300
301 my ($csec_end, $usec_end) = gettimeofday();
302 $queue->{ptime_clam} =
303 int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
304
305 if ($err) {
306 syslog ('err', $err);
307 $vinfo = undef;
308 $queue->{errors} = 1;
309 }
310
311 $queue->{vinfo_clam} = $vinfo;
312
313 return $vinfo ? "$vinfo (clamav)" : undef;
314}
315
316sub analyze_virus {
317 my ($queue, $filename, $pmg_cfg, $testmode) = @_;
318
319 # TODO: support other virus scanners?
320
321 # always scan with clamav
322 return analyze_virus_clam($queue, $filename, $pmg_cfg);
323}
324
26357b0a
DM
325sub magic_mime_type_for_file {
326 my ($filename) = @_;
dff363d9 327
26357b0a
DM
328 # we do not use get_mime_type_for_file, because that considers
329 # filename extensions - we only want magic type detection
330
331 my $bufsize = Xdgmime::xdg_mime_get_max_buffer_extents();
332 die "got strange value for max_buffer_extents" if $bufsize > 4096*10;
333
334 my $ct = "application/octet-stream";
335
dff363d9 336 my $fh = IO::File->new("<$filename") ||
26357b0a
DM
337 die "unable to open file '$filename' - $!";
338
339 my ($buf, $len);
340 if (($len = $fh->read($buf, $bufsize)) > 0) {
341 $ct = xdg_mime_get_mime_type_for_data($buf, $len);
342 }
343 $fh->close();
dff363d9 344
26357b0a 345 die "unable to read file '$filename' - $!" if ($len < 0);
dff363d9 346
26357b0a
DM
347 return $ct;
348}
758c7b6b 349
1d4193a1
DM
350sub add_ct_marks {
351 my ($entity) = @_;
352
353 if (my $path = $entity->{PMX_decoded_path}) {
354
355 # set a reasonable default if magic does not give a result
356 $entity->{PMX_magic_ct} = $entity->head->mime_attr('content-type');
357
358 if (my $ct = magic_mime_type_for_file($path)) {
359 if ($ct ne 'application/octet-stream' || !$entity->{PMX_magic_ct}) {
360 $entity->{PMX_magic_ct} = $ct;
361 }
362 }
363
364 my $filename = $entity->head->recommended_filename;
365 $filename = basename($path) if !defined($filename) || $filename eq '';
366
367 if (my $ct = xdg_mime_get_mime_type_from_file_name($filename)) {
368 $entity->{PMX_glob_ct} = $ct;
369 }
370 }
371
372 foreach my $part ($entity->parts) {
373 add_ct_marks ($part);
374 }
375}
376
f609bf7f
DM
377# x509 certificate utils
378
896ef634
DM
379# only write output if something fails
380sub run_silent_cmd {
381 my ($cmd) = @_;
382
383 my $outbuf = '';
384
385 my $record_output = sub {
386 $outbuf .= shift;
387 $outbuf .= "\n";
388 };
389
390 eval {
391 PVE::Tools::run_command($cmd, outfunc => $record_output,
392 errfunc => $record_output);
393 };
394 my $err = $@;
395
396 if ($err) {
397 print STDERR $outbuf;
398 die $err;
399 }
400}
401
3278b571 402my $proxmox_tls_cert_fn = "/etc/pmg/pmg-tls.pem";
f609bf7f
DM
403
404sub gen_proxmox_tls_cert {
bc44eb02 405 my ($force) = @_;
f609bf7f 406
bc44eb02
DM
407 my $resolv = PVE::INotify::read_file('resolvconf');
408 my $domain = $resolv->{search};
409
410 my $company = $domain; # what else ?
411 my $cn = "*.$domain";
412
413 return if !$force && -f $proxmox_tls_cert_fn;
f609bf7f
DM
414
415 my $sslconf = <<__EOD__;
416RANDFILE = /root/.rnd
417extensions = v3_req
418
419[ req ]
420default_bits = 4096
421distinguished_name = req_distinguished_name
422req_extensions = v3_req
423prompt = no
424string_mask = nombstr
425
426[ req_distinguished_name ]
427organizationalUnitName = Proxmox Mail Gateway
428organizationName = $company
429commonName = $cn
430
431[ v3_req ]
432basicConstraints = CA:FALSE
433nsCertType = server
434keyUsage = nonRepudiation, digitalSignature, keyEncipherment
435__EOD__
436
3278b571 437 my $cfgfn = "/tmp/pmgtlsconf-$$.tmp";
f609bf7f
DM
438 my $fh = IO::File->new ($cfgfn, "w");
439 print $fh $sslconf;
440 close ($fh);
441
442 eval {
896ef634
DM
443 my $cmd = ['openssl', 'req', '-batch', '-x509', '-new', '-sha256',
444 '-config', $cfgfn, '-days', 3650, '-nodes',
445 '-out', $proxmox_tls_cert_fn,
446 '-keyout', $proxmox_tls_cert_fn];
447 run_silent_cmd($cmd);
f609bf7f
DM
448 };
449
450 if (my $err = $@) {
451 unlink $proxmox_tls_cert_fn;
452 unlink $cfgfn;
453 die "unable to generate proxmox certificate request:\n$err";
454 }
455
456 unlink $cfgfn;
457}
458
459sub find_local_network_for_ip {
460 my ($ip) = @_;
461
462 my $testip = Net::IP->new($ip);
463
464 my $isv6 = $testip->version == 6;
465 my $routes = $isv6 ?
466 PVE::ProcFSTools::read_proc_net_ipv6_route() :
467 PVE::ProcFSTools::read_proc_net_route();
468
469 foreach my $entry (@$routes) {
470 my $mask;
471 if ($isv6) {
472 $mask = $entry->{prefix};
473 next if !$mask; # skip the default route...
474 } else {
475 $mask = $PVE::Network::ipv4_mask_hash_localnet->{$entry->{mask}};
476 next if !defined($mask);
477 }
478 my $cidr = "$entry->{dest}/$mask";
479 my $testnet = Net::IP->new($cidr);
480 my $overlap = $testnet->overlaps($testip);
481 if ($overlap == $Net::IP::IP_B_IN_A_OVERLAP ||
482 $overlap == $Net::IP::IP_IDENTICAL)
483 {
484 return $cidr;
485 }
486 }
487
488 die "unable to detect local network for ip '$ip'\n";
489}
d0d91cda 490
ef07e4d0
DM
491my $service_aliases = {
492 'postfix' => 'postfix@-',
493 'postgres' => 'postgresql@9.6-main',
494};
495
496sub lookup_real_service_name {
497 my $alias = shift;
498
499 return $service_aliases->{$alias} // $alias;
500}
501
230b4e03
DM
502sub get_full_service_state {
503 my ($service) = @_;
504
505 my $res;
506
507 my $parser = sub {
508 my $line = shift;
509 if ($line =~ m/^([^=\s]+)=(.*)$/) {
510 $res->{$1} = $2;
511 }
512 };
513
ef07e4d0 514 $service = $service_aliases->{$service} // $service;
230b4e03
DM
515 PVE::Tools::run_command(['systemctl', 'show', $service], outfunc => $parser);
516
517 return $res;
518}
519
cfdf6608
DM
520sub service_wait_stopped {
521 my ($timeout, $service_list) = @_;
522
523 my $starttime = time();
524
525 foreach my $service (@$service_list) {
526 PVE::Tools::run_command(['systemctl', 'stop', $service]);
527 }
528
529 while (1) {
530 my $wait = 0;
531
532 foreach my $service (@$service_list) {
533 my $ss = get_full_service_state($service);
534 my $state = $ss->{ActiveState} // 'unknown';
535
536 if ($state ne 'inactive') {
537 if ((time() - $starttime) > $timeout) {
538 syslog('err', "unable to stop services (got timeout)");
539 $wait = 0;
540 last;
541 }
542 $wait = 1;
543 }
544 }
545
546 last if !$wait;
547
548 sleep(1);
549 }
550}
551
3f85510e
DM
552sub service_cmd {
553 my ($service, $cmd) = @_;
554
555 die "unknown service command '$cmd'\n"
556 if $cmd !~ m/^(start|stop|restart|reload)$/;
557
558 if ($service eq 'pmgdaemon' || $service eq 'pmgproxy') {
559 if ($cmd eq 'restart') {
560 # OK
561 } else {
562 die "invalid service cmd '$service $cmd': ERROR";
563 }
564 }
565
ef07e4d0 566 $service = $service_aliases->{$service} // $service;
3f85510e
DM
567 PVE::Tools::run_command(['systemctl', $cmd, $service]);
568};
569
9f67f5b3
DM
570# this is also used to get the IP of the local node
571sub lookup_node_ip {
572 my ($nodename, $noerr) = @_;
573
574 my ($family, $packed_ip);
575
576 eval {
577 my @res = PVE::Tools::getaddrinfo_all($nodename);
578 $family = $res[0]->{family};
579 $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
580 };
581
582 if ($@) {
583 die "hostname lookup failed:\n$@" if !$noerr;
584 return undef;
585 }
586
587 my $ip = Socket::inet_ntop($family, $packed_ip);
588 if ($ip =~ m/^127\.|^::1$/) {
589 die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
590 return undef;
591 }
592
593 return wantarray ? ($ip, $family) : $ip;
594}
595
630d1ae3
DM
596sub run_postmap {
597 my ($filename) = @_;
598
599 # make sure the file exists (else postmap fails)
600 IO::File->new($filename, 'a', 0644);
601
78982d93
DM
602 my $age_src = -M $filename // 0;
603 my $age_dst = -M "$filename.db" // 10000000000;
604
605 # if not changed, do nothing
606 return if $age_src > $age_dst;
607
630d1ae3
DM
608 eval {
609 PVE::Tools::run_command(
610 ['/usr/sbin/postmap', $filename],
611 errmsg => "unable to update postfix table $filename");
612 };
613 my $err = $@;
614
615 warn $err if $err;
616}
617
d17c5265
DM
618sub clamav_dbstat {
619
620 my $res = [];
621
622 my $read_cvd_info = sub {
623 my ($dbname, $dbfile) = @_;
624
625 my $header;
626 my $fh = IO::File->new("<$dbfile");
627 if (!$fh) {
628 warn "cant open ClamAV Database $dbname ($dbfile) - $!\n";
629 return;
630 }
631 $fh->read($header, 512);
632 $fh->close();
633
634 ## ClamAV-VDB:16 Mar 2016 23-17 +0000:57:4218790:60:06386f34a16ebeea2733ab037f0536be:
635 if ($header =~ m/^(ClamAV-VDB):([^:]+):(\d+):(\d+):/) {
636 my ($ftype, $btime, $version, $nsigs) = ($1, $2, $3, $4);
637 push @$res, {
638 name => $dbname,
639 type => $ftype,
640 build_time => $btime,
641 version => $version,
642 nsigs => $nsigs,
643 };
644 } else {
645 warn "unable to parse ClamAV Database $dbname ($dbfile)\n";
646 }
647 };
648
649 # main database
650 my $filename = "/var/lib/clamav/main.inc/main.info";
651 $filename = "/var/lib/clamav/main.cvd" if ! -f $filename;
652
653 $read_cvd_info->('main', $filename) if -f $filename;
654
655 # daily database
656 $filename = "/var/lib/clamav/daily.inc/daily.info";
657 $filename = "/var/lib/clamav/daily.cvd" if ! -f $filename;
658 $filename = "/var/lib/clamav/daily.cld" if ! -f $filename;
659
660 $read_cvd_info->('daily', $filename) if -f $filename;
661
662 $filename = "/var/lib/clamav/bytecode.cvd";
663 $read_cvd_info->('bytecode', $filename) if -f $filename;
664
9860e592
DM
665 $filename = "/var/lib/clamav/safebrowsing.cvd";
666 $read_cvd_info->('safebrowsing', $filename) if -f $filename;
667
dabcd20e
DM
668 my $ss_dbs_fn = "/var/lib/clamav-unofficial-sigs/configs/ss-include-dbs.txt";
669 my $ss_dbs_files = {};
670 if (my $ssfh = IO::File->new("<${ss_dbs_fn}")) {
671 while (defined(my $line = <$ssfh>)) {
672 chomp $line;
673 $ss_dbs_files->{$line} = 1;
674 }
675 }
d17c5265
DM
676 my $last = 0;
677 my $nsigs = 0;
678 foreach $filename (</var/lib/clamav/*>) {
dabcd20e
DM
679 my $fn = basename($filename);
680 next if !$ss_dbs_files->{$fn};
681
d17c5265
DM
682 my $fh = IO::File->new("<$filename");
683 next if !defined($fh);
684 my $st = stat($fh);
685 next if !$st;
686 my $mtime = $st->mtime();
687 $last = $mtime if $mtime > $last;
688 while (defined(my $line = <$fh>)) { $nsigs++; }
689 }
690
691 if ($nsigs > 0) {
692 push @$res, {
9860e592 693 name => 'sanesecurity',
d17c5265 694 type => 'unofficial',
2d011fef 695 build_time => strftime("%d %b %Y %H-%M %z", localtime($last)),
d17c5265
DM
696 nsigs => $nsigs,
697 };
698 }
699
700 return $res;
701}
702
dff363d9
DM
703# RRD related code
704my $rrd_dir = "/var/lib/rrdcached/db";
705my $rrdcached_socket = "/var/run/rrdcached.sock";
706
707my $rrd_def_node = [
708 "DS:loadavg:GAUGE:120:0:U",
709 "DS:maxcpu:GAUGE:120:0:U",
710 "DS:cpu:GAUGE:120:0:U",
711 "DS:iowait:GAUGE:120:0:U",
712 "DS:memtotal:GAUGE:120:0:U",
713 "DS:memused:GAUGE:120:0:U",
714 "DS:swaptotal:GAUGE:120:0:U",
715 "DS:swapused:GAUGE:120:0:U",
716 "DS:roottotal:GAUGE:120:0:U",
717 "DS:rootused:GAUGE:120:0:U",
718 "DS:netin:DERIVE:120:0:U",
719 "DS:netout:DERIVE:120:0:U",
720
721 "RRA:AVERAGE:0.5:1:70", # 1 min avg - one hour
722 "RRA:AVERAGE:0.5:30:70", # 30 min avg - one day
723 "RRA:AVERAGE:0.5:180:70", # 3 hour avg - one week
724 "RRA:AVERAGE:0.5:720:70", # 12 hour avg - one month
725 "RRA:AVERAGE:0.5:10080:70", # 7 day avg - ony year
726
727 "RRA:MAX:0.5:1:70", # 1 min max - one hour
728 "RRA:MAX:0.5:30:70", # 30 min max - one day
729 "RRA:MAX:0.5:180:70", # 3 hour max - one week
730 "RRA:MAX:0.5:720:70", # 12 hour max - one month
731 "RRA:MAX:0.5:10080:70", # 7 day max - ony year
732];
733
734sub cond_create_rrd_file {
735 my ($filename, $rrddef) = @_;
736
737 return if -f $filename;
738
739 my @args = ($filename);
740
741 push @args, "--daemon" => "unix:${rrdcached_socket}"
742 if -S $rrdcached_socket;
743
744 push @args, '--step', 60;
745
746 push @args, @$rrddef;
747
748 # print "TEST: " . join(' ', @args) . "\n";
749
750 RRDs::create(@args);
751 my $err = RRDs::error;
752 die "RRD error: $err\n" if $err;
753}
754
755sub update_node_status_rrd {
756
757 my $filename = "$rrd_dir/pmg-node-v1.rrd";
758 cond_create_rrd_file($filename, $rrd_def_node);
759
760 my ($avg1, $avg5, $avg15) = PVE::ProcFSTools::read_loadavg();
761
762 my $stat = PVE::ProcFSTools::read_proc_stat();
763
764 my $netdev = PVE::ProcFSTools::read_proc_net_dev();
765
766 my ($uptime) = PVE::ProcFSTools::read_proc_uptime();
767
768 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
769
770 my $maxcpu = $cpuinfo->{cpus};
771
772 # traffic from/to physical interface cards
773 my $netin = 0;
774 my $netout = 0;
775 foreach my $dev (keys %$netdev) {
776 next if $dev !~ m/^eth\d+$/;
777 $netin += $netdev->{$dev}->{receive};
778 $netout += $netdev->{$dev}->{transmit};
779 }
780
781 my $meminfo = PVE::ProcFSTools::read_meminfo();
782
783 my $dinfo = df('/', 1); # output is bytes
784
785 my $ctime = time();
786
787 # everything not free is considered to be used
788 my $dused = $dinfo->{blocks} - $dinfo->{bfree};
789
790 my $data = "$ctime:$avg1:$maxcpu:$stat->{cpu}:$stat->{wait}:" .
791 "$meminfo->{memtotal}:$meminfo->{memused}:" .
792 "$meminfo->{swaptotal}:$meminfo->{swapused}:" .
793 "$dinfo->{blocks}:$dused:$netin:$netout";
794
795
796 my @args = ($filename);
797
798 push @args, "--daemon" => "unix:${rrdcached_socket}"
799 if -S $rrdcached_socket;
800
801 push @args, $data;
802
803 # print "TEST: " . join(' ', @args) . "\n";
804
805 RRDs::update(@args);
806 my $err = RRDs::error;
807 die "RRD error: $err\n" if $err;
808}
809
065b2986
DM
810sub create_rrd_data {
811 my ($rrdname, $timeframe, $cf) = @_;
812
813 my $rrd = "${rrd_dir}/$rrdname";
814
815 my $setup = {
816 hour => [ 60, 70 ],
817 day => [ 60*30, 70 ],
818 week => [ 60*180, 70 ],
819 month => [ 60*720, 70 ],
820 year => [ 60*10080, 70 ],
821 };
822
823 my ($reso, $count) = @{$setup->{$timeframe}};
824 my $ctime = $reso*int(time()/$reso);
825 my $req_start = $ctime - $reso*$count;
826
827 $cf = "AVERAGE" if !$cf;
828
829 my @args = (
830 "-s" => $req_start,
831 "-e" => $ctime - 1,
832 "-r" => $reso,
833 );
834
835 push @args, "--daemon" => "unix:${rrdcached_socket}"
836 if -S $rrdcached_socket;
837
838 my ($start, $step, $names, $data) = RRDs::fetch($rrd, $cf, @args);
839
840 my $err = RRDs::error;
841 die "RRD error: $err\n" if $err;
842
843 die "got wrong time resolution ($step != $reso)\n"
844 if $step != $reso;
845
846 my $res = [];
847 my $fields = scalar(@$names);
848 for my $line (@$data) {
849 my $entry = { 'time' => $start };
850 $start += $step;
851 for (my $i = 0; $i < $fields; $i++) {
852 my $name = $names->[$i];
853 if (defined(my $val = $line->[$i])) {
854 $entry->{$name} = $val;
855 } else {
856 # leave empty fields undefined
857 # maybe make this configurable?
858 }
859 }
860 push @$res, $entry;
861 }
862
863 return $res;
864}
865
8da6f9ec
DM
866sub decode_to_html {
867 my ($charset, $data) = @_;
868
869 my $res = $data;
870
871 eval { $res = encode_entities(decode($charset, $data)); };
872
873 return $res;
874}
875
46f9e9af
DM
876sub decode_rfc1522 {
877 my ($enc) = @_;
878
879 my $res = '';
880
881 return '' if !$enc;
882
883 eval {
884 foreach my $r (MIME::Words::decode_mimewords($enc)) {
885 my ($d, $cs) = @$r;
886 if ($d) {
887 if ($cs) {
888 $res .= decode($cs, $d);
889 } else {
890 $res .= $d;
891 }
892 }
893 }
894 };
895
896 $res = $enc if $@;
897
898 return $res;
899}
900
9a56f771
DM
901sub rfc1522_to_html {
902 my ($enc) = @_;
903
904 my $res = '';
905
906 return '' if !$enc;
907
908 eval {
909 foreach my $r (MIME::Words::decode_mimewords($enc)) {
910 my ($d, $cs) = @$r;
911 if ($d) {
912 if ($cs) {
913 $res .= encode_entities(decode($cs, $d));
914 } else {
915 $res .= encode_entities($d);
916 }
917 }
918 }
919 };
920
921 $res = $enc if $@;
922
923 return $res;
924}
925
9d26ab13
DM
926# RFC 2047 B-ENCODING http://rfc.net/rfc2047.html
927# (Q-Encoding is complex and error prone)
928sub bencode_header {
929 my $txt = shift;
930
931 my $CRLF = "\015\012";
932
933 # Nonprintables (controls + x7F + 8bit):
934 my $NONPRINT = "\\x00-\\x1F\\x7F-\\xFF";
935
936 # always use utf-8 (work with japanese character sets)
937 $txt = encode("UTF-8", $txt);
938
939 return $txt if $txt !~ /[$NONPRINT]/o;
940
941 my $res = '';
942
943 while ($txt =~ s/^(.{1,42})//sm) {
944 my $t = MIME::Words::encode_mimeword ($1, 'B', 'UTF-8');
945 $res .= $res ? "\015\012\t$t" : $t;
946 }
947
948 return $res;
949}
950
7d89adf4
DM
951sub load_sa_descriptions {
952
953 my @dirs = ('/usr/share/spamassassin',
954 '/usr/share/spamassassin-extra');
955
956 my $res = {};
957
958 my $parse_sa_file = sub {
959 my ($file) = @_;
960
961 open(my $fh,'<', $file);
962 return if !defined($fh);
963
964 while (defined(my $line = <$fh>)) {
965 if ($line =~ m/^describe\s+(\S+)\s+(.*)\s*$/) {
966 my ($name, $desc) = ($1, $2);
967 next if $res->{$name};
968 $res->{$name}->{desc} = $desc;
969 if ($desc =~ m|[\(\s](http:\/\/\S+\.[^\s\.\)]+\.[^\s\.\)]+)|i) {
970 $res->{$name}->{url} = $1;
971 }
972 }
973 }
974 close($fh);
975 };
976
977 foreach my $dir (@dirs) {
978 foreach my $file (<$dir/*.cf>) {
979 $parse_sa_file->($file);
980 }
981 }
982
983 return $res;
984}
985
8162c745
DM
986sub format_uptime {
987 my ($uptime) = @_;
988
989 my $days = int($uptime/86400);
990 $uptime -= $days*86400;
991
992 my $hours = int($uptime/3600);
993 $uptime -= $hours*3600;
994
995 my $mins = $uptime/60;
996
997 if ($days) {
998 my $ds = $days > 1 ? 'days' : 'day';
999 return sprintf "%d $ds %02d:%02d", $days, $hours, $mins;
1000 } else {
1001 return sprintf "%02d:%02d", $hours, $mins;
1002 }
1003}
1004
3dfe317a
DM
1005sub finalize_report {
1006 my ($tt, $template, $data, $mailfrom, $receiver, $debug) = @_;
1007
1008 my $html = '';
1009
1010 $tt->process($template, $data, \$html) ||
1011 die $tt->error() . "\n";
1012
1013 my $title;
1014 if ($html =~ m|^\s*<title>(.*)</title>|m) {
1015 $title = $1;
1016 } else {
1017 die "unable to extract template title\n";
1018 }
1019
1020 my $top = MIME::Entity->build(
1021 Type => "multipart/related",
1022 To => $data->{pmail},
1023 From => $mailfrom,
1024 Subject => bencode_header(decode_entities($title)));
1025
1026 $top->attach(
1027 Data => $html,
1028 Type => "text/html",
1029 Encoding => $debug ? 'binary' : 'quoted-printable');
1030
1031 if ($debug) {
1032 $top->print();
1033 return;
1034 }
1035 # we use an empty envelope sender (we dont want to receive NDRs)
1036 PMG::Utils::reinject_mail ($top, '', [$receiver], undef, $data->{fqdn});
1037}
1038
2f7031b7
DM
1039sub lookup_timespan {
1040 my ($timespan) = @_;
1041
1042 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
1043 my $daystart = timelocal(0, 0, 0, $mday, $mon, $year);
1044
1045 my $start;
1046 my $end;
1047
1048 if ($timespan eq 'today') {
1049 $start = $daystart;
1050 $end = $start + 86400;
1051 } elsif ($timespan eq 'yesterday') {
1052 $end = $daystart;
1053 $start = $end - 86400;
1054 } elsif ($timespan eq 'week') {
1055 $end = $daystart;
1056 $start = $end - 7*86400;
1057 } else {
1058 die "internal error";
1059 }
1060
1061 return ($start, $end);
1062}
1063
758c7b6b 10641;