#!/usr/bin/env bash
# This file is in the public domain.
#
# Regression test for finding 13: 'GET /info' must implement the RFC 6750
# bearer-token error contract.
#
# A missing 'Authorization' header, a token that does not decode and a
# well-formed but unknown token were answered 403, 403 and 404
# respectively, never 401, and never with a 'WWW-Authenticate' header --
# so a conforming OAuth client cannot tell "refresh your token" from
# "resource gone", and the 403/404 split reveals whether a guessed token
# is well-formed before revealing whether it exists.
#
# All of these must now be 401 with a 'WWW-Authenticate: Bearer error=...'
# header, and the malformed and the unknown token must be answered
# identically.

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

LAST_RESPONSE=$(mktemp responseXXXXXX.log)
LAST_HEADERS=$(mktemp headersXXXXXX.log)

# 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/"
CLIENT_SECRET="secret-token:secret"
CLIENT_ID=1

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

echo -n "Add challenger client ..."
challenger-admin -c "${CONF}" -a "${CLIENT_SECRET}" "${REDIRECT_URI}" &> admin.log
echo " OK"

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"

# Check that the last response carried a 'WWW-Authenticate: Bearer' header
# naming the given OAuth error, with the realm and a description as
# required/recommended by RFC 6750 section 3.
function check_www_authenticate() {
    if ! grep -qi "^WWW-Authenticate:" "$LAST_HEADERS"
    then
        exit_fail "missing WWW-Authenticate header: $(cat $LAST_HEADERS)"
    fi
    if ! grep -qi "^WWW-Authenticate:.*Bearer.*realm=\"challenger\"" "$LAST_HEADERS"
    then
        exit_fail "expected WWW-Authenticate to carry realm=\"challenger\". Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)"
    fi
    if ! grep -qi "^WWW-Authenticate:.*Bearer.*error=\"$1\"" "$LAST_HEADERS"
    then
        exit_fail "expected WWW-Authenticate 'Bearer error=\"$1\"'. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)"
    fi
    if ! grep -qi "^WWW-Authenticate:.*error_description=\"" "$LAST_HEADERS"
    then
        exit_fail "expected WWW-Authenticate to carry an error_description. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)"
    fi
}

# RFC 6750 section 3: "If the request lacks any authentication information
# [...] the resource server SHOULD NOT include an error code."  So this
# challenge must carry the realm and nothing else.
echo -n "/info without an Authorization header ..."
STATUS=$(curl "${BURL}/info" \
    -D "$LAST_HEADERS" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)
if [ "$STATUS" != "401" ]
then
    exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
if ! grep -qi "^WWW-Authenticate:.*Bearer.*realm=\"challenger\"" "$LAST_HEADERS"
then
    exit_fail "expected WWW-Authenticate 'Bearer realm=\"challenger\"'. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)"
fi
if grep -qi "^WWW-Authenticate:.*error=" "$LAST_HEADERS"
then
    exit_fail "challenge for a request without credentials must not name an error code. Got: $(grep -i '^WWW-Authenticate:' $LAST_HEADERS)"
fi
echo " OK"

echo -n "/info with a non-Bearer Authorization header ..."
STATUS=$(curl "${BURL}/info" \
    -H "Authorization: Basic dXNlcjpwYXNz" \
    -D "$LAST_HEADERS" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)
if [ "$STATUS" != "401" ]
then
    exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
check_www_authenticate "invalid_request"
echo " OK"

echo -n "/info with a token that does not decode ..."
STATUS=$(curl "${BURL}/info" \
    -H "Authorization: Bearer not-a-base32-token" \
    -D "$LAST_HEADERS" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)
if [ "$STATUS" != "401" ]
then
    exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
check_www_authenticate "invalid_token"
MALFORMED_STATUS="$STATUS"
MALFORMED_BODY=$(cat "$LAST_RESPONSE")
echo " OK"

echo -n "/info with a well-formed but unknown token ..."
# 32 zero bytes in GNUnet's base32 alphabet
UNKNOWN_TOKEN=$(printf '0%.0s' $(seq 1 52))
STATUS=$(curl "${BURL}/info" \
    -H "Authorization: Bearer ${UNKNOWN_TOKEN}" \
    -D "$LAST_HEADERS" \
    -w "%{http_code}" -s -o $LAST_RESPONSE)
if [ "$STATUS" != "401" ]
then
    exit_fail "Expected 401 Unauthorized. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
check_www_authenticate "invalid_token"
echo " OK"

echo -n "Malformed and unknown tokens must be indistinguishable ..."
if [ "$STATUS" != "$MALFORMED_STATUS" ] || \
   [ "$(cat $LAST_RESPONSE)" != "$MALFORMED_BODY" ]
then
    exit_fail "a malformed token is answered differently from an unknown one: '$MALFORMED_BODY' vs '$(cat $LAST_RESPONSE)'"
fi
echo " OK"

exit 0
