#!/usr/bin/env bash
# This file is in the public domain.
#
# Regression test: /token must not be an oracle for the state of a
# validation.  The nonce is trivially recovered from any authorization
# code (CH_code_to_nonce() just parses the part before the '-'), so every
# check that runs before the code MAC is verified answers a question about
# a validation the caller may not hold a code for.
#
# Three requests that all carry a *forged* code must therefore be answered
# identically:
#   1. a validation that exists but has no proven address yet,
#   2. a nonce that is syntactically valid but unknown,
#   3. a validation that was solved (so only the MAC is wrong).
#
# Before the fix these produced 409/invalid_request/9759,
# 400/invalid_grant/9755 and 400/invalid_grant/9754 respectively.

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-token-errors.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.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

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

# The MAC half of an authorization code is never parsed, only compared,
# so any syntactically plausible value serves as a forgery.
FORGED_MAC="00000000000000000000000000000000000000000000000000000"

# Post a code to /token and store "<status> <body>" in TOKEN_RESULT.
function post_token()
{
    local STATUS
    STATUS=$(curl "${BURL}/token" \
        -X POST \
        --data-urlencode "client_id=${CLIENT_ID}" \
        --data-urlencode "redirect_uri=${REDIRECT_URI}" \
        --data-urlencode "client_secret=${CLIENT_SECRET}" \
        --data-urlencode "code=$1" \
        --data-urlencode "grant_type=authorization_code" \
        -w "%{http_code}" -s -o $LAST_RESPONSE)
    TOKEN_RESULT="${STATUS} $(jq -cS . < "$LAST_RESPONSE")"
}

# Run /setup and /authorize, leaving the nonce in NONCE.
function start_validation()
{
    local STATUS
    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 from /setup. Got: $STATUS" $(cat $LAST_RESPONSE)
    fi
    NONCE=$(jq -r .nonce < "$LAST_RESPONSE")
    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 from /authorize. Got: $STATUS" $(cat $LAST_RESPONSE)
    fi
}


echo -n "Redeeming a forged code for a validation without address..."
start_validation
NO_ADDRESS_NONCE="${NONCE}"
post_token "${NO_ADDRESS_NONCE}-${FORGED_MAC}"
NO_ADDRESS_RESULT="${TOKEN_RESULT}"
case "${NO_ADDRESS_RESULT}" in
    400\ *)
        # Good!
        ;;
    *)
        exit_fail "Expected 400 for a forged code, got: ${NO_ADDRESS_RESULT}"
        ;;
esac
echo " OK"

echo -n "Redeeming a forged code for an unknown nonce..."
# Mangle the first character; the trailing bits of the last one are
# padding, so only the leading character can be changed safely.
case "${NO_ADDRESS_NONCE}" in
    X*) UNKNOWN_NONCE="Y${NO_ADDRESS_NONCE:1}" ;;
    *)  UNKNOWN_NONCE="X${NO_ADDRESS_NONCE:1}" ;;
esac
post_token "${UNKNOWN_NONCE}-${FORGED_MAC}"
UNKNOWN_RESULT="${TOKEN_RESULT}"
echo " OK"

echo -n "Redeeming a forged code for a solved validation..."
start_validation
SOLVED_NONCE="${NONCE}"
STATUS=$(curl "${BURL}/challenge/${SOLVED_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 from /challenge. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
PIN=$(cat ${FILENAME} | awk '{print $5}')
RESULT=$(curl "${BURL}/solve/${SOLVED_NONCE}" \
    -X POST \
    -H "Accept: text/html" \
    --data-urlencode "pin=${PIN}" \
    -w "%{http_code} %{redirect_url}" -s -o $LAST_RESPONSE)
STATUS=$(echo "$RESULT" | awk '{print $1}')
if [ "$STATUS" != "302" ]
then
    exit_fail "Expected 302 from /solve. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
TCODE=$(echo "$RESULT" | awk '{print $2}' | sed -e "s/.*?code=//g" -e "s/&.*//g")
post_token "${SOLVED_NONCE}-${FORGED_MAC}"
BAD_MAC_RESULT="${TOKEN_RESULT}"
echo " OK"

echo -n "Checking /token does not distinguish the three failures..."
if [ "${NO_ADDRESS_RESULT}" != "${BAD_MAC_RESULT}" ]
then
    exit_fail "/token reveals that no address was provided:" \
              "'${NO_ADDRESS_RESULT}' vs '${BAD_MAC_RESULT}'"
fi
if [ "${UNKNOWN_RESULT}" != "${BAD_MAC_RESULT}" ]
then
    exit_fail "/token reveals that the validation is unknown:" \
              "'${UNKNOWN_RESULT}' vs '${BAD_MAC_RESULT}'"
fi
echo " OK"

# The genuine code must of course still work; the solved validation above
# was never consumed, since all we sent for it was a forgery.
echo -n "Redeeming the genuine code..."
post_token "${TCODE}"
case "${TOKEN_RESULT}" in
    200\ *)
        # Good!
        ;;
    *)
        exit_fail "Expected 200 for the genuine code, got: ${TOKEN_RESULT}"
        ;;
esac
echo " OK"

exit 0
