#!/usr/bin/env bash
# This file is in the public domain.
#
# Tests that a failed PIN transmission does not invalidate the PIN the
# user already received.
#
# Regression test: /challenge used to commit the new PIN (overwriting
# last_pin) before forking the AUTH_COMMAND helper, so a helper failure
# returned 502 *and* left the user without a usable PIN until the
# retransmission cooldown elapsed.

set -eu

# Exit, with status code "skip" (no 'real' failure)
function exit_skip() {
    echo " SKIP: $1"
    exit 77
}

# Exit, with error message (hard failure)
function exit_fail() {
    echo " FAIL: $@"
    exit 1
}

# Cleanup to run whenever we exit
function cleanup()
{
    for n in $(jobs -p)
    do
        kill $n 2> /dev/null || true
    done
    rm -f "$LAST_RESPONSE" "$FILENAME" "$FILENAME.sent"
    rm -f "$FILENAME2" "$FILENAME2.sent"
    rm -f "$FILENAME3" "$FILENAME3.sent"
    wait
}

LAST_RESPONSE=$(mktemp responseXXXXXX.log)
FILENAME="test-challenger-pinfail.txt"
FILENAME2="test-challenger-pinfail-changed.txt"
FILENAME3="test-challenger-pinfail-nopin.txt"

# Install cleanup handler (except for kill -9)
trap cleanup EXIT

export PATH="$PATH:."

echo -n "Testing for jq"
jq -h > /dev/null || exit_skip "jq required"
echo " FOUND"
echo -n "Testing for curl"
curl -h > /dev/null || exit_skip "curl required"
echo " FOUND"
echo -n "Testing for wget"
wget -h > /dev/null || exit_skip "wget required"
echo " FOUND"
echo -n "Testing for challenger-httpd ..."
challenger-httpd -h > /dev/null || exit_skip "challenger-httpd required"
echo " FOUND"

CONF="test-challenger-pinfail.conf"
BURL="http://localhost:9967"
REDIRECT_URI="http://client.example.com/"

rm -f "$FILENAME" "$FILENAME.sent"
rm -f "$FILENAME2" "$FILENAME2.sent"

echo -n "Initialize challenger database ..."
challenger-dbinit -r -c "${CONF}" &> dbinit.log
echo " OK"

echo -n "Add challenger client ..."
CLIENT_SECRET="secret-token:secret"
challenger-admin -c "${CONF}" -a "${CLIENT_SECRET}" "${REDIRECT_URI}" &> admin.log
echo " OK"
# We just reset the DB, thus the client ID must be 1 here:
CLIENT_ID=1

echo -n "Start challenger-httpd ..."
challenger-httpd -L INFO -c "${CONF}" &> httpd.log &

# Wait for challenger to be available
for n in $(seq 1 50)
do
    echo -n "."
    sleep 0.2
    OK=0
    wget --tries=1 --timeout=1 "${BURL}/config" -o /dev/null -O /dev/null >/dev/null || continue
    OK=1
    break
done
if [ 1 != $OK ]
then
    exit_skip "Failed to launch challenger service"
fi
echo " OK"

echo -n "Setup new validation process..."
STATUS=$(curl "${BURL}/setup/${CLIENT_ID}" \
    -H "Authorization: Bearer ${CLIENT_SECRET}" \
    -d '' \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
NONCE=$(jq -r .nonce < "$LAST_RESPONSE")
echo " OK"

CLIENT_STATE="the-client-state"
CLIENT_SCOPE="the-client-scope"

echo -n "Initiating user login..."
STATUS=$(curl "${BURL}/authorize/${NONCE}" \
    -G \
    -H "Accept: application/json" \
    --data-urlencode "response_type=code" \
    --data-urlencode "client_id=${CLIENT_ID}" \
    --data-urlencode "redirect_uri=${REDIRECT_URI}" \
    --data-urlencode "state=${CLIENT_STATE}" \
    --data-urlencode "scope=${CLIENT_SCOPE}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
echo " OK"

echo -n "Initiating address submission..."
STATUS=$(curl "${BURL}/challenge/${NONCE}" \
    -X POST \
    -H "Accept: application/json" \
    --data-urlencode "filename=${FILENAME}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
echo " OK"

PIN=$(cat ${FILENAME} | awk '{print $5}')

# The helper now refuses to deliver anything (see cat-once.sh).
sleep 1.5
echo -n "Requesting a retransmission that the helper will fail..."
STATUS=$(curl "${BURL}/challenge/${NONCE}" \
    -X POST \
    -H "Accept: application/json" \
    --data-urlencode "filename=${FILENAME}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "502" ]
then
    exit_fail "Expected 502 Bad Gateway. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
echo " OK"

# The PIN the user got in the first message must still work: the PIN of
# the failed transmission never reached anybody.
echo -n "Solving with the PIN ${PIN} from the delivered message..."
RESULT=$(curl "${BURL}/solve/${NONCE}" \
    -X POST \
    -H "Accept: text/html" \
    --data-urlencode "pin=${PIN}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$RESULT" != "302" ]
then
    exit_fail "Expected 302 (PIN still valid). Got: $RESULT" $(cat $LAST_RESPONSE)
fi
echo " OK"

# The flip side: deferring the new PIN must not keep the *old* PIN alive
# across an address change.  A changed address always (re)transmits -- the
# change refills the transmission budget and clears the cooldown -- so
# before this patch the PIN on file was replaced in the same call.  Now it
# is only replaced once the helper confirms, and a helper failure must
# therefore leave the validation with no usable PIN rather than with the
# PIN that went to the address the user just replaced.

echo -n "Setup a second validation process..."
STATUS=$(curl "${BURL}/setup/${CLIENT_ID}" \
    -H "Authorization: Bearer ${CLIENT_SECRET}" \
    -d '' \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
NONCE=$(jq -r .nonce < "$LAST_RESPONSE")
echo " OK"

echo -n "Transmitting a PIN for the first address..."
STATUS=$(curl "${BURL}/challenge/${NONCE}" \
    -X POST \
    -H "Accept: application/json" \
    --data-urlencode "filename=${FILENAME2}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
OLD_PIN=$(awk '{print $5}' < "${FILENAME2}")
echo " OK"

# The helper refuses to deliver anything from here on (see cat-once.sh);
# the address change transmits regardless of the cooldown.
echo -n "Changing the address while the helper is down..."
STATUS=$(curl "${BURL}/challenge/${NONCE}" \
    -X POST \
    -H "Accept: application/json" \
    --data-urlencode "filename=${FILENAME2}" \
    --data-urlencode "tag=corrected" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "502" ]
then
    exit_fail "Expected 502 Bad Gateway. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
echo " OK"

echo -n "The PIN of the previous address must not solve the new one..."
RESULT=$(curl "${BURL}/solve/${NONCE}" \
    -X POST \
    -H "Accept: text/html" \
    --data-urlencode "pin=${OLD_PIN}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$RESULT" = "302" ] || [ "$RESULT" = "200" ]
then
    exit_fail "the PIN sent for the previous address attested the new one"
fi
echo " OK ($RESULT)"

# Regression test for finding 24: since the PIN is only promoted to
# 'last_pin' once the helper confirmed the transmission, a validation
# whose every transmission failed has no PIN on file at all -- 'last_pin'
# is SQL NULL.  The caller reads 'out_last_pin' through a non-NULLable
# result spec, so the fall-through path of the stored procedure must not
# hand it NULL; it did, and that turned an ordinary "no transmissions
# left" answer into a 500.

# The marker makes cat-once.sh fail from the very first invocation, so no
# PIN is ever confirmed for this validation.
touch "${FILENAME3}.sent"

echo -n "Setup a third validation whose PINs never get through..."
STATUS=$(curl "${BURL}/setup/${CLIENT_ID}" \
    -H "Authorization: Bearer ${CLIENT_SECRET}" \
    -d '' \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
NONCE=$(jq -r .nonce < "$LAST_RESPONSE")
echo " OK"

echo -n "Spending all three transmissions on a helper that always fails..."
for i in 1 2 3
do
    if [ "$i" != "1" ]
    then
        # Wait out the retransmission cooldown; the address does not
        # change, so nothing else would clear it.
        sleep 1.5
    fi
    STATUS=$(curl "${BURL}/challenge/${NONCE}" \
        -X POST \
        -H "Accept: application/json" \
        --data-urlencode "filename=${FILENAME3}" \
        -w "%{http_code}" -s -o $LAST_RESPONSE)
    if [ "$STATUS" != "502" ]
    then
        exit_fail "Expected 502 Bad Gateway. Got: $STATUS" $(cat $LAST_RESPONSE)
    fi
    echo -n "."
done
echo " OK"

# No PIN was ever transmitted and no transmission is left, so this takes
# the fall-through path with 'last_pin' still NULL.  Note that this does
# not depend on the cooldown: the empty transmission budget alone blocks
# the (re)transmission.
echo -n "A further /challenge with no PIN on file must not be a server error..."
STATUS=$(curl "${BURL}/challenge/${NONCE}" \
    -X POST \
    -H "Accept: application/json" \
    --data-urlencode "filename=${FILENAME3}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)

if [ "$STATUS" != "429" ]
then
    exit_fail "Expected 429 (out of PIN transmissions). Got: $STATUS" \
        $(cat $LAST_RESPONSE)
fi
echo " OK"

exit 0
