#!/usr/local/cpanel/3rdparty/bin/perl

# cpanel - autofixer2/update_lets_encrypt_cabundles
#                                                  Copyright 2021 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

package autofixer2::update_lets_encrypt_cabundles;

use strict;
use warnings;

use Try::Tiny;

use Whostmgr::ACLS                    ();
use Cpanel::SSLCerts                  ();
use Cpanel::SSLInstall                ();
use Cpanel::Apache::TLS               ();
use Cpanel::Apache::TLS::Index        ();
use Cpanel::SSL::CABundleCache        ();
use Cpanel::SSLInstall::Service       ();
use Cpanel::SSL::Objects::Certificate ();
use Cpanel::SSL::Objects::CABundle    ();
use Cpanel::AcctUtils::DomainOwner    ();

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

exit run() unless caller;

sub run {

    return 0 unless supported_on_this_major( 94, 100 );    # Set this to the Min/Max we support this autofixer on.

    update_lets_encrypt_cabundles();

    return 0;                                              # exit(0);
}

# Do we run this code?
sub supported_on_this_major {
    my ( $min_ver, $max_ver ) = @_;

    my $major = get_major_version();

    return 0 if $major < $min_ver;
    return 0 if $major > $max_ver;

    return 1;
}

sub get_major_version {
    my $major_version;

    if ( open( my $fh, '<', '/usr/local/cpanel/version' ) ) {
        my $full_version = <$fh>;
        chomp $full_version;
        close($fh);
        ($major_version) = $full_version =~ /^[0-9]+\.([0-9]+)/;
    }

    # Safe default.
    return $major_version || 30;
}

sub update_lets_encrypt_cabundles {

    my @services = qw( dovecot exim ftp cpanel );
    for my $service (@services) {
        print "Checking '$service' service...";
        try {
            _do_service($service);
            print "Done\n";
        }
        catch {
            warn "Failed to check $service: $_";
        };
    }

    #Needed for Cpanel::SSLInstall to be happy.
    local $ENV{'REMOTE_USER'} = 'root';
    local %Whostmgr::ACLS::ACL;
    Whostmgr::ACLS::init_acls();

    my $apache_tls_idx = Cpanel::Apache::TLS::Index->new();
    my $all_records    = $apache_tls_idx->get_all_ar();
    my %vhost_record   = map { $_->{'vhost_name'} => $_ } @$all_records;

    foreach my $vhost_name ( sort keys %vhost_record ) {
        print "Checking '$vhost_name' vhost...";
        try {
            _update_vhost_if_needed($vhost_name);
            print "Done\n";
        }
        catch {
            warn "Failed to check vhost: $vhost_name: $_";
        };
    }
    system("/usr/local/cpanel/scripts/restartsrv_httpd");

    return;
}

sub _do_service {
    my ($service) = @_;

    my $ssldata = Cpanel::SSLCerts::fetchSSLFiles(
        service => $service,
    );

    my $cert_pem = $ssldata->{'crt'} or do {
        print "\tNo SSL installed.\n";
        return;
    };

    my $cert_obj = Cpanel::SSL::Objects::Certificate->new(
        cert => $cert_pem,
    );

    if ( $cert_obj->expired() ) {
        print "\tLeaf certificate is already expired.\n";
        return;
    }

    my @cab_pieces = split m[(?<=)-[\r\n]+(?=-)], $ssldata->{'cab'} // ();

    my $needs_replace;

    for my $cabcert_pem (@cab_pieces) {
        my $cabcert_obj = Cpanel::SSL::Objects::Certificate->new(
            cert => $cabcert_pem,
        );

        my $issuer_text = $cabcert_obj->issuer_text();

        if ( $issuer_text =~ m{isrg|dst|encrypt}i ) {
            $needs_replace = 1;
            print "\tCA certificate needs replacement.\n";
            last;
        }
    }

    if ($needs_replace) {
        my $caissuers_url = $cert_obj->caIssuers_url();

        if ($caissuers_url) {
            print "\tFetching CA bundle ...\n";

            my $cab_pem = Cpanel::SSL::CABundleCache->load($caissuers_url);

            print "\tReinstalling certificate with new CA bundle ...";

            my ( $ok, $why ) = Cpanel::SSLCerts::installSSLFiles(
                service => $service,
                crt     => $cert_pem,
                key     => $ssldata->{'key'},
                cab     => $cab_pem,
            );

            if ($ok) {
                print "\tInstallation succeeded! Restarting $service ...\n";

                Cpanel::SSLInstall::Service->new()->_restart_service($service);
            }
            else {
                print "\tFAILED: $why\n";
            }
        }
        else {
            print "\tNo caIssuers URL in leaf certificate!\n";
        }
    }
    else {
        print "\tNo replacement needed.\n";
    }

    return;
}

sub _update_vhost_if_needed {
    my ($vhost_name) = @_;

    # nobody is the user for unowned domains (Cpanel::SSLInstall::USER_FOR_UNOWNED_DOMAINS)
    # the constant was added in CPANEL-19457 - Thu Mar 29 14:02:17 2018 -0500 so it won't
    # be in older versions
    my $domainowner = Cpanel::AcctUtils::DomainOwner::getdomainowner( $vhost_name, { default => 'nobody' } );

    my ( $key, $crt, @cab ) = Cpanel::Apache::TLS->get_tls($vhost_name);
    return if !$key || !$crt || !@cab;

    my $cert = Cpanel::SSL::Objects::Certificate->new( 'cert' => $crt );

    # If the cert itself is expired do not update it
    # as we don't want to reinstall expired certs,
    # only certs with an expired cert in the chain.
    return if $cert->expired();

    my $cabundle = Cpanel::SSL::Objects::CABundle->new( 'cab' => join( "\n", @cab ) );
    my ( $ok, $ordered_ar ) = $cabundle->get_chain();

    # If a dst/isrg cert in the chain, do a reinstall
    my $replace = 0;
    foreach my $cert (@$ordered_ar) {
        if ( $cert->issuer_text() =~ m{isrg|dst|encrypt}i ) {
            $replace = 1;
            last;
        }
    }

    return if !$replace;

    print "\tUpdating cabundle for '$vhost_name'....";

    # A reinstall will fetch the latest CAB from
    # the caissuers so we don't need to pass in a
    # new cab since Let's Encrpyt has now updated the
    # caissuers one.
    my $install = Cpanel::SSLInstall::real_installssl(
        domain          => $vhost_name,
        key             => $key,
        crt             => $crt,
        installing_user => $domainowner,
    );

    print "$install->{message}.\n";

    die $install->{message} if !$install->{status};

    return;
}
