#!/usr/bin/perl -w
#
# A dummy SSL certificate validator helper that
# echos back all the SSL errors sent by Squid.
#

use warnings;
use strict;
use Getopt::Long;
use Pod::Usage;
use Crypt::OpenSSL::X509;
use FileHandle;
use POSIX qw(strftime);

my $debug = 0;
my $help = 0;

=pod

=head1 NAME

cert_valid.pl - A fake cert validation helper for Squid

=head1 SYNOPSIS

cert_valid.pl [-d | --debug] [-h | --help]

=over 8

=item  B<-h | --help>

brief help message

=item B<-d | --debug>

enable debug messages to stderr

=back

=head1 DESCRIPTION

Retrieves the SSL certificate error list from squid and echo back without any change.

=head1 COPYRIGHT

 * Copyright (C) 1996-2017 The Squid Software Foundation and contributors
 *
 * Squid software is distributed under GPLv2+ license and includes
 * contributions from numerous individuals and organizations.
 * Please see the COPYING and CONTRIBUTORS files for details.

(C) 2012 The Measurement Factory, Author: Tsantilas Christos

This program is free software. You may redistribute copies of it under the
terms of the GNU General Public License version 2, or (at your opinion) any
later version.

=cut

GetOptions(
    'help' => \$help,
    'debug' => \$debug,
    ) or pod2usage(1);

pod2usage(1) if ($help);

$|=1;
while (<>) {
    my $first_line = $_;
    my @line_args = split;

    if ($first_line =~ /^\s*$/) {
        next;
    }

    my $response;
    my $haserror = 0;
    my $channelId = $line_args[0];
    my $code = $line_args[1];
    my $bodylen = $line_args[2];
    my $body = $line_args[3] . "\n";
    if ($channelId !~ /\d+/) {
        $response = $channelId." BH message=\"This helper is  concurrent and requires the concurrency option to be specified.\"\1";
    } elsif ($bodylen !~ /\d+/) {
        $response = $channelId." BH message=\"cert validator request syntax error \" \1";
    } else {
        my $readlen = length($body);
        my %certs = ();
        my %errors = ();
        my @responseErrors = ();

        while($readlen < $bodylen) {
	    my $t = <>;
            if (defined $t) {
                $body  = $body . $t;
                $readlen = length($body);
            }
        }

        print(STDERR logPrefix()."GOT ". "Code=".$code." $bodylen \n") if ($debug); #.$body;
        my $hostname;
        my $sslVersion = "-";
        my $sslCipher = "-";
        parseRequest($body, \$hostname, \$sslVersion, \$sslCipher, \%errors, \%certs);
        print(STDERR logPrefix()."Parse result: \n") if ($debug);
        print(STDERR logPrefix()."\tFOUND host:".$hostname."\n") if ($debug);
        print(STDERR logPrefix()."\tFOUND ssl version:".$sslVersion."\n") if ($debug);
        print(STDERR logPrefix()."\tFOUND ssl cipher:".$sslCipher."\n") if ($debug);
        print(STDERR logPrefix()."\tFOUND ERRORS:") if ($debug);
        foreach my $err (keys %errors) {
            print(STDERR logPrefix().$errors{$err}{"name"}."/".$errors{$err}{"cert"}." ,")  if ($debug);
        }
        print(STDERR "\n") if ($debug);
        foreach my $key (keys %certs) {
            ## Use "perldoc Crypt::OpenSSL::X509" for X509 available methods.
            print(STDERR logPrefix()."\tFOUND cert ".$key.": ".$certs{$key}->subject() . "\n") if ($debug);
        }

        #got the peer certificate ID. Assume that the peer certificate is the first one.
        my $peerCertId = (keys %certs)[0];

        # Echo back the errors: fill the responseErrors array  with the errors we read.
        foreach my $err (keys %errors) {
            $haserror = 1;
            appendError (\@responseErrors, 
                         $errors{$err}{"name"}, #The error name
                         "Checked by Cert Validator", # An error reason
                         $errors{$err}{"cert"} # The cert ID. We are always filling with the peer certificate.
                );
        }

        $response = createResponse(\@responseErrors);
        my $len = length($response);
        if ($haserror) {
            $response = $channelId." ERR ".$len." ".$response."\1";
        } else {
            $response = $channelId." OK ".$len." ".$response."\1";
        }
    }

    print $response;
    print(STDERR logPrefix().">> ".$response."\n") if ($debug);
}

sub trim
{
    my $s = shift;
    $s =~ s/^\s+//;
    $s =~ s/\s+$//;
    return $s;
}

sub appendError
{
    my ($errorArrays) = shift;
    my($errorName) = shift;
    my($errorReason) = shift;
    my($errorCert) = shift;
    push @$errorArrays, { "error_name" => $errorName, "error_reason" => $errorReason, "error_cert" => $errorCert};
}

sub createResponse
{
    my ($responseErrors) = shift;
    my $response="";
    my $i = 0;
    foreach my $err (@$responseErrors) {
        $response=$response."error_name_".$i."=".$err->{"error_name"}."\n".
            "error_reason_".$i."=".$err->{"error_reason"}."\n".
            "error_cert_".$i."=".$err->{"error_cert"}."\n";
        $i++;
    }
    return $response;
}

sub parseRequest
{
    my($request)=shift;
    my $hostname = shift;
    my $sslVersion = shift;
    my $sslCipher = shift;
    my $errors = shift;
    my $certs = shift;
    while ($request !~ /^\s*$/) {
        $request = trim($request);
        if ($request =~ /^host=/) {
            my($vallen) = index($request, "\n");
            my $host = substr($request, 5, $vallen - 5);
            $$hostname = $host;
            $request =~ s/^host=.*$//m;
        }
        if ($request =~ s/^proto_version=(.*?)$//m) {
            $$sslVersion = $1;
        }
        if ($request =~ s/^cipher=(.*?)$//m) {
            $$sslCipher = $1;
        }
        if ($request =~ /^cert_(\d+)=/) {
            my $certId = "cert_".$1;
            my($vallen) = index($request, "-----END CERTIFICATE-----") + length("-----END CERTIFICATE-----");
            my $x509 = Crypt::OpenSSL::X509->new_from_string(substr($request, index($request, "-----BEGIN")));
            $certs->{$certId} = $x509;
            $request = substr($request, $vallen);
        }
        elsif ($request =~ /^error_name_(\d+)=(.*)$/m) {
            my $errorId = $1;
            my $errorName = $2;
            $request =~ s/^error_name_\d+=.*$//m;
            $errors->{$errorId}{"name"} = $errorName;
        }
        elsif ($request =~ /^error_cert_(\d+)=(.*)$/m) {
            my $errorId = $1;
            my $certId = $2;
            $request =~ s/^error_cert_\d+=.*$//m;
            $errors->{$errorId}{"cert"} = $certId;
        }
        else {
            print(STDERR logPrefix()."ParseError on \"".$request."\"\n") if ($debug);
            $request = "";# finish processing....
        }
    }
}


sub logPrefix
{
  return strftime("%Y/%m/%d %H:%M:%S.0", localtime)." ".$0." ".$$." | " ;
}
