#!/usr/local/cpanel/3rdparty/bin/perl
# cpanel - autofixer2/exim_cve_workaround          Copyright 2019 cPanel, L.L.C.
#                                                           All rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use strict;
use warnings;

BEGIN { unshift @INC, '/usr/local/cpanel'; }

package autofixer2::exim_cve_workaround;

use List::Util qw(max);
use Cpanel::LoadFile              ();
use Cpanel::Version::Compare::RPM ();

our $ACL_ROOT = '/usr/local/cpanel/etc/exim/acls';

our %ACLS = (
    'ACL_OUTGOING_NOTSMTP_CHECKALL_BLOCK' => {
        'data' => ACL_OUTGOING_NOTSMTP_CHECKALL_BLOCK(),
        'path' => "$ACL_ROOT/ACL_OUTGOING_NOTSMTP_CHECKALL_BLOCK/000_restricted_chars",
    },
    'ACL_PRE_RECIPIENT_BLOCK' => {
        'data' => ACL_PRE_RECIPIENT_BLOCK(),
        'path' => "$ACL_ROOT/ACL_PRE_RECIPIENT_BLOCK/000_restricted_chars",
    },
);

our $EXIM_LOCALOPTS_PATH = '/etc/exim.conf.localopts';

exit( run() // 0 ) unless caller;

sub run {
    my $rpm_version = get_exim_rpm_version();
    if ( !has_workaround() && exim_is_vulnerable($rpm_version) ) {
        apply_acl_workaround();
    }
    elsif ( !exim_is_vulnerable($rpm_version) && has_workaround() ) {
        remove_acl_workaround();
    }

    return 0;
}

sub acl_paths {
    return map { $_->{'path'} } values %ACLS;
}

sub has_workaround {
    return ( grep { -e $_ } acl_paths() ) ? 1 : 0;
}

sub get_exim_rpm_version {
    local $SIG{'ALRM'} = sub { die 'rpm timed out while querying the exim version' };
    alarm 1000;

    my @exim_rpms = _run('/bin/rpm -q --queryformat "%{BUILDTIME} %{VERSION}-%{RELEASE}" exim');

    my %rpm_version_by_buildtime;
    for my $rpm_data (@exim_rpms) {
        my ( $build_time, $rpm_version ) = split q{ }, $rpm_data;
        chomp $rpm_version;
        $rpm_version_by_buildtime{$build_time} = $rpm_version;
    }

    my @build_times = sort { $a <=> $b } keys %rpm_version_by_buildtime;
    my $newest_buildtime = pop @build_times;

    return $rpm_version_by_buildtime{$newest_buildtime};
}

sub exim_is_vulnerable {
    my $rpm_version = shift;

    return 1 if ( Cpanel::Version::Compare::RPM::version_cmp( $rpm_version, q{4.87-1} ) >= 0
        && Cpanel::Version::Compare::RPM::version_cmp( $rpm_version, q{4.91-4} ) < 0 );

    return 0;
}

sub apply_acl_workaround {
    create_acl_files();
    update_exim_localopts();
    rebuild_conf_and_restart_exim();
    return;
}

sub rebuild_conf_and_restart_exim {
    system '/usr/local/cpanel/scripts/buildeximconf';
    system '/usr/local/cpanel/scripts/restartsrv_exim';
    return;
}

sub remove_acl_workaround {
    my $removed = 0;
    for my $acl_file ( acl_paths() ) {
        if ( -e $acl_file ) {
            $removed++ if unlink($acl_file);
        }
    }

    # Note this will leave acl_000_restricted_chars in /etc/exim.conf.localopts
    # but since there are no acl files in place no inserts will be made in
    # exim.conf when it is rebuilt.  We do this to minimize the risk of writing
    # a rewriting exim.conf.localopts since its possible the system could be out
    # of disk space which would leave exim in a broken state.
    if ($removed) {
        rebuild_conf_and_restart_exim();
    }

    return $removed;
}

sub update_exim_localopts {

    # Avoid dupes
    return if Cpanel::LoadFile::loadfile($EXIM_LOCALOPTS_PATH) =~ m{^\s*acl_000_restricted_chars}m;

    # For safety we only append to /etc/exim.conf.localopts since
    # its possible the system could be out of disk space which
    # would leave exim in a broken state.

    if ( open( my $opts_fh, '>>', $EXIM_LOCALOPTS_PATH ) ) {
        print {$opts_fh} qq{acl_000_restricted_chars=1\n};
        close($opts_fh);
    }

    return;
}

sub create_acl_files {
    for my $acl ( values %ACLS ) {
        open my $fh, '>', $acl->{'path'} or do {
            warn "Unable to open $acl->{'path'} for writing: $!";
            next;
        };
        print {$fh} $acl->{'data'};
        close $fh;
    }

    return;
}

sub _run {
    my $cmd = shift;
    return `$cmd`;
}

sub ACL_OUTGOING_NOTSMTP_CHECKALL_BLOCK {
    return <<'EOM';
deny
    condition = ${if lt {$exim_version}{4.92}{1}{0}}
    condition = ${if forany{<, $recipients}{match_local_part{$item}{\N^.*\$\{.*$\N}}{yes}{no}}
    message = restricted characters in recipient address

deny
    condition = ${if lt {$exim_version}{4.92}{1}{0}}
    condition = ${if match{$sender_address_local_part}{\N^.*\$\{.*$\N}{yes}{no}}
    message = restricted characters in sender address
EOM
}

sub ACL_PRE_RECIPIENT_BLOCK {
    return <<'EOM';
deny
    condition = ${if lt {$exim_version}{4.92}{1}{0}}
    local_parts = \N^.*\$\{.*$\N
    message = restricted characters in recipient address

deny
    condition = ${if lt {$exim_version}{4.92}{1}{0}}
    senders = \N^.*\$\{.*$\N
    message = restricted characters in sender address
EOM
}

