\!/ KyuuKazami \!/

Path : /scripts/
Upload :
Current File : //scripts/fix-cpanel-perl

#!/usr/bin/perl

# cpanel - scripts/fix-cpanel-perl                 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

# This script was written to provide a means to recover from a catostropic loss
# of cpanel-perl RPMs so that you cannot run check_cpanel_rpms and/or upcp to get
# all your lost cPanel files back. It is automatically invoked if need-be if
# check_cpanel_rpms cannot run.

package scripts::fixcpanelperl;

use strict;
use warnings;

use IPC::Open3 ();
use POSIX      ();

my $COLOR_RED    = 31;
my $COLOR_YELLOW = 33;

our $CPANEL_CONFIG_FILE        = q[/var/cpanel/cpanel.config];
our $SIG_VALIDATION_CPCONF_KEY = 'signature_validation';

sub colorize_bold {
    my ( $color, $msg ) = @_;

    return $msg if !defined $color || -e q{/var/cpanel/disable_cpanel_terminal_colors};
    $msg ||= '';

    return chr(27) . '[1;' . $color . 'm' . $msg . chr(27) . '[0;m';
}

sub DEBUG($) { return _MSG( 'DEBUG', "  " . shift ) }                             ## no critic(ProhibitSubroutinePrototypes)
sub ERROR($) { return _MSG( 'ERROR', colorize_bold( $COLOR_RED, shift ) ) }       ## no critic(ProhibitSubroutinePrototypes)
sub WARN($)  { return _MSG( 'WARN',  colorize_bold( $COLOR_YELLOW, shift ) ) }    ## no critic(ProhibitSubroutinePrototypes)
sub INFO($)  { return _MSG( 'INFO',  shift ) }                                    ## no critic(ProhibitSubroutinePrototypes)
sub FATAL($) { _MSG( 'FATAL', colorize_bold( $COLOR_RED, shift ) ); die "\n"; }   ## no critic(ProhibitSubroutinePrototypes)

# Cached and used all over the place.
my ( $wget_bin, $wget_args,      $gpg_bin );
my ( $distro,   $distro_version, $distro_arch );
my %sha;

exit script() unless caller();

sub script {
    return 0 if cpanel_perl_is_stable();

    ERROR("Core cpanel-perl modules have been found to be corrupt. Attempting to correct this.") unless $ENV{CPANEL_BASE_INSTALL};

    ( $wget_bin, $wget_args ) = get_download_tool_binary();
    $gpg_bin = gpg_bin();
    ( $distro, $distro_version, $distro_arch ) = check_system_support();

    fetch_and_install_gpg_keys() unless $ENV{CPANEL_BASE_INSTALL_GPG_KEYS_IMPORTED};

    my ( $rpm_version_source, @rpms ) = rpms_to_download();
    my $rpm_downloads_dir = '/usr/local/cpanel/tmp/rpm_downloads';
    my $rpm_url_base      = "/RPM/$rpm_version_source/centos/$distro_version/x86_64";
    my $core_perl_rpm     = shift @rpms;

    get_rpm_sha512( $rpm_downloads_dir, $rpm_url_base, $rpm_version_source );

    chdir $rpm_downloads_dir;

    my $core_perl_pid;
    if ( $core_perl_pid = fork() ) {

    }
    else {
        wget_and_validate_file( $core_perl_rpm, $rpm_url_base, $rpm_downloads_dir );
        system( qw{/bin/rpm -Uvh --force}, $core_perl_rpm );
        exit(0);
    }

    foreach my $rpm_file (@rpms) {
        wget_and_validate_file( $rpm_file, $rpm_url_base, $rpm_downloads_dir );
    }

    {
        waitpid( $core_perl_pid, 0 );
        FATAL "Core perl RPM transaction failed" unless $? == 0;
    }

    system( qw{/bin/rpm -Uvh --force}, @rpms );

    FATAL "RPM transaction failed" unless $? == 0;

    # updatenow.static will do the needful during an install.
    return 0 if $ENV{CPANEL_BASE_INSTALL};

    if ( !-x '/usr/local/cpanel/scripts/check_cpanel_rpms' ) {
        FATAL "Unable to run scripts/check_cpanel_rpms. You will need to run updatenow.static and then re-run /usr/local/cpanel/scripts/check_cpanel_rpms --fix";
    }

    exec(qw{/usr/local/cpanel/scripts/check_cpanel_rpms --fix --long-list --no-digest})
      or FATAL "Failed to exec /usr/local/cpanel/scripts/check_cpanel_rpms --fix";

    return 255;
}

sub rpms_to_download {
    return "11.86",

      # The main perl rpm must always come first
      qw {
      cpanel-perl-530-5.30.0-4.cp1186.x86_64.rpm
      cpanel-perl-530-common-sense-3.74-1.cp1186.noarch.rpm
      cpanel-perl-530-CDB_File-0.99-1.cp1186.x86_64.rpm
      cpanel-perl-530-IO-SigGuard-0.14-1.cp1186.noarch.rpm
      cpanel-perl-530-JSON-XS-3.04-1.cp1186.x86_64.rpm
      cpanel-perl-530-Compress-Raw-Lzma-2.087-1.cp1186.x86_64.rpm
      cpanel-perl-530-Proc-FastSpawn-1.2-1.cp1186.x86_64.rpm
      cpanel-perl-530-Try-Tiny-0.30-1.cp1186.noarch.rpm
      cpanel-perl-530-Types-Serialiser-1.0-1.cp1186.noarch.rpm
      cpanel-perl-530-YAML-Syck-1.31-1.cp1186.x86_64.rpm
      cpanel-perl-530-Net-SSLeay-1.88-1.cp1186.x86_64.rpm
      cpanel-perl-530-IO-Socket-SSL-2.066-2.cp1186.noarch.rpm
    };
}

sub cpanel_perl_modules {

    # Throw out 11.70 and the first RPM (perl)
    my ( undef, undef, @rpms ) = rpms_to_download();

    my @modules;
    foreach my $rpm (@rpms) {
        $rpm =~ s/^cpanel-perl-530-//;
        $rpm =~ s/-\d.+$//;
        $rpm =~ s/-/::/g;
        push @modules, $rpm;
    }

    return @modules;
}

sub cpanel_perl_is_stable {

    my $command = '/usr/local/cpanel/3rdparty/bin/perl';

    $command .= " -M$_" foreach cpanel_perl_modules();

    my $got = `$command -E'print q{ok}' 2>&1`;
    return ( !$? && $got && $got eq 'ok' ) ? 1 : 0;
}

sub get_rpm_sha512 {
    my ( $rpm_downloads_dir, $rpm_url_base, $rpm_version_source ) = @_;

    # Setup the directory as best we can.
    unlink( '/usr/local/cpanel/tmp', $rpm_downloads_dir );
    mkdir '/usr/local/cpanel/tmp';
    mkdir $rpm_downloads_dir;
    -d $rpm_downloads_dir or FATAL("Can't make directory $rpm_downloads_dir ");

    my $sha_file = "$rpm_downloads_dir/rpm.$rpm_version_source.sha512";
    my $sig_file = "$rpm_downloads_dir/rpm.$rpm_version_source.sha512.asc";

    #
    wget_file( "$rpm_url_base/rpm.sha512.asc", $sig_file );
    wget_file( "$rpm_url_base/rpm.sha512",     $sha_file );
    verify_file_signature( $sha_file, $sig_file, "$rpm_url_base/rpm.sha512" );

    open( my $fh, '<', $sha_file ) or FATAL("Can't read $sha_file");
    while ( my $line = <$fh> ) {
        chomp $line;
        my ( $sha, $file ) = split( qr{\s+}, $line );

        $sha{$file} = $sha;
    }
    close $fh;

    return;
}

sub get_download_tool_binary {

    for my $bin (qw(/bin/wget /usr/bin/wget /usr/local/bin/wget)) {
        next                                                                                                                                   if ( !-e $bin );
        next                                                                                                                                   if ( !-x _ );
        next                                                                                                                                   if ( -z _ );
        return ( $bin, ' -nv --no-dns-cache --tries=20 --timeout=60 --dns-timeout=60 --read-timeout=30 --waitretry=1 --retry-connrefused -O' ) if ( `$bin --version` =~ m/GNU\s+Wget\s+\d+\.\d+/ims );
    }

    FATAL "Can't bootstrap cpanel-perl without wget. Try: yum -y install wget";
    return;
}

sub gpg_bin {

    for my $bin (qw(/bin/gpg /usr/bin/gpg /usr/local/bin/gpg)) {
        next if ( !-e $bin );
        next if ( !-x _ );
        next if ( -z _ );
        return $bin;
    }

    FATAL "Can't bootstrap cpanel-perl without gpg. Try: yum -y install gnupg2";
    return;
}

sub _MSG {
    my $level = shift;
    my $msg   = shift || '';
    chomp $msg;

    my $message_caller_depth = 1;

    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime;
    my ( $package, $filename, $line ) = caller($message_caller_depth);
    my $stamp_msg = sprintf( "%04d-%02d-%02d %02d:%02d:%02d %4s (%5s): %s\n", $year + 1900, $mon + 1, $mday, $hour, $min, $sec, $line, $level, $msg );

    print $stamp_msg;
    return;
}

sub get_update_source {
    my $update_source = 'httpupdate.cpanel.net';
    my $source_file   = '/etc/cpsources.conf';
    if ( -r $source_file && -s $source_file ) {    # pull in from cpsources.conf if it's set.
        open( my $fh, "<", $source_file ) or return $update_source;
        while (<$fh>) {
            next if ( $_ !~ m/^\s*HTTPUPDATE\s*=\s*(\S+)/ );
            $update_source = "$1";
            FATAL("HTTPUPDATE is set to '$update_source' in the $source_file file.") if ( !$update_source );
            last;
        }
    }

    return $update_source;
}

sub wget_and_validate_file {
    my ( $rpm_file, $rpm_url_base, $rpm_downloads_dir ) = @_;
    my $rpm_file_path = wget_file( "$rpm_url_base/$rpm_file", "$rpm_downloads_dir/$rpm_file" );

    my $result = `/usr/bin/sha512sum $rpm_file_path 2>&1`;
    my ($sha) = $result =~ m/^([a-fA-F0-9]+)/;

    if ( $sha{$rpm_file} ne $sha ) {
        FATAL("Couldn't verify the expected sha ($sha{$rpm_file}) for $rpm_file. Got $sha");
    }

    return;
}

sub wget_file {
    my ( $url, $dest_file ) = @_;

    $url = 'http://' . get_update_source() . $url;

    DEBUG "Retrieving $url";
    my $output = `$wget_bin $wget_args '$dest_file' $url 2>&1`;

    if ( !-e $dest_file || -z $dest_file ) {
        unlink $dest_file;
        FATAL "The system could not fetch the $dest_file file: $output";
    }

    return $dest_file;
}

sub signature_validation_enabled {

    my $config = read_config();

    return 1 unless defined $config->{$SIG_VALIDATION_CPCONF_KEY};
    return 0 if $config->{$SIG_VALIDATION_CPCONF_KEY} eq '0' || lc( $config->{$SIG_VALIDATION_CPCONF_KEY} ) eq 'off';

    return 1;
}

sub verify_file_signature {
    my ( $file, $sig, $url ) = @_;

    if ( !signature_validation_enabled() ) {
        INFO "Skipping signature validation [currently disabled in cpanel.config]";
        return;
    }

    INFO "FILE - $file";
    INFO "SIG  - $sig";
    INFO "URL  - $url";

    my @gpg_args = (
        '--logger-fd', '1',
        '--status-fd', '1',
        '--homedir',   gpg_homedir(),
        '--verify',    $sig,
        $file,
    );

    # Verify the validity of the GPG signature.
    # Information on these return values can be found in 'doc/DETAILS' in the GnuPG source.

    my ( %notes, $curnote );
    my ( $gpg_out, $success, $status );
    my $gpg_pid = IPC::Open3::open3( undef, $gpg_out, undef, $gpg_bin, @gpg_args );

    while ( my $line = readline($gpg_out) ) {
        if ( $line =~ /^\[GNUPG:\] VALIDSIG ([A-F0-9]+) (\d+-\d+-\d+) (\d+) ([A-F0-9]+) ([A-F0-9]+) ([A-F0-9]+) ([A-F0-9]+) ([A-F0-9]+) ([A-F0-9]+) ([A-F0-9]+)$/ ) {
            $status  = "Valid signature for $file";
            $success = 1;
        }
        elsif ( $line =~ /^\[GNUPG:\] NOTATION_NAME (.+)$/ ) {
            $curnote = $1;
            $notes{$curnote} = '';
        }
        elsif ( $line =~ /^\[GNUPG:\] NOTATION_DATA (.+)$/ ) {
            $notes{$curnote} .= $1;
        }
        elsif ( $line =~ /^\[GNUPG:\] BADSIG ([A-F0-9]+) (.+)$/ ) {
            $status = "Invalid signature for $file.";
        }
        elsif ( $line =~ /^\[GNUPG:\] NO_PUBKEY ([A-F0-9]+)$/ ) {
            $status = "Could not find public key in keychain.";
        }
        elsif ( $line =~ /^\[GNUPG:\] NODATA ([A-F0-9]+)$/ ) {
            $status = "Could not find a GnuPG signature in the signature file.";
        }
    }

    waitpid( $gpg_pid, 0 );

    $status ||= "Unknown error from gpg.";
    $status .= " ($file)";

    if ($success) {
        INFO $status;
    }
    else {
        FATAL $status;
    }

    # At this point, the signature should be valid.
    # We now need to check to see if the filename signature notation is correct.

    $url =~ s/\.bz2$//;

    if ( defined( $notes{'filename@gpg.notations.cpanel.net'} ) ) {
        my $file_note = $notes{'filename@gpg.notations.cpanel.net'};
        if ( $file_note ne $url ) {
            FATAL "Filename notation ($file_note) does not match URL ($url).";
        }
    }
    else {
        FATAL "Signature does not contain a filename notation.";
    }

    return;
}

sub fetch_and_install_gpg_keys {

    my $pub_keys = public_keys();
    _create_gpg_homedir();

    foreach my $key ( @{ keys_to_download() } ) {
        INFO("Downloading GPG public key, $pub_keys->{$key}");
        my $target   = secure_downloads() . $pub_keys->{$key};
        my $dest     = gpg_homedir() . "/" . $pub_keys->{$key};
        my $wget_cmd = $wget_args . " " . $dest . " " . $target;
        my $wget_out = `$wget_bin $wget_cmd 2>&1`;
        if ( !-e $dest ) {
            WARN("Could not download GPG public key at $target : $wget_out");
            return;
        }
        my $gpg_cmd = $gpg_bin . " -q --homedir " . gpg_homedir() . " --import " . $dest;
        `$gpg_cmd`;
    }
    return;
}

sub _create_gpg_homedir {
    mkdir( gpg_homedir(), 0700 ) if !-e gpg_homedir();
    return;
}

sub invalid_system {
    my $message = shift || '';
    chomp $message;
    ERROR "$message";

    ERROR "The system detected an unsupported distribution. cPanel & WHM only supports CentOS 6 and 7, Red Hat Enterprise Linux® 6 and 7, and CloudLinux™ 6 and 7.";
    FATAL "Please reinstall cPanel & WHM from a valid distribution.";

    return;    # Fatal will die.
}

sub get_distro_release_rpm {

    # /etc/redhat-release or /etc/system-release must be present
    my ( $rhel_release, $amazon_release ) = ( '/etc/redhat-release', '/etc/system-release' );

    my $distro_release;

    if ( -e $rhel_release ) {
        $distro_release = $rhel_release;
    }
    elsif ( -e $amazon_release ) {
        $distro_release = $amazon_release;
    }
    else {
        invalid_system("The system could not detect a valid release file for this distribution");
    }

    chomp( my $release_rpm = `rpm -qf $distro_release` );

    return $release_rpm;
}

sub _distro_name {
    my ( $distro, $full ) = @_;
    for my $names (
        [ 'centos', 'CentOS',       'CentOS' ],
        [ 'redhat', 'Red Hat',      'Red Hat Enterprise Linux®' ],
        [ 'cloud',  'CloudLinux',   'CloudLinux™' ],
        [ 'amazon', 'Amazon Linux', 'Amazon Linux' ],
    ) {
        return $names->[ $full ? 2 : 1 ] if $distro eq $names->[0];
    }
    return $distro;
}

sub check_system_support {

    # Some of these variables are unused *as of now*. However! some of these values may be useful for 'filling in the blanks' when the RPM check we do fails to provide all required info.
    # For now, only the $system and $machine variables are used, $machine only when Amazon Linux is detected. See https://metacpan.org/pod/POSIX#uname for more info.
    my ( $system, $nodename, $release, $version, $machine ) = POSIX::uname();

    if ( $system !~ m/linux/i ) {
        invalid_system("Could not detect version for operating system");
    }

    my $release_rpm = get_distro_release_rpm();

    $release_rpm or invalid_system("RPMs do not manage release file.");

    # an rpm we recognize must manage it.
    # We require a non capturing group while still supporting CloudLinux 5, otherwise this will fail to install as it does not include the arch.
    my ( $distro_type, $distro_version, $distro_arch ) = $release_rpm =~ m{^(\D+)-([\d\.]+).*?(?:\.([a-z0-9_]+))?$}ms;
    $distro_version = $distro_version + 0;
    $distro_version or invalid_system("The system found that the unexpected '$release_rpm' RPM manages the release file.");

    # This is required for CloudLinux 5 as they do not set an arch for their rpm. So we want to ignore it.
    $distro_arch ||= '';

    # That RPM must have redhat or centos in the name.
    $distro_type =~ m/centos|redhat|enterprise-release|system-release|cloud/i or invalid_system("The system found that the unexpected '$distro_type' RPM manages the release file.");
    $distro_type =~ s/-release//imsg;

    my $distro;

    if ( $distro_type eq 'enterprise' ) {
        $distro = 'redhat';
    }
    elsif ( $distro_type eq 'system' ) {
        $distro      = 'amazon';
        $distro_arch = $machine if $distro_arch eq 'noarch';    # SEE CPANEL-8050
    }
    else {
        $distro = $distro_type;
    }

    INFO _distro_name($distro) . " $distro_version (Linux) detected!";

    # Handle redhat/centos versioning
    if ( $distro ne 'amazon' ) {

        $distro_version =~ s/\.\d+//;                           # Strip off the decimal on Cloud Linux 7

        # The version number must be 6 or 7.
        ( int($distro_version) <= 7 && $distro_version >= 6 ) or invalid_system( "cPanel, L.L.C. does not support " . _distro_name($distro) . " version $distro_version." );

        # Supported distros for installer: redhat/red hat enterprise/cloud/centos/amazon
        $distro = ( $distro =~ m/redhat|hat enterprise/i ) ? 'redhat' : ( $distro =~ m/cloud/i ) ? 'cloud' : 'centos';
    }
    else {
        $distro_version = '6';                                  # Amazon Linux needs to act like CentOS 6 for RPMs.
    }

    return ( $distro, $distro_version, $distro_arch );
}

our $CACHE_CONFIG;

sub read_config {
    my $file = $CPANEL_CONFIG_FILE;

    return $CACHE_CONFIG if $CACHE_CONFIG;

    my $config = {};

    open( my $fh, "<", $file ) or return $config;
    while ( my $line = readline $fh ) {
        chomp $line;
        if ( $line =~ m/^\s*([^=]+?)\s*$/ ) {
            my $key = $1 or next;    # Skip loading the key if it's undef or 0
            $config->{$key} = undef;
        }
        elsif ( $line =~ m/^\s*([^=]+?)\s*=\s*(.*?)\s*$/ ) {
            my $key = $1 or next;    # Skip loading the key if it's undef or 0
            $config->{$key} = $2;
        }
    }

    $CACHE_CONFIG = $config;

    return $config;
}

sub keys_to_download {

    my $config   = read_config();
    my $keyrings = gpg_keyrings();

    if ( !defined $config->{'signature_validation'} ) {
        my $mirror = get_update_source();

        if ( $mirror =~ /^(?:.*\.dev|qa-build|next)\.cpanel\.net$/ ) {
            return $keyrings->{'development'};
        }
        else {
            return $keyrings->{'release'};
        }
    }
    elsif ( $config->{'signature_validation'} =~ /^Release and (?:Development|Test) Keyrings$/ ) {
        return $keyrings->{'development'};
    }
    else {
        return $keyrings->{'release'};
    }
}

# The installer may set $ENV{'CPANEL_BASE_INSTALL_GPG_KEYS_IMPORTED'}
# to true in which case the keys will be in /var/cpanel/.gpgtmpdir
sub gpg_homedir {
    return '/var/cpanel/.gpgtmpdir';
}

sub public_keys {
    return {
        'release'     => 'cPanelPublicKey.asc',
        'development' => 'cPanelDevelopmentKey.asc',
    };
}

sub secure_downloads {
    return 'https://securedownloads.cpanel.net/';
}

sub gpg_keyrings {
    return {
        'release'     => ['release'],
        'development' => [ 'release', 'development' ],

    };
}

1;

@KyuuKazami