#!/usr/bin/perl -w
#
# Tests for k5start daemon functionality.
#
# Written by Russ Allbery <rra@stanford.edu>
# Copyright 2008, 2009, 2011, 2012
#     The Board of Trustees of the Leland Stanford Junior University
#
# See LICENSE for licensing terms.

use Cwd;

use Test::More;

# The full path to the newly-built k5start client.
our $K5START = "$ENV{BUILD}/../k5start";

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

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

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

# Decide whether we have the configuration to run the tests.
if (-f "$DATA/test.keytab" and -f "$DATA/test.principal") {
    plan tests => 76;
} else {
    plan skip_all => "no keytab configuration";
    exit 0;
}

# Get the test principal.
my $principal = contents ("$DATA/test.principal");

# Don't overwrite the user's ticket cache.
$ENV{KRB5CCNAME} = "$TMP/krb5cc_test";

# Start a k5start daemon and be sure it gets tickets and stays running.
unlink "$TMP/krb5cc_test";
my $pid = fork;
if (!defined $pid) {
    BAIL_OUT ("can't fork: $!");
} elsif ($pid == 0) {
    exec ($K5START, '-K', 1, '-f', "$DATA/test.keytab", '-p', "$TMP/pid",
          $principal) or BAIL_OUT ("can't run $K5START: $!");
}
my $tries = 0;
while (not -f "$TMP/krb5cc_test" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
my ($default, $service) = klist ();
like ($default, qr/^\Q$principal\E(\@\S+)?\z/,
      'Authentication succeeded for the right principal');
like ($service, qr%^krbtgt/%, ' and the right service');
if (-f "$TMP/pid") {
    my $daemon = contents ("$TMP/pid");
    is ($pid, $daemon, ' and the right PID is written');
    $pid = $daemon if $daemon;
} else {
    ok (0, ' and the right PID is written');
}
ok (kill (0, $pid), ' and k5start is still running');
unlink "$TMP/krb5cc_test";
ok (! -f "$TMP/krb5cc_test", 'Ticket cache was deleted');
kill (14, $pid) or warn "Can't kill $pid: $!\n";
$tries = 0;
while (not -f "$TMP/krb5cc_test" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
ok (kill (0, $pid), ' and k5start is still running after ALRM');
($default, $service) = klist ();
like ($default, qr/^\Q$principal\E(\@\S+)?\z/,
      ' and recreates cache with the right principal');
like ($service, qr%^krbtgt/%, ' and the right service');
kill (15, $pid) or warn "Can't kill $pid: $!\n";
is (waitpid ($pid, 0), $pid, ' and k5start dies after SIGTERM');
ok (!-f "$TMP/pid", ' and the PID file was removed');

# Try again with the -b flag.
unlink "$TMP/krb5cc_test";
my ($out, $err, $status)
    = command ($K5START, '-bK', 1, '-f', "$DATA/test.keytab", '-p',
               "$TMP/pid", $principal);
is ($status, 0, 'Backgrounding k5start works');
is ($err, '', ' with no error output');
is ($out, '', ' and -q was added implicitly');
$tries = 0;
while (not -f "$TMP/krb5cc_test" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
($default, $service) = klist ();
like ($default, qr/^\Q$principal\E(\@\S+)?\z/,
      'Authentication succeeded for the right principal');
like ($service, qr%^krbtgt/%, ' and the right service');
$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.2);
ok (!-f "$TMP/pid", ' and the PID file was removed');

# Check that k5start 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/k5start-errors")
        or BAIL_OUT ("can't create $TMP/k5start-errors: $!");
    exec ($K5START, '-K', 1, '-Uf', "$DATA/test.keytab", '-p', "$TMP/pid")
        or BAIL_OUT ("can't run $K5START: $!");
}
$tries = 0;
while (not -f "$TMP/pid" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'k5start -K 1 started');
chmod 0555, $TMP or BAIL_OUT ("cannot chmod $TMP: $!");
kill (14, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.5);
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/k5start-errors")) {
    like (scalar (<ERRORS>), qr/^k5start: error initializing ticket cache: /,
          ' and the correct error message');
} else {
    ok (0, ' and the correct error message');
}
unlink "$TMP/k5start-errors";
kill (15, $pid) or warn "Can't kill $pid: $!\n";
is (waitpid ($pid, 0), $pid, ' and k5start dies after SIGTERM');
ok (!-f "$TMP/pid", ' and the PID file was removed');

# If we do that again with -x, k5start should exit.
($out, $err, $status)
    = command ($K5START, '-xbK', 1, '-f', "$DATA/test.keytab", '-p',
               "$TMP/pid", $principal);
is ($status, 0, 'k5start -xb works');
is ($err, '', ' with no error output');
is ($out, '', ' and -q was added implicitly');
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'k5start -xb started');
chmod 0555, $TMP or BAIL_OUT ("cannot chmod $TMP: $!");
kill (14, $pid) or warn "Can't kill $pid: $!\n";
select (undef, undef, undef, 0.5);
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 "$TMP/krb5cc_test", "$TMP/krb5cc_child", "$TMP/child-out";
($out, $err, $status)
    = command ($K5START, '-bK', 1, '-k', "$TMP/krb5cc_child", '-f',
               "$DATA/test.keytab", '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               $principal, '--', "$ENV{SOURCE}/data/command",
               "$TMP/child-out");
is ($status, 0, 'Backgrounding k5start works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -f "$TMP/child-pid" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
($default, $service) = klist ();
is ($default, undef, 'The normal ticket cache is untouched');
$ENV{KRB5CCNAME} = "$TMP/krb5cc_child";
($default, $service) = klist ();
like ($default, qr/^\Q$principal\E(\@\S+)?\z/,
      ' but the other cache has the right principal');
like ($service, qr%^krbtgt/%, ' and the right service');
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'k5start 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 < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
kill (1, $child) or warn "Cannot send HUP to child $child: $!\n";
select (undef, undef, undef, 0.1);
kill (2, $child) or warn "Cannot send INT to child $child: $!\n";
select (undef, undef, undef, 0.1);
kill (15, $child) or warn "Cannot send TERM to child $child: $!\n";
select (undef, undef, undef, 0.2);
ok (!kill (0, $pid), 'k5start 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 child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
is (scalar (<OUT>), "FILE:$TMP/krb5cc_child\n", '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');
close OUT;
ok (!-f "$TMP/pid", 'PID file cleaned up');
ok (!-f "$TMP/child-pid", 'Child PID file cleaned up');

# Now, do that again, but test signal propagation from the parent to the
# child.
unlink "$TMP/krb5cc_child", "$TMP/child-out";
($out, $err, $status)
    = command ($K5START, '-bK', 1, '-k', "$TMP/krb5cc_child", '-f',
               "$DATA/test.keytab", '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               $principal, '--', "$ENV{SOURCE}/data/command",
               "$TMP/child-out");
is ($status, 0, 'Backgrounding k5start works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -f "$TMP/child-pid" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'k5start 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 < 10) {
    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";
select (undef, undef, undef, 0.2);
ok (!kill (0, $pid), 'k5start 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 child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
is (scalar (<OUT>), "FILE:$TMP/krb5cc_child\n", 'Child cache is correct');
is (scalar (<OUT>), "got SIGHUP\n", 'SIGHUP was propagated');
is (scalar (<OUT>), "got SIGINT\n", 'SIGINT was propagated');
is (scalar (<OUT>), "got SIGTERM\n", 'SIGTERM was propagated');
ok (eof OUT, 'No more child output written');
close OUT;
ok (!-f "$TMP/pid", 'PID file cleaned up');
ok (!-f "$TMP/child-pid", 'Child PID file cleaned up');

# One more time to test SIGQUIT.
unlink "$TMP/krb5cc_child", "$TMP/child-out";
($out, $err, $status)
    = command ($K5START, '-bK', 1, '-k', "$TMP/krb5cc_child", '-f',
               "$DATA/test.keytab", '-p', "$TMP/pid", '-c', "$TMP/child-pid",
               $principal, '--', "$ENV{SOURCE}/data/command",
               "$TMP/child-out");
is ($status, 0, 'Backgrounding k5start works');
is ($err, '', ' with no error output');
is ($out, '', ' and output was redirected properly');
$tries = 0;
while (not -f "$TMP/child-pid" and $tries < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
$pid = contents ("$TMP/pid");
ok (kill (0, $pid), 'k5start 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 < 10) {
    select (undef, undef, undef, 0.1);
    $tries++;
}
kill (3, $pid) or warn "Cannot send QUIT to parent $pid: $!\n";
select (undef, undef, undef, 0.2);
ok (!kill (0, $pid), 'k5start 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 child-out: $!");
is (scalar (<OUT>), "$child\n", 'Child PID is correct');
is (scalar (<OUT>), "/\n", 'Child working directory is /');
is (scalar (<OUT>), "FILE:$TMP/krb5cc_child\n", 'Child cache is correct');
is (scalar (<OUT>), "got SIGQUIT\n", 'SIGQUIT was propagated');
ok (eof OUT, 'No more child output written');
close OUT;
ok (!-f "$TMP/pid", 'PID file cleaned up');
ok (!-f "$TMP/child-pid", 'Child PID file cleaned up');

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