#!/usr/bin/env perl # # tls_server_client_test.pl # # Starts `openssl s_server` in the background (safe under GitHub Actions: # strips RUNNER_TRACKING_ID so the runner's cleanup sweep won't reap it, # and redirects stdin from /dev/null with -ign_eof so s_server doesn't # quit after the first connection), waits until it's actually accepting # connections, then drives one or more `openssl s_client` checks against # it. Server is always killed on the way out, success or failure. # # Host, port, and the self-signed cert/key are all generated/fixed here # rather than taken from the command line. # # Usage: # ./tls_server_client_test.pl # use strict; use warnings; use IPC::Run3; use File::Temp qw(tempdir); use constant { HOST => '127.0.0.1', PORT => 4433, }; my $LOG = 's_server.' . PORT . '.log'; # tempdir with CLEANUP => 1 removes itself (and the cert/key inside it) # when the script exits, normally or via die/signal. my $CERT_DIR = tempdir(CLEANUP => 1); my $CERT = "$CERT_DIR/cert.pem"; my $KEY = "$CERT_DIR/key.pem"; generate_cert($CERT, $KEY); print "generated self-signed cert/key in $CERT_DIR\n"; # --------------------------------------------------------------------- # Start the server # --------------------------------------------------------------------- my $server_pid = start_server(HOST, PORT, $CERT, $KEY, $LOG); print "started s_server pid=$server_pid on " . HOST . ":" . PORT . " (log: $LOG)\n"; # Make sure we clean up on normal exit *and* on die()/signal. my $cleaned_up = 0; local $SIG{__DIE__} = sub { cleanup($server_pid) unless $cleaned_up; }; local $SIG{INT} = sub { cleanup($server_pid) unless $cleaned_up; exit 1; }; local $SIG{TERM} = sub { cleanup($server_pid) unless $cleaned_up; exit 1; }; my $exit_code = 0; eval { wait_for_ready(HOST, PORT, 20, 0.5); run_client_checks(HOST, PORT); 1; } or do { warn "test failed: $@"; $exit_code = 1; }; cleanup($server_pid); exit $exit_code; # --------------------------------------------------------------------- sub generate_cert { my ($cert, $key) = @_; my ($out, $err); run3( ['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', $key, '-out', $cert, '-days', '1', '-subj', '/CN=localhost', '-addext', 'subjectAltName=IP:' . HOST], \undef, \$out, \$err, ); die "cert generation failed:\n$err\n" if $? != 0; } sub start_server { my ($host, $port, $cert, $key, $log) = @_; my $pid = fork(); die "fork failed: $!" unless defined $pid; if ($pid == 0) { # --- child: becomes openssl s_server --- # Strip the runner's tracking env var so the Actions runner's # process-cleanup sweep (which greps for it) doesn't kill us # the moment this step/job ends. delete local $ENV{RUNNER_TRACKING_ID}; # Detach stdin from whatever the parent had (important: s_server # treats stdin EOF as "quit after this connection" unless told # otherwise). -ign_eof is a second belt-and-suspenders layer. open(STDIN, '<', '/dev/null') or die "reopen STDIN: $!"; open(STDOUT, '>', $log) or die "reopen STDOUT: $!"; open(STDERR, '>&STDOUT') or die "reopen STDERR: $!"; exec('openssl', 's_server', '-accept', "$host:$port", '-cert', $cert, '-key', $key, '-ign_eof', '-quiet'); # exec only returns on failure die "exec openssl s_server failed: $!"; } return $pid; } sub wait_for_ready { my ($host, $port, $tries, $delay) = @_; for my $i (1 .. $tries) { my ($out, $err); # Empty stdin: just probe the handshake, don't send app data. run3( ['openssl', 's_client', '-connect', "$host:$port"], \'', \$out, \$err, ); return 1 if $? == 0; select(undef, undef, undef, $delay); # fractional sleep } die "server on $host:$port never became ready after $tries attempts\n"; } sub run_client_checks { my ($host, $port) = @_; # Replace this with your real test(s). Shown here: a basic connect # that feeds a line of data through and checks openssl's exit code # plus captured output. Swap in Test::More ok()/is() calls as needed. my ($out, $err); run3( ['testssl.sh', '-p', "$host:$port"], \"", \$out, \$err, ); die "s_client exited non-zero: $?\nstderr:\n$err\n" if $? != 0; print "client check ok, output:\n$out\n"; } sub cleanup { my ($pid) = @_; return unless $pid; $cleaned_up = 1; if (kill(0, $pid)) { # still alive? kill('TERM', $pid); # give it a moment, then force it for (1 .. 10) { last unless kill(0, $pid); select(undef, undef, undef, 0.2); } kill('KILL', $pid) if kill(0, $pid); } waitpid($pid, 0); print "cleaned up s_server pid=$pid\n"; }