#!/usr/bin/perl -w
#
# Tests for krenew daemon functionality.
#
# Written by Russ Allbery <eagle@eyrie.org>
# Copyright 2021 Russ Allbery <eagle@eyrie.org>
# Copyright 2008-2009, 2011-2012, 2014
#     The Board of Trustees of the Leland Stanford Junior University
#
# SPDX-License-Identifier: MIT

use Cwd;

use Test::More;

# The full path to the newly-built krenew client.
our $KRENEW = "$ENV{C_TAP_BUILD}/../commands/krenew";

# The path to our data directory, which contains the keytab to use to test.
our $DATA = "$ENV{C_TAP_BUILD}/data";

# The path to our temporary directory used for test ticket caches and the
# like.
our $TMP = "$ENV{C_TAP_BUILD}/tmp";
unless (-d $TMP) {
    mkdir $TMP or BAIL_OUT ("cannot create $TMP: $!");
}

# Load our test utility programs.
require "$ENV{C_TAP_SOURCE}/libtest.pl";

# Decide whether we have the configuration to run the tests.
my ($cwd, $principal);
if (not -f "$DATA/test.keytab" or not -f "$DATA/test.principal") {
    plan skip_all => 'no keytab configuration';
    exit 0;
} else {
    $principal = contents ("$DATA/test.principal");
    $ENV{KRB5CCNAME} = "$TMP/krb5cc_test";
    unlink "$TMP/krb5cc_test";
    unless (kinit ("$DATA/test.keytab", $principal, '-r', '2h', '-l', '10m')) {
        plan skip_all => 'cannot get renewable tickets';
        exit 0;
    }
    plan tests => 94;
}

# Start a krenew daemon and be sure it gets tickets and stays running.
my $pid = fork;
if (!defined $pid) {
    BAIL_OUT ("can't fork: $!");
} elsif ($pid == 0) {
    open (STDERR, '>', "$TMP/krenew-errors")
        or BAIL_OUT ("can't create $TMP/krenew-errors: $!");
    exec ($KRENEW, '-K', 30, '-p', "$TMP/pid")
        or BAIL_OUT ("can't run $KRENEW: $!");
}
my $tries = 0;
while (not -s "$TMP/pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
if (-f "$TMP/pid") {
    my $daemon = contents ("$TMP/pid");
    is ($pid, $daemon, 'The right PID is written');
} else {
    ok (0, 'The right PID is written');
}
ok (kill (0, $pid), ' and krenew is still running');
unlink "$TMP/krb5cc_test";
kill (14, $pid) or warn "Can't kill $pid: $!\n";
is (waitpid ($pid, 0), $pid, ' and it dies after failure to renew');
is (($? >> 8), 1, ' with non-zero exit status');
if (open (ERRORS, '<', "$TMP/krenew-errors")) {
    like (scalar (<ERRORS>), qr/^krenew: error reading ticket cache: /,
          ' and the correct error message');
} else {
    ok (0, ' and the correct error message');
}
unlink "$TMP/krenew-errors";
ok (!-f "$TMP/pid", ' and the PID file was removed');
kinit ("$DATA/test.keytab", $principal, '-r', '2h', '-l', '10m');

# Try again with the -b flag.
my ($out, $err, $status)
    = command ($KRENEW, '-bK', 30, '-p', "$TMP/pid");
is ($status, 0, 'Backgrounding krenew works');
is ($err, '', ' with no error output');
is ($out, '', ' and -q was added implicitly');
$tries = 0;
while (not -s "$TMP/pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), ' and the PID file is correct');
kill (15, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.5);
ok (!kill (0, $pid), ' and it dies after SIGTERM');
ok (!-f "$TMP/pid", ' and the PID file was removed');

# Now try with -i.  In this case, krenew should keep running even if the
# ticket cache disappears and be able to start refreshing it again when it
# reappears.
($out, $err, $status) = command ($KRENEW, '-biK', 30, '-p', "$TMP/pid");
is ($status, 0, 'Backgrounding krenew works');
is ($err, '', ' with no error output');
is ($out, '', ' and -q was added implicitly');
$tries = 0;
while (not -s "$TMP/pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), ' and the PID file is correct');
unlink "$TMP/krb5cc_test";
kill (14, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.5);
ok (kill (0, $pid), ' and it keeps running after failure to renew');
kinit ("$DATA/test.keytab", $principal, '-r', '2h', '-l', '10m');
my $time = (stat "$TMP/krb5cc_test")[9];
while (time == $time) {
    select (undef, undef, undef, 0.1);
}
is ($time, (stat "$TMP/krb5cc_test")[9], 'Cache has not been touched');
kill (14, $pid) or warn "Can't kill $pid: $!\n";
$tries = 0;
while ($time >= (stat "$TMP/krb5cc_test")[9] && $tries < 10) {
    select (undef, undef, undef, 0.5);
    $tries++;
}
ok ($time < (stat "$TMP/krb5cc_test")[9], ' and is updated after SIGALRM');
kill (15, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.5);
ok (!kill (0, $pid), ' and it dies after SIGTERM');
ok (!-f "$TMP/pid", ' and the PID file was removed');

# Check that krenew keeps running if the ticket cache directory is not
# writeable.
$pid = fork;
if (!defined $pid) {
    BAIL_OUT ("can't fork: $!");
} elsif ($pid == 0) {
    open (STDERR, '>', "$TMP/krenew-errors")
        or BAIL_OUT ("can't create $TMP/krenew-errors: $!");
    exec ($KRENEW, '-K', 30, '-p', "$TMP/pid")
        or BAIL_OUT ("can't run $KRENEW: $!");
}
$tries = 0;
while (not -s "$TMP/pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'krenew -K 30 started');
chmod 0555, $TMP or BAIL_OUT ("cannot chmod $TMP: $!");
kill (14, $pid) or warn "Can't kill $pid: $!\n";
$tries = 0;
while (not -s "$TMP/krenew-errors" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
ok (kill (0, $pid), ' and it keeps running on a non-writeable cache');
chmod 0755, $TMP or BAIL_OUT ("cannot chmod $TMP: $!");
if (open (ERRORS, '<', "$TMP/krenew-errors")) {
    like (scalar (<ERRORS>), qr/^krenew: error reinitializing cache: /,
          ' and the correct error message');
} else {
    ok (0, ' and the correct error message');
}
unlink "$TMP/krenew-errors";
kill (15, $pid) or warn "Can't kill $pid: $!\n";
is (waitpid ($pid, 0), $pid, ' and it dies on SIGTERM');
ok (!-f "$TMP/pid", ' and the PID file was removed');

# If we do that again with -x, krenew should exit.
($out, $err, $status) = command ($KRENEW, '-xbK', 30, '-p', "$TMP/pid");
is ($status, 0, 'krenew -xb works');
is ($err, '', ' with no error output');
is ($out, '', ' and no regular output');
$tries = 0;
while (not -s "$TMP/pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'krenew -xb started');
chmod 0555, $TMP or BAIL_OUT ("cannot chmod $TMP: $!");
kill (14, $pid) or warn "Can't kill $pid: $!\n";
$tries = 0;
while (kill (0, $pid) and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
ok (!kill (0, $pid), ' and it exits on a non-writeable cache');
chmod 0755, $TMP or BAIL_OUT ("cannot chmod $TMP: $!");
unlink "$TMP/pid";

# Now, run a command in the background.
unlink 'child-out';
($out, $err, $status)
    = command ($KRENEW, '-bK', 30, '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               '--', "$ENV{C_TAP_SOURCE}/data/command", "$TMP/child-out");
is ($status, 0, 'Backgrounding krenew works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -s "$TMP/child-pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'krenew is running');
$child = contents ("$TMP/child-pid");
ok (kill (0, $child), 'The child process is running');
$tries = 0;
while (not -s "$TMP/child-out" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
kill (1, $pid) or warn "Cannot send HUP to parent $pid: $!\n";
select (undef, undef, undef, 0.1);
kill (2, $pid) or warn "Cannot send INT to parent $pid: $!\n";
select (undef, undef, undef, 0.1);
kill (15, $pid) or warn "Cannot send TERM to parent $pid: $!\n";
$tries = 0;
while (kill (0, $pid) and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
ok (!kill (0, $pid), 'krenew is no longer running');
ok (!kill (0, $child), 'The child process is no longer running');
open (OUT, '<', "$TMP/child-out")
    or BAIL_OUT ("cannot open $TMP/child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
my $cache = scalar <OUT>;
like ($cache, qr%^/tmp/krb5cc_%, 'Child cache is correct');
is (scalar (<OUT>), "got SIGHUP\n", 'SIGHUP was recorded');
is (scalar (<OUT>), "got SIGINT\n", 'SIGINT was recorded');
is (scalar (<OUT>), "got SIGTERM\n", 'SIGTERM was recorded');
ok (eof OUT, 'No more child output written');
chomp $cache;
ok (! -f $cache, 'New child cache removed');
ok (!-f "$TMP/pid", ' and the PID file was removed');
ok (!-f "$TMP/child-pid", ' and the child PID file was removed');
unlink "$TMP/child-out";

# One more time to test propagation of QUIT signals.
($out, $err, $status)
    = command ($KRENEW, '-bK', 30, '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               '--', "$ENV{C_TAP_SOURCE}/data/command", "$TMP/child-out");
is ($status, 0, 'Backgrounding krenew works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -s "$TMP/child-pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'krenew is running');
$child = contents ("$TMP/child-pid");
ok (kill (0, $child), 'The child process is running');
$tries = 0;
while (not -s "$TMP/child-out" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
kill (3, $pid) or warn "Cannot send QUIT to parent $pid: $!\n";
$tries = 0;
while (kill (0, $pid) and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
ok (!kill (0, $pid), 'krenew is no longer running');
ok (!kill (0, $child), 'The child process is no longer running');
open (OUT, '<', "$TMP/child-out")
    or BAIL_OUT ("cannot open $TMP/child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
$cache = scalar <OUT>;
like ($cache, qr%^/tmp/krb5cc_%, 'Child cache is correct');
is (scalar (<OUT>), "got SIGQUIT\n", 'SIGQUIT was recorded');
ok (eof OUT, 'No more child output written');
close OUT;
chomp $cache;
ok (! -f $cache, 'New child cache removed');
ok (!-f "$TMP/pid", ' and the PID file was removed');
ok (!-f "$TMP/child-pid", ' and the child PID file was removed');
unlink "$TMP/child-out";

# Normally, if we are running a command and krenew has to exit because it
# can't renew the ticket cache any more, it should exit and leave the command
# running.
($out, $err, $status)
    = command ($KRENEW, '-bK', 30, '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               '--', "$ENV{C_TAP_SOURCE}/data/command", "$TMP/child-out");
is ($status, 0, 'Backgrounding krenew works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -s "$TMP/child-pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'krenew is running');
$child = contents ("$TMP/child-pid");
ok (kill (0, $child), 'The child process is running');
$tries = 0;
while (not -s "$TMP/child-out" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
open (OUT, '<', "$TMP/child-out")
    or BAIL_OUT ("cannot open $TMP/child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
$cache = scalar <OUT>;
chomp $cache;
like ($cache, qr%^/tmp/krb5cc_%, 'Child cache is correct');
unlink $cache;
kill (14, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.2);
ok (!kill (0, $pid), 'krenew dies after failure to renew');
ok (kill (0, $child), ' and the child process is still running');
kill (15, $child) or warn "Can't kill child $child: $!\n";
select (undef, undef, undef, 0.2);
ok (!kill (0, $child), 'The child process is no longer running');
is (scalar (<OUT>), "got SIGTERM\n", 'SIGTERM was recorded');
ok (eof OUT, 'No more child output written');
close OUT;
ok (!-f "$TMP/pid", ' and the PID file was removed');
ok (!-f "$TMP/child-pid", ' and the child PID file was removed');
unlink "$TMP/child-out";

# If run with -s, krenew should instead kill the child process with HUP on
# failure to renew the ticket.
($out, $err, $status)
    = command ($KRENEW, '-bsK', 30, '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               '--', "$ENV{C_TAP_SOURCE}/data/command", "$TMP/child-out");
is ($status, 0, 'Backgrounding krenew -s works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -s "$TMP/child-pid" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'krenew is running');
$child = contents ("$TMP/child-pid");
ok (kill (0, $child), 'The child process is running');
$tries = 0;
while (not -s "$TMP/child-out" and $tries < 100) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
open (OUT, '<', "$TMP/child-out")
    or BAIL_OUT ("cannot open $TMP/child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
$cache = scalar <OUT>;
chomp $cache;
like ($cache, qr%^/tmp/krb5cc_%, 'Child cache is correct');
unlink $cache;
kill (14, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.2);
ok (!kill (0, $pid), 'krenew dies after failure to renew');
ok (kill (0, $child), ' and the child process is still running');
kill (15, $child) or warn "Can't kill child $child: $!\n";
select (undef, undef, undef, 0.2);
ok (!kill (0, $child), 'The child process is no longer running');
is (scalar (<OUT>), "got SIGHUP\n", 'SIGHUP was recorded');
is (scalar (<OUT>), "got SIGTERM\n", 'SIGTERM was recorded');
ok (eof OUT, 'No more child output written');
close OUT;
ok (!-f "$TMP/pid", ' and the PID file was removed');
ok (!-f "$TMP/child-pid", ' and the child PID file was removed');
unlink "$TMP/child-out";

# Clean up.
unlink "$TMP/krb5cc_test", "$TMP/child-out";
rmdir $TMP;
