package Cpanel::UpdateGatherer::modules::Certificates;

#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

use Cpanel::UpdateGatherer::Std;

use Cpanel::JSON              ();
use Cpanel::Themes::Fallback  ();
use Whostmgr::Resellers::List ();
use Cpanel::YAML              ();
use Cpanel::SSL::Utils        ();
use Cpanel::SSLStorage::User  ();
use Cpanel::Template::Plugin::Apache();
use Whostmgr::SSL ();
use Net::SSLeay   ();

=head1 NAME

Cpanel::UpdateGatherer::modules::Certificates

=head1 SYNOPSIS

    use Cpanel::UpdateGatherer::modules::Certificates ();

    my $meta = {};
    Cpanel::UpdateGatherer::modules::Certificates->compile($meta)

=head1 DESCRIPTION

Module for collecting certificate metrics from server.

=head1 RETURNS

If the function returns successfully, entry for certificates should
have structure similar to

    {
        'certificates' => {
                "certificate_id": "d34db33f",
                "le_org_name": 0,
                "ocsp_status": 0,
                "affected": 0
            }
        }
    }

=cut

sub compile ( $self, $meta ) {
    _add_ocsp_stats($meta);
    return;
}

sub _add_ocsp_stats ($meta) {
    my @ocsp_stats;

    my $domains = Cpanel::Config::LoadUserDomains::loaduserdomains( {}, 0, 1 );
    foreach my $user ( keys %$domains ) {
        foreach my $domain ( @{ $domains->{$user} } ) {

            my %domain_data = (
                'domain' => $domain,
                'ip'     => Cpanel::DomainIp::getdomainip($domain),
            );

            # List all certs for the user
            my ( $ok, $sslstorage, $certs ) = ( 0, undef, undef );
            ( $ok, $sslstorage ) = Cpanel::SSLStorage::User->new( user => $user );
            next unless $ok;

            ( $ok, $certs ) = $sslstorage->find_certificates();
            next unless $ok;

            foreach my $cert (@$certs) {

                # Only check certs that cover our domain
                next unless grep { $_ eq $domain } @{ $cert->{domains} || [] };

                my $cert_id     = $cert->{id};
                my $cert_serial = $cert->{serial};
                my $cert_path   = $sslstorage->get_certificate_path($cert_id);

                next unless $cert_path && -e $cert_path;

                open my $fh, '<', $cert_path or next;
                my $cert_text = do { local $/; <$fh> };
                close $fh;

                # skip unless currently valid
                my $cert_obj;
                eval { $cert_obj = Cpanel::SSL::Objects::Certificate::File->new( 'path' => $cert_path ); };
                if ($@) {
                    next;
                }
                next unless $cert_obj->valid_for_domain($domain);
                undef $cert_obj;
                my $has_ocsp = Cpanel::Template::Plugin::Apache::certificate_has_ocsp($cert_path) // 0;
                my ( $ocsp_uri, $has_must_staple ) = _check_extensions($cert_path);

                my $parse_ok;
                my $cert_info;
                ( $parse_ok, $cert_info ) = Cpanel::SSL::Utils::parse_certificate_text($cert_text);

                next unless $parse_ok;
                my $issuer_orgName = 'unknown';

                eval {
                    if ( defined $cert_info->{'issuer_list'} ) {
                        foreach my $issuer ( @{ $cert_info->{'issuer_list'} } ) {
                            ( my $k, my $v ) = @{$issuer};
                            if ( $k eq 'organizationName' ) {
                                $issuer_orgName = $v;
                                last;
                            }
                        }
                    }
                };

                my %cert_data = (
                    'certificate_id' => $cert_serial,
                    'ocsp_status'    => $has_ocsp // '',
                    'le_org_name'    => ( $issuer_orgName        =~ m/Let.+Encrypt/i )                     ? 1 : 0,
                    'affected'       => ( $has_ocsp && $ocsp_uri =~ m/\.lencr\.org/i && $has_must_staple ) ? 1 : 0,
                    'must_staple'    => $has_must_staple,
                );
                push( @ocsp_stats, \%cert_data );
            }
        }
    }
    $meta->{'certificates'} = \@ocsp_stats;
    return 1;
}

sub _check_extensions ($cert_path) {

    # Read certificate from file
    my $pem_data = do {
        open my $fh, '<', $cert_path or return ( '', '' );
        local $/;
        <$fh>;
    };

    # Load the certificate
    my $bio = Net::SSLeay::BIO_new( Net::SSLeay::BIO_s_mem() );
    Net::SSLeay::BIO_write( $bio, $pem_data );
    my $x509 = Net::SSLeay::PEM_read_bio_X509($bio)
      or return ( '', '' );

    # Find the Authority Information Access extension
    my $ext_count       = Net::SSLeay::X509_get_ext_count($x509);
    my $ocsp_uri        = '';
    my $has_must_staple = 0;

    for my $i ( 0 .. $ext_count - 1 ) {
        my $ext = Net::SSLeay::X509_get_ext( $x509, $i );
        my $obj = Net::SSLeay::X509_EXTENSION_get_object($ext);
        my $nid = Net::SSLeay::OBJ_obj2nid($obj);
        my $sn  = Net::SSLeay::OBJ_nid2sn($nid);

        if ( $sn eq 'authorityInfoAccess' ) {
            my $ext_str = Net::SSLeay::X509V3_EXT_print($ext);

            # Look for an OCSP URI inside stringified extension
            if ( $ext_str =~ m{\bOCSP - URI:(\S+)}i ) {
                $ocsp_uri = $1;
            }
        }
        elsif ( $sn eq 'tlsfeature' || $sn eq 'TLS Feature' ) {
            $has_must_staple = 1;
        }
    }

    return ( $ocsp_uri, $has_must_staple );
}

1;
