#!/usr/bin/env bash
# This file is in the public domain.
#
# Tests that an oversized upload is answered with 413 on every endpoint
# that reads a request body, both when the size is announced via
# Content-Length and when it is not.
#
# Regression test: the per-field hard cap in the post processor iterators
# used to set 'too_big' and then return MHD_NO.  That puts MHD's post
# processor into an error state, so MHD_post_process() failed, the handler
# returned MHD_NO and MHD tore the connection down *without any response*.
# The 'too_big' branch that was supposed to produce the 413 was therefore
# unreachable, and a client streaming an oversized field (chunked, i.e.
# without a Content-Length that TALER_MHD_check_content_length() could
# have rejected up front) just saw the connection drop.

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

LAST_RESPONSE=$(mktemp responseXXXXXX.log)
BIG=$(mktemp bigXXXXXX.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
echo " OK"

# 5 KiB exceeds every per-field cap in the service (1 KiB for /challenge
# and /solve, 2 KiB for /token).
printf 'x%.0s' $(seq 1 5120) > "$BIG"

# $1: human-readable description, $2: URL, $3: form field to overflow,
# $4... : extra curl arguments.
function expect_413()
{
    local WHAT="$1"
    local URL="$2"
    local FIELD="$3"
    shift 3
    local STATUS
    local CODE

    STATUS=$(curl "$URL" \
        -X POST \
        -H "Content-Type: application/x-www-form-urlencoded" \
        --data-urlencode "${FIELD}@${BIG}" \
        "$@" \
        -w "%{http_code}" -s -o $LAST_RESPONSE) \
        || exit_fail "${WHAT}: connection failed instead of returning 413"
    if [ "$STATUS" != "413" ]
    then
        exit_fail "${WHAT}: expected 413. Got: $STATUS" $(cat $LAST_RESPONSE)
    fi
    CODE=$(jq -r .code < "$LAST_RESPONSE")
    # 32 == TALER_EC_GENERIC_UPLOAD_EXCEEDS_LIMIT
    if [ "$CODE" != "32" ]
    then
        exit_fail "${WHAT}: expected code 32 (UPLOAD_EXCEEDS_LIMIT). Got: $CODE" $(cat $LAST_RESPONSE)
    fi
}

# A fresh nonce for each request, so that a rejected upload can never be
# confused with a validation that ran out of attempts.
function fresh_nonce()
{
    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")
}

# curl sends a Content-Length for --data-urlencode, so these exercise the
# TALER_MHD_check_content_length() path.
echo -n "/challenge rejects an oversized announced upload ..."
fresh_nonce
expect_413 "/challenge with Content-Length" "${BURL}/challenge/${NONCE}" "filename"
echo " OK"

echo -n "/solve rejects an oversized announced upload ..."
fresh_nonce
expect_413 "/solve with Content-Length" "${BURL}/solve/${NONCE}" "pin"
echo " OK"

echo -n "/token rejects an oversized announced upload ..."
expect_413 "/token with Content-Length" "${BURL}/token" "code"
echo " OK"

# Forcing chunked transfer removes the Content-Length, so the only thing
# left to stop the upload is the per-field cap in the post iterator --
# the code path this test was written for.
echo -n "/challenge rejects an oversized chunked upload ..."
fresh_nonce
expect_413 "/challenge chunked" "${BURL}/challenge/${NONCE}" "filename" \
           -H "Transfer-Encoding: chunked"
echo " OK"

echo -n "/solve rejects an oversized chunked upload ..."
fresh_nonce
expect_413 "/solve chunked" "${BURL}/solve/${NONCE}" "pin" \
           -H "Transfer-Encoding: chunked"
echo " OK"

echo -n "/token rejects an oversized chunked upload ..."
expect_413 "/token chunked" "${BURL}/token" "code" \
           -H "Transfer-Encoding: chunked"
echo " OK"

# The daemon must still be alive and serving after all of that.
echo -n "Service survived the oversized uploads ..."
STATUS=$(curl "${BURL}/config" -w "%{http_code}" -s -o $LAST_RESPONSE)
if [ "$STATUS" != "200" ]
then
    exit_fail "Expected 200 OK from /config. Got: $STATUS" $(cat $LAST_RESPONSE)
fi
echo " OK"

exit 0
