#!/usr/bin/env bash
# This file is in the public domain.
#
# Tests the per-validation message budget of a challenge.
#
# By design a validation allows a fixed number of *addresses* and, for each
# of them, a fixed number of PIN transmissions -- 3 x 3 -- so a user who
# mistyped their address is not made to wait out the cooldown of a message
# that went to somebody else.  The ceiling of nine messages is therefore
# intended, but it is intended to be spread over three *distinct* addresses.
#
# Regression test: do_challenge_address.sql used to decide "is this the same
# address?" on the raw JSON text.  Submitting the very same address with its
# fields in a different order then counted as a new address, so all nine
# messages could be aimed at one recipient -- and an honest user could lose
# an address attempt to nothing but a reordered form submission (the daemon
# itself re-appends 'read_only' as the last field).
#
# Also checks the property that makes the 3 x 3 budget safe: a PIN that was
# transmitted for one address must not solve the challenge once the address
# has been changed.

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"
    wait
}

LAST_RESPONSE=$(mktemp responseXXXXXX.log)
FILENAME="test-challenger-pinlimit.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-pinlimit.conf"
BURL="http://localhost:9967"
REDIRECT_URI="http://client.example.com/"

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"

# All addresses used below deliver to the *same* recipient: cat.sh only
# looks at the "filename" field, "tag" is decoration that the (empty)
# ADDRESS_RESTRICTIONS do not constrain.
SENT=0

# submit /challenge with the given curl arguments; counts transmissions in
# $SENT, leaves the HTTP status in $STATUS.
function submit()
{
    rm -f "${FILENAME}"
    STATUS=$(curl "${BURL}/challenge/${NONCE}" \
        -X POST \
        -H "Accept: application/json" \
        "$@" \
        -w "%{http_code}" -s -o $LAST_RESPONSE)
    if [ "true" = "$(jq -r '.transmitted // false' < "$LAST_RESPONSE")" ]
    then
        SENT=$(( SENT + 1 ))
        PIN=$(awk '{print $5}' < "${FILENAME}")
    fi
}

# submit_address $1=tag: the address {"filename":...,"tag":...}
function submit_address()
{
    submit --data-urlencode "filename=${FILENAME}" \
           --data-urlencode "tag=$1"
}

# expect_sent $1=count $2=what was submitted
function expect_sent()
{
    if [ "$SENT" != "$1" ]
    then
        exit_fail "expected ${1} PIN transmissions after $2, counted ${SENT}"
    fi
}

# expect_status $1=code $2=what was submitted
function expect_status()
{
    if [ "$STATUS" != "$1" ]
    then
        exit_fail "/challenge: expected ${1} for $2. Got: $STATUS" \
            $(cat $LAST_RESPONSE)
    fi
}

echo -n "Spending the three PIN transmissions of the first address..."
for i in 1 2 3
do
    submit_address "one"
    expect_status 200 "a PIN transmission of the first address"
    echo -n "."
done
expect_sent 3 "three submissions of the first address"
PIN_ONE="${PIN}"
echo " OK"

echo -n "A fourth transmission for that address must be refused..."
submit_address "one"
expect_status 429 "a fourth transmission of the same address"
expect_sent 3 "a fourth transmission of the same address"
echo " OK"

# Same JSON value, fields in the other order.  This is the regression: with
# a textual comparison this counts as a new address, refills the budget and
# transmits a fourth PIN to the very same recipient.
echo -n "The same address with its fields reordered is not a new address..."
submit --data-urlencode "tag=one" \
       --data-urlencode "filename=${FILENAME}"
expect_status 429 "a reordered rendering of the current address"
expect_sent 3 "a reordered rendering of the current address"
echo " OK"

echo -n "Changing the address transmits and invalidates the previous PIN..."
submit_address "two"
expect_status 200 "the first submission of the second address"
expect_sent 4 "the first submission of the second address"
RESULT=$(curl "${BURL}/solve/${NONCE}" \
    -X POST \
    -H "Accept: application/json" \
    --data-urlencode "pin=${PIN_ONE}" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)
if [ "$RESULT" = "302" ] || [ "$RESULT" = "200" ]
then
    exit_fail "the PIN sent for the previous address solved the challenge"
fi
echo " OK ($RESULT)"

echo -n "Spending the rest of the designed 3 x 3 budget..."
for i in 2 3
do
    submit_address "two"
    expect_status 200 "a PIN transmission of the second address"
done
for i in 1 2 3
do
    submit_address "three"
    expect_status 200 "a PIN transmission of the third address"
done
expect_sent 9 "three transmissions for each of three addresses"
echo " OK (9)"

echo -n "A fourth address must be refused..."
submit_address "four"
expect_status 429 "a fourth address"
expect_sent 9 "a fourth address"
echo " OK"

exit 0
