#!/usr/bin/env bash # Webhook Monster CLI. Compatible with Bash 3.2 (macOS) and newer (WSL/Linux). set -euo pipefail # Explicit environment variables win over the shared credentials file format. env_api_key=${WM_API_KEY-} env_base_url=${WM_BASE_URL-} env_capture_base_url=${WM_CAPTURE_BASE_URL-} env_token=${WM_TOKEN-} env_api=${WM_API-} env_capture_base=${WM_CAPTURE_BASE-} credentials_explicit=${WM_CREDENTIALS+x} WM_CREDENTIALS=${WM_CREDENTIALS:-${WM_CREDENTIALS_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/webhook-monster/credentials}} if [ -r "$WM_CREDENTIALS" ]; then # This is intentionally a shell-format file so users can also source it from # their own scripts. Keep it mode 0600 because it contains WM_TOKEN. # shellcheck source=/dev/null source "$WM_CREDENTIALS" elif [ -n "$credentials_explicit" ]; then printf 'wm: credentials file is not readable: %s\n' "$WM_CREDENTIALS" >&2 exit 3 fi WM_API_KEY=${env_api_key:-${env_token:-${WM_API_KEY:-${WM_TOKEN:-}}}} WM_BASE_URL=${env_base_url:-${env_api:-${WM_BASE_URL:-${WM_API:-https://webhook-monster.gkes.pipedream.net}}}} WM_CAPTURE_BASE_URL=${env_capture_base_url:-${env_capture_base:-${WM_CAPTURE_BASE_URL:-${WM_CAPTURE_BASE:-}}}} WM_BASE_URL=${WM_BASE_URL%/} if [ -z "$WM_CAPTURE_BASE_URL" ]; then case $WM_BASE_URL in https://webhook-monster.gkes.pipedream.net) WM_CAPTURE_BASE_URL=https://wm.gkes.pipedream.net ;; *) WM_CAPTURE_BASE_URL=$WM_BASE_URL ;; esac fi WM_CAPTURE_BASE_URL=${WM_CAPTURE_BASE_URL%/} # cubicle-hands' original credentials format stores WM_CAPTURE_BASE with /b; # the newer WM_CAPTURE_BASE_URL form may store the origin. Accept both. case $WM_CAPTURE_BASE_URL in */b) WM_CAPTURE_PREFIX=$WM_CAPTURE_BASE_URL ;; *) WM_CAPTURE_PREFIX=$WM_CAPTURE_BASE_URL/b ;; esac usage() { cat <<'EOF' Usage: wm url SLUG [QUERY_STRING | --path PATH] [response options] wm bins wm new [LABEL] (alias: create [--name LABEL]) wm reqs SLUG [LIMIT] (alias: requests SLUG [--limit N]) wm show SLUG ID wm body SLUG ID wm clear SLUG wm reset-seq SLUG wm api METHOD PATH Response options for `wm url`: --status N Return status N (200-599) --seq N,N,... Return statuses in sequence; last repeats --delay-ms N Delay response, capped by server at 30000 ms --redirect URL Redirect to https://wm.gkes.pipedream.net only --redirect-status N 301, 302, 303, 307, or 308 (default 302) --content-type TYPE Response Content-Type --body BODY Response body Environment: WM_API_KEY Required for every command except `url` WM_BASE_URL App/API origin WM_CAPTURE_BASE_URL Capture origin WM_CREDENTIALS Defaults to ~/.config/webhook-monster/credentials Credentials file (compatible with the original cubicle-hands CLI): WM_API=https://webhook-monster.gkes.pipedream.net WM_CAPTURE_BASE=https://wm.gkes.pipedream.net/b WM_TOKEN=wm_xxxxx Explicit WM_TOKEN / WM_API / WM_CAPTURE_BASE (and their newer aliases WM_API_KEY / WM_BASE_URL / WM_CAPTURE_BASE_URL) override the file. Examples: wm url pine_stout --seq 503,503,204 wm reset-seq pine_stout wm requests pine_stout --limit 20 | jq EOF } die() { printf 'wm: %s\n' "$*" >&2 exit 2 } need_arg() { [ "$#" -ge 2 ] || die "missing value for $1" } valid_status() { case $1 in ''|*[!0-9]*) return 1 ;; esac [ "$1" -ge 200 ] && [ "$1" -le 599 ] } valid_seq() { local rest item rest=$1 [ -n "$rest" ] || return 1 while :; do case $rest in *,*) item=${rest%%,*}; rest=${rest#*,} ;; *) item=$rest; rest= ;; esac valid_status "$item" || return 1 [ -n "$rest" ] || break done } # Set ENCODED to the RFC 3986 percent-encoded form of $1. LC_ALL=C makes the # Bash 3 substring operations consume UTF-8 one byte at a time. urlencode() { local LC_ALL=C input char hex input=$1 ENCODED= while [ -n "$input" ]; do char=${input%"${input#?}"} input=${input#?} case $char in [a-zA-Z0-9.~_-]) ENCODED=$ENCODED$char ;; *) printf -v hex '%02X' "'$char" ENCODED=$ENCODED%$hex ;; esac done } require_key() { [ -n "${WM_API_KEY:-}" ] || die 'WM_API_KEY is required for this command' } api_raw() { local method=$1 path=$2 require_key # Feed the secret header through curl's stdin config so the token does not # appear in shell history or the process command line. printf 'header = "Authorization: Bearer %s"\n' "$WM_API_KEY" | \ curl -fsS -K - -X "$method" "$WM_BASE_URL$path" } api() { api_raw "$1" "$2" printf '\n' } add_query() { local key=$1 value=$2 urlencode "$value" if [ -z "$QUERY" ]; then QUERY="?$key=$ENCODED" else QUERY=$QUERY\&$key=$ENCODED fi } command=${1:-} [ -n "$command" ] || { usage; exit 2; } shift || true case $command in -h|--help|help) usage ;; url) [ "$#" -ge 1 ] || die 'url requires a bin slug' slug=$1 shift path= QUERY= # Compatibility with the original CLI: `wm url SLUG 'status=503'`. if [ "$#" -eq 1 ] && [ "${1#--}" = "$1" ]; then QUERY="?${1#\?}" shift fi while [ "$#" -gt 0 ]; do option=$1 need_arg "$@" value=$2 shift 2 case $option in --path) path=$value ;; --status) valid_status "$value" || die '--status must be between 200 and 599' add_query status "$value" ;; --seq) valid_seq "$value" || die '--seq must be a comma-separated list of statuses 200-599' add_query seq "$value" ;; --delay-ms) case $value in ''|*[!0-9]*) die '--delay-ms must be a non-negative integer' ;; esac add_query delay_ms "$value" ;; --redirect) case $value in https://wm.gkes.pipedream.net|https://wm.gkes.pipedream.net/*|https://wm.gkes.pipedream.net\?*|https://wm.gkes.pipedream.net\#*) ;; *) die '--redirect must target https://wm.gkes.pipedream.net' ;; esac add_query redirect "$value" ;; --redirect-status) case $value in 301|302|303|307|308) ;; *) die 'invalid --redirect-status' ;; esac add_query redirect_status "$value" ;; --content-type) add_query content_type "$value" ;; --body) add_query body "$value" ;; *) die "unknown url option: $option" ;; esac done case $path in '') ;; /*) ;; *) path=/$path ;; esac printf '%s/%s%s%s\n' "$WM_CAPTURE_PREFIX" "$slug" "$path" "$QUERY" ;; bins) [ "$#" -eq 0 ] || die 'bins takes no arguments' api GET /api/bins ;; new) [ "$#" -le 1 ] || die 'new accepts at most one label' name=${1:-} path=/api/bins if [ -n "$name" ]; then urlencode "$name" path="$path?name=$ENCODED" fi api POST "$path" ;; create) name= while [ "$#" -gt 0 ]; do case $1 in --name) need_arg "$@"; name=$2; shift 2 ;; *) die "unknown create option: $1" ;; esac done path=/api/bins if [ -n "$name" ]; then urlencode "$name" path="$path?name=$ENCODED" fi api POST "$path" ;; reqs) [ "$#" -ge 1 ] && [ "$#" -le 2 ] || die 'reqs requires a slug and optional limit' slug=$1 limit=${2:-20} case $limit in ''|*[!0-9]*) die 'limit must be an integer' ;; esac api GET "/api/bins/$slug/requests?limit=$limit" ;; requests) [ "$#" -ge 1 ] || die 'requests requires a bin slug' slug=$1 shift limit=100 while [ "$#" -gt 0 ]; do case $1 in --limit) need_arg "$@" case $2 in ''|*[!0-9]*) die '--limit must be an integer' ;; esac limit=$2 shift 2 ;; *) die "unknown requests option: $1" ;; esac done api GET "/api/bins/$slug/requests?limit=$limit" ;; show) [ "$#" -eq 2 ] || die 'show requires a bin slug and request id' api GET "/api/bins/$1/requests/$2" ;; body) [ "$#" -eq 2 ] || die 'body requires a bin slug and request id' api_raw GET "/api/bins/$1/requests/$2/body" ;; api) [ "$#" -eq 2 ] || die 'api requires a method and path' case $2 in /*) ;; *) die 'api path must start with /' ;; esac api_raw "$1" "$2" ;; clear) [ "$#" -eq 1 ] || die 'clear requires exactly one bin slug' api DELETE "/api/bins/$1/requests" ;; reset-seq) [ "$#" -eq 1 ] || die 'reset-seq requires exactly one bin slug' api POST "/api/bins/$1/reset_seq" ;; *) die "unknown command: $command (try: wm help)" ;; esac