]> git.proxmox.com Git - pmg-api.git/blobdiff - src/PMG/Utils.pm
utils: postgres_admin_cmd chdir to / before running
[pmg-api.git] / src / PMG / Utils.pm
index f9e07b669f28abf18c440ceb3e0a402816beb506..52701a3bfe2605aff63bc53c06424a7ee30d0eec 100644 (file)
@@ -2,6 +2,7 @@ package PMG::Utils;
 
 use strict;
 use warnings;
+use Cwd;
 use DBI;
 use Net::Cmd;
 use Net::SMTP;
@@ -10,6 +11,7 @@ use File::stat;
 use POSIX qw(strftime);
 use File::stat;
 use File::Basename;
+use MIME::Entity;
 use MIME::Words;
 use MIME::Parser;
 use Time::HiRes qw (gettimeofday);
@@ -27,6 +29,7 @@ use utf8;
 no utf8;
 
 use HTML::Entities;
+use JSON;
 
 use PVE::ProcFSTools;
 use PVE::Network;
@@ -36,6 +39,7 @@ use PVE::ProcFSTools;
 use PMG::AtomicFile;
 use PMG::MailQueue;
 use PMG::SMTPPrinter;
+use PMG::MIMEUtils;
 
 use base 'Exporter';
 
@@ -114,7 +118,7 @@ PVE::JSONSchema::register_standard_option('username', {
 PVE::JSONSchema::register_standard_option('pmg-email-address', {
     description => "Email Address (allow most characters).",
     type => 'string',
-    pattern => '(?:[^\s\/\\\@]+\@[^\s\/\\\@]+)',
+    pattern => '(?:[^\s\\\@]+\@[^\s\/\\\@]+)',
     maxLength => 512,
     minLength => 3,
 });
@@ -168,21 +172,20 @@ sub extract_filename {
 }
 
 sub remove_marks {
-    my ($entity, $add_id, $id) = @_;
+    my ($entity, $add_id) = @_;
 
-    $id //= 1;
+    my $id = 1;
 
-    foreach my $tag (grep {/^x-proxmox-tmp/i} $entity->head->tags) {
-       $entity->head->delete ($tag);
-    }
-
-    $entity->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
+    PMG::MIMEUtils::traverse_mime_parts($entity, sub {
+       my ($part) = @_;
+       foreach my $tag (grep {/^x-proxmox-tmp/i} $part->head->tags) {
+           $part->head->delete($tag);
+       }
 
-    foreach my $part ($entity->parts)  {
-       $id = remove_marks($part, $add_id, $id + 1);
-    }
+       $part->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
 
-    return $id;
+       $id++;
+    });
 }
 
 sub subst_values {
@@ -201,7 +204,7 @@ sub subst_values {
 }
 
 sub reinject_mail {
-    my ($entity, $sender, $targets, $xforward, $me, $nodsn) = @_;
+    my ($entity, $sender, $targets, $xforward, $me, $params) = @_;
 
     my $smtp;
     my $resid;
@@ -209,7 +212,7 @@ sub reinject_mail {
     my $resmess;
 
     eval {
-       my $smtp = Net::SMTP->new('127.0.0.1', Port => 10025, Hello => $me) ||
+       my $smtp = Net::SMTP->new('::FFFF:127.0.0.1', Port => 10025, Hello => $me) ||
            die "unable to connect to localhost at port 10025";
 
        if (defined($xforward)) {
@@ -242,15 +245,28 @@ sub reinject_mail {
            $mail_opts .= " SMTPUTF8" if $has_utf8_targets;
        }
 
+       if (defined($params->{mail})) {
+           my $mailparams = $params->{mail};
+           for my $p (keys %$mailparams) {
+               $mail_opts .= " $p=$mailparams->{$p}";
+           }
+       }
+
        if (!$smtp->_MAIL("FROM:" . $sender_addr . $mail_opts)) {
            syslog('err', "smtp error - got: %s %s", $smtp->code, scalar ($smtp->message));
            die "smtp from: ERROR";
        }
 
-       my $rcpt_opts = $nodsn ? " NOTIFY=NEVER" : "";
-
        foreach my $target (@$targets) {
            my $rcpt_addr;
+           my $rcpt_opts = '';
+           if (defined($params->{rcpt}->{$target})) {
+               my $rcptparams = $params->{rcpt}->{$target};
+               for my $p (keys %$rcptparams) {
+                   $rcpt_opts .= " $p=$rcptparams->{$p}";
+               }
+           }
+
            if (utf8::is_utf8($target)) {
                $rcpt_addr = encode('UTF-8', $smtp->_addr($target));
            } else {
@@ -708,12 +724,16 @@ sub find_local_network_for_ip {
 
 my $service_aliases = {
     'postfix' =>  'postfix@-',
-    'postgres' => 'postgresql@11-main',
 };
 
 sub lookup_real_service_name {
     my $alias = shift;
 
+    if ($alias eq 'postgres') {
+       my $pg_ver = get_pg_server_version();
+       return "postgresql\@${pg_ver}-main";
+    }
+
     return $service_aliases->{$alias} // $alias;
 }
 
@@ -729,7 +749,7 @@ sub get_full_service_state {
        }
     };
 
-    $service = $service_aliases->{$service} // $service;
+    $service = lookup_real_service_name($service);
     PVE::Tools::run_command(['systemctl', 'show', $service], outfunc => $parser);
 
     return $res;
@@ -777,43 +797,18 @@ sub service_cmd {
        if $cmd !~ m/^(start|stop|restart|reload|reload-or-restart)$/;
 
     if ($service eq 'pmgdaemon' || $service eq 'pmgproxy') {
-       die "invalid service cmd '$service $cmd': ERROR" if $cmd eq 'stop';
+       die "invalid service cmd '$service $cmd': refusing to stop essential service!\n"
+           if $cmd eq 'stop';
     } elsif ($service eq 'fetchmail') {
        # use restart instead of start - else it does not start 'exited' unit
        # after setting START_DAEMON=yes in /etc/default/fetchmail
        $cmd = 'restart' if $cmd eq 'start';
     }
 
-    $service = $service_aliases->{$service} // $service;
+    $service = lookup_real_service_name($service);
     PVE::Tools::run_command(['systemctl', $cmd, $service]);
 };
 
-# this is also used to get the IP of the local node
-sub lookup_node_ip {
-    my ($nodename, $noerr) = @_;
-
-    my ($family, $packed_ip);
-
-    eval {
-       my @res = PVE::Tools::getaddrinfo_all($nodename);
-       $family = $res[0]->{family};
-       $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
-    };
-
-    if ($@) {
-       die "hostname lookup failed:\n$@" if !$noerr;
-       return undef;
-    }
-
-    my $ip = Socket::inet_ntop($family, $packed_ip);
-    if ($ip =~ m/^127\.|^::1$/) {
-       die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
-       return undef;
-    }
-
-    return wantarray ? ($ip, $family) : $ip;
-}
-
 sub run_postmap {
     my ($filename) = @_;
 
@@ -821,7 +816,7 @@ sub run_postmap {
     IO::File->new($filename, 'a', 0644);
 
     my $mtime_src = (CORE::stat($filename))[9] //
-       die "unbale to read mtime of $filename\n";
+       die "unable to read mtime of $filename\n";
 
     my $mtime_dst = (CORE::stat("$filename.db"))[9] // 0;
 
@@ -848,7 +843,7 @@ sub clamav_dbstat {
         my $header;
        my $fh = IO::File->new("<$dbfile");
        if (!$fh) {
-           warn "cant open ClamAV Database $dbname ($dbfile) - $!\n";
+           warn "can't open ClamAV Database $dbname ($dbfile) - $!\n";
            return;
        }
        $fh->read($header, 512);
@@ -885,9 +880,6 @@ sub clamav_dbstat {
     $filename = "/var/lib/clamav/bytecode.cvd";
     $read_cvd_info->('bytecode', $filename) if -f $filename;
 
-    $filename = "/var/lib/clamav/safebrowsing.cvd";
-    $read_cvd_info->('safebrowsing', $filename) if -f $filename;
-
     my $ss_dbs_fn = "/var/lib/clamav-unofficial-sigs/configs/ss-include-dbs.txt";
     my $ss_dbs_files = {};
     if (my $ssfh = IO::File->new("<${ss_dbs_fn}")) {
@@ -1188,7 +1180,7 @@ sub load_sa_descriptions {
        return if !defined($fh);
 
        while (defined(my $line = <$fh>)) {
-           if ($line =~ m/^describe\s+(\S+)\s+(.*)\s*$/) {
+           if ($line =~ m/^(?:\s*)describe\s+(\S+)\s+(.*)\s*$/) {
                my ($name, $desc) = ($1, $2);
                next if $res->{$name};
                $res->{$name}->{desc} = $desc;
@@ -1260,7 +1252,7 @@ sub finalize_report {
        $top->print();
        return;
     }
-    # we use an empty envelope sender (we dont want to receive NDRs)
+    # we use an empty envelope sender (we don't want to receive NDRs)
     PMG::Utils::reinject_mail ($top, '', [$receiver], undef, $data->{fqdn});
 }
 
@@ -1306,23 +1298,23 @@ sub scan_journal_for_rbl_rejects {
     my $pregreet_count = 0;
 
     my $parser = sub {
-       my $line = shift;
+       my $log = decode_json(shift);
 
-       if ($line =~ m/^--\scursor:\s(\S+)$/) {
-           $rbl_scan_last_cursor = $1;
-           return;
-       }
+       $rbl_scan_last_cursor = $log->{__CURSOR} if defined($log->{__CURSOR});
+
+       my $message = $log->{MESSAGE};
+       return if !defined($message);
 
-       if ($line =~ m/\s$identifier\[\d+\]:\sNOQUEUE:\sreject:.*550 5.7.1 Service unavailable;/) {
+       if ($message =~ m/^NOQUEUE:\sreject:.*550 5.7.1 Service unavailable/) {
            $rbl_count++;
-       } elsif ($line =~ m/\s$identifier\[\d+\]:\sPREGREET\s\d+\safter\s/) {
+       } elsif ($message =~ m/^PREGREET\s\d+\safter\s/) {
            $pregreet_count++;
        }
     };
 
     # limit to last 5000 lines to avoid long delays
-    my $cmd = ['journalctl', '--show-cursor', '-o', 'short-unix', '--no-pager',
-              '--identifier', $identifier, '-n', 5000];
+    my $cmd = ['journalctl', '-o', 'json', '--output-fields', '__CURSOR,MESSAGE',
+       '--no-pager', '--identifier', $identifier, '-n', 5000];
 
     if (defined($rbl_scan_last_cursor)) {
        push @$cmd, "--after-cursor=${rbl_scan_last_cursor}";
@@ -1392,6 +1384,10 @@ sub postgres_admin_cmd {
     my $save_uid = POSIX::getuid();
     my $pg_uid = getpwnam('postgres') || die "getpwnam postgres failed\n";
 
+    #cd to / to prevent warnings on EPERM (e.g. when running in /root)
+    my $cwd = getcwd() || die "getcwd failed\n";
+    ($cwd) = ($cwd =~ m|^(/.*)$|); #untaint
+    chdir('/') || die "could not chdir to '/'\n";
     PVE::Tools::setresuid(-1, $pg_uid, -1) ||
        die "setresuid postgres ($pg_uid) failed - $!\n";
 
@@ -1399,6 +1395,8 @@ sub postgres_admin_cmd {
 
     PVE::Tools::setresuid(-1, $save_uid, -1) ||
        die "setresuid back failed - $!\n";
+
+    chdir("$cwd") || die "could not chdir back to $cwd\n";
 }
 
 sub get_pg_server_version {
@@ -1428,4 +1426,120 @@ sub get_pg_server_version {
     return $major_ver;
 }
 
+sub reload_smtp_filter {
+
+    my $pid_file = '/run/pmg-smtp-filter.pid';
+    my $pid = PVE::Tools::file_read_firstline($pid_file);
+
+    return 0 if !$pid;
+
+    return 0 if $pid !~ m/^(\d+)$/;
+    $pid = $1; # untaint
+
+    return kill (10, $pid); # send SIGUSR1
+}
+
+sub domain_regex {
+    my ($domains) = @_;
+
+    my @ra;
+    foreach my $d (@$domains) {
+       # skip domains with non-DNS name characters
+       next if $d =~ m/[^A-Za-z0-9\-\.]/;
+       if ($d =~ m/^\.(.*)$/) {
+           my $dom = $1;
+           $dom =~ s/\./\\\./g;
+           push @ra, $dom;
+           push @ra, "\.\*\\.$dom";
+       } else {
+           $d =~ s/\./\\\./g;
+           push @ra, $d;
+       }
+    }
+
+    my $re = join ('|', @ra);
+
+    my $regex = qr/\@($re)$/i;
+
+    return $regex;
+}
+
+sub read_sa_channel {
+    my ($filename) = @_;
+
+    my $content = PVE::Tools::file_get_contents($filename);
+    my $channel = {
+       filename => $filename,
+    };
+
+    ($channel->{keyid}) = ($content =~ /^KEYID=([a-fA-F0-9]+)$/m);
+    die "no KEYID in $filename!\n" if !defined($channel->{keyid});
+    ($channel->{channelurl}) = ($content =~ /^CHANNELURL=(.+)$/m);
+    die "no CHANNELURL in $filename!\n" if !defined($channel->{channelurl});
+    ($channel->{gpgkey}) = ($content =~ /(?:^|\n)(-----BEGIN PGP PUBLIC KEY BLOCK-----.+-----END PGP PUBLIC KEY BLOCK-----)(?:\n|$)/s);
+    die "no GPG public key in $filename!\n" if !defined($channel->{gpgkey});
+
+    return $channel;
+};
+
+sub local_spamassassin_channels {
+
+    my $res = [];
+
+    my $local_channel_dir = '/etc/mail/spamassassin/channel.d/';
+
+    PVE::Tools::dir_glob_foreach($local_channel_dir, '.*\.conf', sub {
+       my ($filename) = @_;
+       my $channel = read_sa_channel($local_channel_dir.$filename);
+       push(@$res, $channel);
+    });
+
+    return $res;
+}
+
+sub update_local_spamassassin_channels {
+    my ($verbose) = @_;
+    # import all configured channel's gpg-keys to sa-update's keyring
+    my $localchannels = PMG::Utils::local_spamassassin_channels();
+    for my $channel (@$localchannels) {
+       my $importcmd = ['sa-update', '--import', $channel->{filename}];
+       push @$importcmd, '-v' if $verbose;
+
+       print "Importing gpg key from $channel->{filename}\n" if $verbose;
+       PVE::Tools::run_command($importcmd);
+    }
+
+    my $fresh_updates = 0;
+
+    for my $channel (@$localchannels) {
+       my $cmd = ['sa-update', '--channel', $channel->{channelurl}, '--gpgkey', $channel->{keyid}];
+       push @$cmd, '-v' if $verbose;
+
+       print "Updating $channel->{channelurl}\n" if $verbose;
+       my $ret = PVE::Tools::run_command($cmd, noerr => 1);
+       die "updating $channel->{channelurl} failed - sa-update exited with $ret\n" if $ret >= 2;
+
+       $fresh_updates = 1 if $ret == 0;
+    }
+
+    return $fresh_updates
+}
+
+sub get_existing_object_id {
+    my ($dbh, $obj_id, $obj_type, $value) = @_;
+
+    my $sth = $dbh->prepare("SELECT id FROM Object WHERE ".
+       "Objectgroup_ID = ? AND ".
+       "ObjectType = ? AND ".
+       "Value = ?"
+    );
+    $sth->execute($obj_id, $obj_type, $value);
+
+    if (my $ref = $sth->fetchrow_hashref()) {
+       return $ref->{id};
+    }
+
+    return;
+}
+
 1;