#!/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 | 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 "

WARNING: some checks failed

\n"; print $fh "

Generated by check_web_final.pl

\n"; print $fh "\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;