#!/usr/bin/env perl
use strict;
use warnings;
use JSON::MaybeXS;
use LWP::UserAgent;
use HTTP::Request;
use MIME::Base64 qw(encode_base64);
use Date::Parse qw(str2time);
use POSIX qw(strftime);
use File::Spec;
use File::Basename;
use HTTP::Cookies;

# CONFIG
my $basedir     = dirname(__FILE__);
my $input_file  = File::Spec->catfile($basedir, 'web_list.json');
my $output_file = File::Spec->catfile($basedir, 'web_check.json');
my $false_html  = File::Spec->catfile($basedir, 'sld-check-false.html');
my $timeout     = 10;             # timeout secondi
my $openssl_timeout_bin = (-x '/usr/bin/timeout' ? '/usr/bin/timeout' :
                           -x '/bin/timeout'     ? '/bin/timeout'     : '');

# ===== helpers =====
sub read_json_file {
    my ($path) = @_;
    open my $fh, '<', $path or die "Cannot open $path: $!\n";
    local $/;
    my $json = <$fh>;
    close $fh;
    return decode_json($json);
}

sub write_json_file {
    my ($path, $data) = @_;
    open my $out, '>', $path or die "Cannot write $path: $!\n";
    print $out encode_json($data);
    close $out;
}

sub format_date_from_string {
    my ($raw) = @_;
    return undef unless defined $raw && $raw ne '';
    my $ts = str2time($raw);
    return undef unless $ts;
    return strftime("%d/%m/%Y", gmtime($ts));
}

# Run openssl s_client -> parse notBefore notAfter issuer
sub get_cert_info_openssl {
    my ($host, $timeout_sec) = @_;
    my $result = {
        status => 0,
        issued => undef,
        expiry => undef,
        company => undef,
        error => undef,
    };

    # escape host for shell
    my $esc_host = $host;
    $esc_host =~ s/'/'\\''/g; # basic escape if used in single quotes
    my $cmd = sprintf("openssl s_client -connect %s:443 -servername %s -showcerts </dev/null 2>/dev/null | openssl x509 -noout -dates -issuer", $host, $host);
    if ($openssl_timeout_bin) {
        $cmd = join(' ', $openssl_timeout_bin, $timeout_sec, $cmd);
    }

    my $out = qx{$cmd};
    my $exit = $? >> 8;

    if ($exit != 0 || $out eq '') {
        $result->{error} = "openssl failed or timed out (exit=$exit)";
        return $result;
    }

    my ($notBefore) = $out =~ /notBefore=(.+)/i;
    my ($notAfter)  = $out =~ /notAfter=(.+)/i;
    my ($issuer)    = $out =~ /issuer\s*=\s*(.+)/i;

    my $issued_fmt = defined $notBefore ? format_date_from_string($notBefore) : undef;
    my $expiry_fmt = defined $notAfter  ? format_date_from_string($notAfter)  : undef;

    # extract company from issuer: try O= then CN= else whole issuer
    my $company = undef;
    if (defined $issuer) {
        if ($issuer =~ /O=([^,\/]+)/) {
            $company = $1;
        } elsif ($issuer =~ /CN=([^,\/]+)/) {
            $company = $1;
        } else {
            $company = $issuer;
        }
        $company =~ s/^\s+|\s+$//g;
    }

    my $is_valid = 0;
    if ($expiry_fmt) {
        my $ts = str2time($notAfter);
        $is_valid = ($ts && time() < $ts) ? 1 : 0;
    }

    $result->{status}  = $is_valid;
    $result->{issued}  = $issued_fmt;
    $result->{expiry}  = $expiry_fmt;
    $result->{company} = $company;
    $result->{error}   = undef;
    return $result;
}

# build URL from domain + path; ensure https:// prefix
sub build_url {
    my ($domain, $path) = @_;
    $domain =~ s{^https?://}{}i;
    $domain =~ s{/$}{}; # remove trailing slash
    $path = '' unless defined $path;
    $path = '/' if $path eq '';
    $path = "/$path" unless $path =~ m{^/};
    return 'https://' . $domain . $path;
}

# basic sanitize host (remove scheme and path)
sub normalize_host {
    my ($domain) = @_;
    return undef unless defined $domain;
    $domain =~ s{^https?://}{}i;
    $domain =~ s{/.*$}{};     # remove path
    $domain =~ s/:\d+$//;    # remove port if any
    return $domain;
}

# ===== prepare LWP client =====
my $ua = LWP::UserAgent->new(timeout => $timeout);
# Browser-like user agent to avoid bot blocking
$ua->agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ".
           "(KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36");
$ua->default_header('Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
$ua->cookie_jar(HTTP::Cookies->new());    # maintain cookies
$ua->max_redirect(7);

# ===== main =====
die "Input file $input_file not found\n" unless -f $input_file;
my $domains = read_json_file($input_file);
die "Input JSON must be an array\n" unless ref($domains) eq 'ARRAY';

my @results;
my $any_fail = 0;
my $total_ssl_ok = 0;
my $total_content_ok = 0;

foreach my $entry (@$domains) {
    # read fields (tolleranza nomi)
    my $host = $entry->{host} // '';
    my $domain = $entry->{domain} // '';
    my $path = exists $entry->{path} ? $entry->{path} : '';
    my $word = $entry->{'word-to-check'} // '';
    my $basic_auth = $entry->{basic_auth} ? 1 : 0;
    my $user = $entry->{basic_auth_name} // undef;
    my $pass = $entry->{basic_auth_pass} // $entry->{basic_auth_password} // $entry->{basic_auth_passwd} // undef;

    # build url & ssl host
    my $url = build_url($domain, $path);
    my $ssl_host = normalize_host($domain) || $host || '';

    # get cert info (openssl s_client)
    my $ssl_info = get_cert_info_openssl($ssl_host, $timeout);

    # content check via LWP::UserAgent
    my $word_ok = 0;
    my $http_code = undef;
    my $content_err = undef;

    # prepare request
    my $req = HTTP::Request->new(GET => $url);
    # add headers that often avoid 403
    $req->header('Accept-Language' => 'en-US,en;q=0.9');

    if ($basic_auth && defined $user && defined $pass) {
        my $b64 = encode_base64("$user:$pass", '');
        $req->header('Authorization' => "Basic $b64");
    }

    my $res = eval { $ua->request($req) };
    if ($@ or !defined $res) {
        $content_err = $@ || 'request failed';
        $word_ok = 0;
    } else {
        $http_code = $res->code;
        if ($res->is_success) {
            my $body = $res->decoded_content(charset => 'none');
            $word_ok = (index($body, $word) >= 0) ? 1 : 0;
        } else {
            $content_err = "HTTP $http_code - " . ($res->message // '');
            $word_ok = 0;
        }
    }

    $total_ssl_ok++    if $ssl_info->{status} == 1;
    $total_content_ok++ if $word_ok == 1;
    $any_fail = 1 if ($ssl_info->{status} == 0 || $word_ok == 0);

    push @results, {
        host            => $host,
        domain          => $domain,
        path            => ($path eq '' ? '/' : $path),
        wordtocheck     => $word,
        wordcheck_ok    => $word_ok,
        sslcheck_ok     => $ssl_info->{status} // 0,
        'ssl-released'  => $ssl_info->{issued},
        'ssl-expiry'    => $ssl_info->{expiry},
        'ssl-company'   => $ssl_info->{company},
        # debug fields
        content_http_code => defined $http_code ? "$http_code" : undef,
        content_error     => $content_err,
        ssl_error         => $ssl_info->{error},
    };
}

# write output
open my $outfh, '>', $output_file or die "Cannot write $output_file: $!\n";
print $outfh encode_json(\@results);
close $outfh;

# manage sld-check-false.html
if ($any_fail) {
    if (open my $fh, '>', $false_html) {
        print $fh "<html><body><h1>WARNING: some checks failed</h1>\n";
        print $fh "<p>Generated by check_web_final.pl</p>\n";
        print $fh "</body></html>\n";
        close $fh;
    } else {
        warn "Cannot write $false_html: $!\n";
    }
} else {
    unlink $false_html if -f $false_html;
}

# summary
print "Check completato. Risultati salvati in $output_file\n";
print "Totali: SSL ok = $total_ssl_ok, Content ok = $total_content_ok\n";

exit 0;
