commit e81a3b4b25c6f549e67954f0eafbd7b421832e15 Author: southerntofu Date: Tue Sep 29 12:37:55 2020 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..726c3ca --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.*.sw* diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2cd5c8 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Webhook endpoints + +This repository contains the specification and tests for the forge webhook endpoints. + +# Running tests + +Running tests requires the bats framework (`apt install bats`). You can run the `test.sh` script to start the tests. If you are not running from the implementation's folder, you may pass it as first argument the path to the program starting the local server (for tests). + +``` +$ ./test.sh ~/endpoints.php/server +``` diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..281c9b7 --- /dev/null +++ b/test.sh @@ -0,0 +1,29 @@ +#! /bin/bash + +SCRIPTDIR="$(dirname "$0")" + +# Call me with the path to the script/program running the server +# Otherwise we try looking in the parent folder (if the tests are a submodule) +if [ -z "$1" ]; then + potential_server="$(readlink -m "$SCRIPTDIR"/server)" + echo $potential_server + if [ -x "$potential_server" ]; then + export FORGEHOOKENDPOINT="$potential_server" + elif [ -x "$(readlink -m "$potential_server"/../../server)" ]; then + export FORGEHOOKENDPOINT="$(readlink -m "$potential_server"/../../server)" + else + echo "test.sh SERVER" + exit 1 + fi +else + [ ! -x "$1" ] && echo "Cannot execute "$1"" && exit 2 + export FORGEHOOKENDPOINT="$(readlink -m $1)" +fi + +ORIGDIR="$(pwd)" +cd "$SCRIPTDIR" + +export FORGEHOOK="$(pwd)/tests/mock-forgehook.sh" +bats tests/*.bats + +cd "$ORIGDIR" diff --git a/tests/gitea.bats b/tests/gitea.bats new file mode 100644 index 0000000..85e1a48 --- /dev/null +++ b/tests/gitea.bats @@ -0,0 +1,46 @@ +#! /bin/bash + +function setup { + # Load helper functions + load helper + # Which forgehook implementation to use? + if [ -z "$FORGEHOOK" ]; then FORGEHOOK="forgehook"; fi + port=$(find_free_port) + [ ! -z "$FORGEHOOKENDPOINT" ] + + TMPFILE=$(mktemp) + + # Need 3>&- so bats doesn't hang because of background task + $FORGEHOOKENDPOINT $port 3>&- & + export FORGEHOOKPID="$!" + [[ $? = 0 ]] + export FORGEHOOKSRV="http://localhost:$port" +} + +function teardown { + # If setup fails, $FORGEHOOKPID will be empty so nothing to clean + if [ ! -z "$FORGEHOOKPID" ]; then + # Also kill the PID's children processes + kill $(ps -o pid= --ppid $FORGEHOOKPID) + fi + if [ -f $TMPFILE ]; then rm $TMPFILE; fi +} + +@test "correct signature works" { + repo="https://tildegit.org/forge/hook.sh" + webhook="$(gen_webhook tests/gitea.json "$repo")" + sig="$(hash_hmac sha256 "$webhook" "$($FORGEHOOK secret $repo)")" + run send_webhook "${FORGEHOOKSRV}?action=gitea" "$webhook" "$sig" "X-Gitea-Signature" + [ $status -eq 0 ] + [[ "$output" -eq "200" ]] +} + +@test "incorrect signature fails" { + repo="https://tildegit.org/forge/hook.sh" + webhook="$(gen_webhook tests/gitea.json "$repo")" + # Calculate wrong signature + sig="$(hash_hmac sha256 "EXTRA$webhook" "$($FORGEHOOK secret $repo)")" + run send_webhook "${FORGEHOOKSRV}?action=gitea" "$webhook" "$sig" "X-Gitea-Signature" + [ "$status" -eq 2 ] + [[ "$output" = "403" ]] +} diff --git a/tests/gitea.json b/tests/gitea.json new file mode 100644 index 0000000..282ddbe --- /dev/null +++ b/tests/gitea.json @@ -0,0 +1,5 @@ +{ + "repository": { + "html_url": "$repo_url" + } +} diff --git a/tests/github.bats b/tests/github.bats new file mode 100644 index 0000000..349bdaf --- /dev/null +++ b/tests/github.bats @@ -0,0 +1,46 @@ +#! /bin/bash + +function setup { + # Load helper functions + load helper + # Which forgehook implementation to use? + if [ -z "$FORGEHOOK" ]; then FORGEHOOK="forgehook"; fi + port=$(find_free_port) + [ ! -z "$FORGEHOOKENDPOINT" ] + + TMPFILE=$(mktemp) + + # Need 3>&- so bats doesn't hang because of background task + $FORGEHOOKENDPOINT $port 3>&- & + export FORGEHOOKPID="$!" + [[ $? = 0 ]] + export FORGEHOOKSRV="http://localhost:$port" +} + +function teardown { + # If setup fails, $FORGEHOOKPID will be empty so nothing to clean + if [ ! -z "$FORGEHOOKPID" ]; then + # Also kill the PID's children processes + kill $(ps -o pid= --ppid $FORGEHOOKPID) + fi + if [ -f $TMPFILE ]; then rm $TMPFILE; fi +} + +@test "correct signature works" { + repo="https://tildegit.org/forge/hook.sh" + webhook="$(gen_webhook tests/github.json "$repo")" + sig="$(hash_hmac sha256 "$webhook" "$($FORGEHOOK secret $repo)")" + run send_webhook "${FORGEHOOKSRV}?action=github" "$webhook" "$sig" "X-Hub-Signature" + [ $status -eq 0 ] + [[ "$output" -eq "200" ]] +} + +@test "incorrect signature fails" { + repo="https://tildegit.org/forge/hook.sh" + webhook="$(gen_webhook tests/gitea.json "$repo")" + # Calculate wrong signature + sig="$(hash_hmac sha256 "EXTRA$webhook" "$($FORGEHOOK secret $repo)")" + run send_webhook "${FORGEHOOKSRV}?action=github" "$webhook" "$sig" "X-Hub-Signature" + [ "$status" -eq 2 ] + [[ "$output" = "403" ]] +} diff --git a/tests/github.json b/tests/github.json new file mode 100644 index 0000000..282ddbe --- /dev/null +++ b/tests/github.json @@ -0,0 +1,5 @@ +{ + "repository": { + "html_url": "$repo_url" + } +} diff --git a/tests/gitlab.bats b/tests/gitlab.bats new file mode 100644 index 0000000..d6759f5 --- /dev/null +++ b/tests/gitlab.bats @@ -0,0 +1,44 @@ +#! /bin/bash + +function setup { + # Load helper functions + load helper + # Which forgehook implementation to use? + if [ -z "$FORGEHOOK" ]; then FORGEHOOK="forgehook"; fi + port=$(find_free_port) + [ ! -z "$FORGEHOOKENDPOINT" ] + + TMPFILE=$(mktemp) + + # Need 3>&- so bats doesn't hang because of background task + $FORGEHOOKENDPOINT $port 3>&- & + export FORGEHOOKPID="$!" + [[ $? = 0 ]] + export FORGEHOOKSRV="http://localhost:$port" +} + +function teardown { + # If setup fails, $FORGEHOOKPID will be empty so nothing to clean + if [ ! -z "$FORGEHOOKPID" ]; then + # Also kill the PID's children processes + kill $(ps -o pid= --ppid $FORGEHOOKPID) + fi + if [ -f $TMPFILE ]; then rm $TMPFILE; fi +} + +@test "correct token works" { + repo="https://tildegit.org/forge/hook.sh" + webhook="$(gen_webhook tests/gitlab.json "$repo")" + run send_webhook "${FORGEHOOKSRV}?action=gitlab" "$webhook" "$($FORGEHOOK secret $repo)" "X-Gitlab-Token" + [ $status -eq 0 ] + [[ "$output" = "200" ]] +} + +@test "incorrect token fails" { + repo="https://tildegit.org/forge/hook.sh" + webhook="$(gen_webhook tests/gitlab.json "$repo")" + # Send FAKE token + run send_webhook "${FORGEHOOKSRV}?action=gitlab" "$webhook" "FAKE" "X-Gitlab-Token" + [ "$status" -eq 2 ] + [[ "$output" = "403" ]] +} diff --git a/tests/gitlab.json b/tests/gitlab.json new file mode 100644 index 0000000..4fac00b --- /dev/null +++ b/tests/gitlab.json @@ -0,0 +1,5 @@ +{ + "project": { + "git_http_url": "$repo_url" + } +} diff --git a/tests/helper.bash b/tests/helper.bash new file mode 100755 index 0000000..7108bc1 --- /dev/null +++ b/tests/helper.bash @@ -0,0 +1,59 @@ +#! /bin/bash + +# https://unix.stackexchange.com/a/423052 +function find_free_port { + comm -23 <(seq 49152 65535 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 1 +} + +# gen_webhook "REPO_URL" +#function gen_webhook { +# echo "{ \"project\": { \"git_http_url\": \"$1\" } }" +#} + +function gen_webhook() { + export repo_url="$2" + envsubst < "$1" +} + +# send_webhook "ENDPOINT" "PAYLOAD" "SECRET" "HEADER" +# ENDPOINT: where to send the request +# PAYLOAD: POST body +# SECRET: the secret for this transaction +# HEADER: where to store the secret +function send_webhook { + echo "$2" > $TMPFILE + + # We can make a few attempts, just in case the webserver hasn't started yet + n=0 + while [[ "$status" != "0" ]]; do + if [ $n -eq 3 ]; then + # Failed to reach server after 3 attempts + return 1; + fi + # --data-binary so that newlines aren't broken + # (otherwise, signature won't match) + run curl --header "Content-Type: application/json" \ + --header ""$4": "$3"" \ + --request POST \ + --data-binary @$TMPFILE \ + -s -w "%{http_code}" \ + "$1" + # Requested succeeded, break out of loop + if [ $status -eq 0 ]; then + echo "$output" + if [[ ! "$output" = 200 ]]; then return 2; fi + return 0; + fi + ((n++)) + done +} + +# https://stackoverflow.com/a/7385197 +function hash_hmac { + digest="$1" + data="$2" + key="$3" + shift 3 + # Don't print (stdin)= ... + echo -n "$data" | openssl dgst "-$digest" -hmac "$key" | awk '{print $2}' +} diff --git a/tests/mock-forgehook.sh b/tests/mock-forgehook.sh new file mode 100755 index 0000000..0502ab1 --- /dev/null +++ b/tests/mock-forgehook.sh @@ -0,0 +1,10 @@ +#! /bin/bash + +# This script mocks forgehook for use by webhook endpoints, only for testing purposes +# It returns a secret being simply the hashed URL of the repo + +[[ "$#" != 2 ]] && echo "WRONG ARGUMENTS ($#): $@" && exit 1 + +[[ "$1" != "secret" ]] && echo "UNIMPLEMENTED" && exit 2 + +echo -n "$2" | sha256sum | cut -d ' ' -f 1