#!/bin/bash

# Authors:
# Daniel Kahn Gillmor <dkg@fifthhorseman.net>
# Fiona Klute <fiona.klute@gmx.de>

set -e
. ${srcdir}/common.bash
. ${srcdir}/apache_service.bash
netns_reexec ${@}

testid="${1##t-}"

if [ -z "$testid" ] ; then
    echo -e "No test case selected.\nUsage: ${0} t-N" >&2
    exit 1
else
    testid=${srcdir}/tests/"$(printf "%02d" "$testid")"_*
fi
testdir="$(realpath ${testid})"

BADVARS=0
for v in APACHE2 TEST_HOST TEST_PORT TEST_QUERY_TIMEOUT TEST_SERVICE_WAIT \
		 MSVA_PORT; do
    if [ ! -v "$v" ]; then
        printf "You need to set the %s environment variable\n" "$v" >&2
        BADVARS=1
    fi
done

if [ 0 != "$BADVARS" ]; then
    exit 1
fi

# write script file and line to stderr on error
function pinpoint_error()
{
    echo "Command \"${BASH_COMMAND}\" failed. Call trace:" >&2
    local stack=0
    while caller $((stack++)) >&2; do true; done
}
trap 'pinpoint_error' ERR

function stop_msva()
{
    kill_by_pidfile "${msva_pidfile}"
    unset msva_pidfile
}

# Compare expected/actual outputs, filtering out headers from actual
# output that are expected to change between runs or builds (currently
# "Date" and "Server"). The headers must be excluded in the expected
# output.
#
# Parameters:
# $1: path to expected output
# $2: path to actual output
# $3: additional options for diff (optional)
function diff_output_filter_headers()
{
    local expected="$1"
    local actual="$2"
    diff $3 -u "${expected}" <( cat "${actual}" | \
	grep -v -P '^Date:\s.*GMT\s?$' | \
	grep -v -P '^Server:\sApache'  | \
	tail -n "$(wc -l < ${expected})" )
}

# Run a command, storing its PID in the given file
# Usage: run_with_pidfile PIDFILE COMMAND [ARGS]
function run_with_pidfile()
{
    local pidfile=$1
    local cmd=$2
    shift 2
    echo $BASHPID >${pidfile}
    exec ${cmd} $*
}

# Kills the process with the PID contained in a given file, then
# deletes the file.
# Usage: kill_by_pidfile PIDFILE
function kill_by_pidfile()
{
    local pidfile="${1}"
    # In some testcases with expected failure, gnutls-cli sometimes
    # failed before the subshell in front of the pipe (see gnutls-cli
    # call below) got so far as to write the PID, much less exec
    # sleep. So we need to check if there actually is anything to
    # kill.
    if [ -n "${pidfile}" ]; then
	local pid=$(cat "${pidfile}")
	if [ -n "${pid}" ] && ps -p "${pid}"; then
	    kill "${pid}"
	else
	    echo "No running process with PID ${pid} (${pidfile})."
	fi
	rm "${pidfile}"
    fi
}

function apache_down_err() {
    printf "FAILURE: %s\n" "$TEST_NAME"
    ${APACHE2} -f "${testdir}/apache.conf" -k stop || true
    if [ -e output ]; then
	printf "\ngnutls-cli outputs:\n"
	diff_output_filter_headers "output" "$output" || true
    fi

    if [ -r "${testdir}/backend.conf" ]; then
	apache_service "${testdir}" "backend.conf" stop || true
    fi

    if [ -r "${testdir}/ocsp.conf" ]; then
	apache_service "${testdir}" "ocsp.conf" stop || true
    fi

    if [ -n "${sleep_pidfile}" ]; then
	kill_by_pidfile "${sleep_pidfile}"
    fi

    local errlog="logs/${TEST_NAME}.error.log"
    if [ -r "${errlog}" ]; then
	printf "\nApache error logs:\n"
	tail "${errlog}"
    fi

    if [ -n "${USE_MSVA}" ]; then
	stop_msva
    fi
}

if [ -n "${USE_MSVA}" ]; then
    msva_pidfile="$(mktemp mod_gnutls_test-XXXXXX.pid)"
    GNUPGHOME=msva.gnupghome MSVA_KEYSERVER_POLICY=never run_with_pidfile "${msva_pidfile}" monkeysphere-validation-agent &
    trap stop_msva EXIT

    printf "TESTING: initial MSVA verification\n"
    export MONKEYSPHERE_VALIDATION_AGENT_SOCKET="http://127.0.0.1:$MSVA_PORT"

    msva_test_cmd="msva-query-agent https \"$(cat client.uid)\" x509pem client < client/x509.pem"
    # check if MSVA is up, fail if not
    if wait_ready "${msva_test_cmd}"; then
	printf "\nSUCCESS: initial MSVA verification\n"
    else
	printf "\nFAIL: initial MSVA verification\n"
	exit 1
    fi
fi

# configure locking for the Apache process
if [ -n "${USE_TEST_NAMESPACE}" ]; then
    echo "Using namespaces to isolate tests, no need for locking."
    flock_cmd=""
elif [ -n "${FLOCK}" ]; then
    flock_cmd="${FLOCK} -w ${TEST_LOCK_WAIT} $(realpath ${TEST_LOCK})"
else
    echo "Locking disabled, using wait based on Apache PID file."
    wait_pid_gone "${TEST_LOCK}"
    flock_cmd=""
fi

export srcdir="$(realpath ${srcdir})"
export TEST_NAME="$(basename "${testdir}")"
output="outputs/${TEST_NAME}.output"
rm -f "$output"

if [ -e ${testdir}/fail.* ]; then
    EXPECTED_FAILURE="$(printf " (expected: %s)" fail.*)"
else
    unset EXPECTED_FAILURE
fi
printf "TESTING: %s%s\n" "$TEST_NAME" "$EXPECTED_FAILURE"
trap apache_down_err EXIT
if [ -n "${USE_MSVA}" ]; then
    export MONKEYSPHERE_VALIDATION_AGENT_SOCKET="http://127.0.0.1:$MSVA_PORT"
fi

# If VERBOSE is enabled, log the HTTPD build configuration
if [ -n "${VERBOSE}" ]; then
    ${APACHE2} -f "${srcdir}/base_apache.conf" -V
fi

# Start OCSP responder, if configured
if [ -r "${testdir}/ocsp.conf" ]; then
    apache_service "${testdir}" "ocsp.conf" start "${OCSP_LOCK}"
    CHECK_OCSP_SERVER="true"
    if [ -n "${VERBOSE}" ]; then
	echo "OCSP index for the test CA:"
	cat authority/ocsp_index.txt
    fi
fi

# Start proxy backend server, if configured
if [ -r "${testdir}/backend.conf" ]; then
    apache_service "${testdir}" "backend.conf" start "${BACKEND_LOCK}"
fi

if ! ${flock_cmd} ${APACHE2} -f "${testdir}/apache.conf" -k start; then
    if [ -e "${testdir}/fail.server" ]; then
	echo "Apache HTTPD failed to start as expected."
	exit 0
    else
	echo "Apache HTTPD unexpectedly failed to start."
	exit 1
    fi
fi

# check OCSP server
if [ -n "${CHECK_OCSP_SERVER}" ]; then
    if [ -n "${OCSP_RESPONSE_FILE}" ]; then
	store_ocsp="--outfile ${OCSP_RESPONSE_FILE}"
    fi
    echo "---- Testing OCSP server ----"
    wait_ready "ocsptool --ask --nonce --load-issuer authority/x509.pem --load-cert server/x509.pem ${store_ocsp}"
    echo "---- OCSP test done ----"
fi

if [ -n "${TARGET_IP}" ]; then
    TARGET="${TARGET_IP}"
else
    TARGET="${TEST_HOST}"
fi

# PID file for sleep command (explanation below)
sleep_pidfile="$(mktemp mod_gnutls_test-XXXXXX.pid)"

# The sleep call keeps the pipe from the subshell to gnutls-cli
# open. Without it gnutls-cli would terminate as soon as sed is
# done, and not wait for a response from the server, leading to
# failing tests. Sending sleep to the background allows the test
# case to proceed instead of waiting for it to return. The sleep
# process is stopped after gnutls-cli terminates.
#
# The line end manipulation in sed guarantees that all header lines
# end with CRLF as required by RFC 7230, Section 3.1.1 regardless of
# the line ends in the input file.
if (sed -r "s/__HOSTNAME__/${TEST_HOST}/;s/\r?$/\r/" <${testdir}/input && \
	   run_with_pidfile "${sleep_pidfile}" sleep "${TEST_QUERY_TIMEOUT}" &) | \
       gnutls-cli -p "${TEST_PORT}" $(cat ${testdir}/gnutls-cli.args) "${TARGET}" \
       | tee "$output" && test "${PIPESTATUS[1]}" -eq 0;
then
    if [ -e ${testdir}/fail* ]; then
        printf "%s should have failed but succeeded\n" "$(basename "$testdir")" >&2
        exit 1
    fi
else
    if [ ! -e ${testdir}/fail* ]; then
        printf "%s should have succeeded but failed\n" "$(basename "$testdir")" >&2
        exit 1
    fi
fi

kill_by_pidfile "${sleep_pidfile}"
unset sleep_pidfile

if [ -e ${testdir}/output ] ; then
    diff_output_filter_headers "${testdir}/output" "$output" >&2
fi
if [ -n "${USE_MSVA}" ]; then
    trap stop_msva EXIT
else
    trap - EXIT
fi
${APACHE2} -f "${testdir}/apache.conf" -k stop || [ -e ${testdir}/fail.server ]
printf "SUCCESS: %s\n" "$TEST_NAME"

if [ -r "${testdir}/backend.conf" ]; then
    apache_service "${testdir}" "backend.conf" stop || true
fi

if [ -r "${testdir}/ocsp.conf" ]; then
    apache_service "${testdir}" "ocsp.conf" stop || true
fi

if [ -n "${USE_MSVA}" ]; then
    stop_msva
    # Without explicitly resetting the trap function, it would be
    # called again on exit. Of course, we could just not stop MSVA and
    # let the trap do the work, but I think the code is easier to
    # understand like this.
    trap - EXIT
fi
