122 lines
4.1 KiB
Bash
122 lines
4.1 KiB
Bash
#!/usr/bin/env bash
|
|
# Creates a Turnstile widget without writing credentials or the response to disk.
|
|
|
|
set +x
|
|
set -uo pipefail
|
|
|
|
need_arg() {
|
|
if [[ -z "${2-}" || "$2" == --* ]]; then
|
|
echo "widget-create: missing value for $1" >&2
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
MODE="managed"
|
|
ACCOUNT_ID=""
|
|
NAME=""
|
|
DOMAINS=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--account-id) need_arg "$1" "${2-}"; ACCOUNT_ID="$2"; shift 2 ;;
|
|
--name) need_arg "$1" "${2-}"; NAME="$2"; shift 2 ;;
|
|
--domains) need_arg "$1" "${2-}"; DOMAINS="$2"; shift 2 ;;
|
|
--mode) need_arg "$1" "${2-}"; MODE="$2"; shift 2 ;;
|
|
*) echo "widget-create: unknown arg $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN must be set}"
|
|
API_TOKEN="$CLOUDFLARE_API_TOKEN"
|
|
unset CLOUDFLARE_API_TOKEN
|
|
[[ -n "$ACCOUNT_ID" ]] || { echo "widget-create: --account-id required" >&2; exit 2; }
|
|
[[ -n "$NAME" ]] || { echo "widget-create: --name required" >&2; exit 2; }
|
|
[[ -n "$DOMAINS" ]] || { echo "widget-create: --domains required" >&2; exit 2; }
|
|
[[ "$API_TOKEN" =~ ^[A-Za-z0-9_-]+$ ]] || {
|
|
echo "widget-create: CLOUDFLARE_API_TOKEN has an invalid format" >&2
|
|
exit 1
|
|
}
|
|
case "$MODE" in
|
|
managed|invisible|non-interactive) ;;
|
|
*) echo "widget-create: unsupported mode" >&2; exit 2 ;;
|
|
esac
|
|
|
|
for command_name in curl python3; do
|
|
command -v "$command_name" >/dev/null 2>&1 || {
|
|
echo "widget-create: $command_name is required" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
BODY_JSON="$(python3 -I -c '
|
|
import json, sys
|
|
name, domains_csv, mode = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
domains = [domain.strip() for domain in domains_csv.split(",") if domain.strip()]
|
|
if not domains:
|
|
raise SystemExit(2)
|
|
print(json.dumps({"name": name, "domains": domains, "mode": mode}))
|
|
' "$NAME" "$DOMAINS" "$MODE")" || {
|
|
echo "widget-create: --domains must include at least one domain" >&2
|
|
exit 2
|
|
}
|
|
ACCOUNT_ENCODED="$(python3 -I -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$ACCOUNT_ID")"
|
|
|
|
if ! API_RESPONSE="$(
|
|
printf 'header = "Authorization: Bearer %s"\n' "$API_TOKEN" |
|
|
curl --disable --config - --silent --show-error --write-out $'\n%{http_code}' -X POST \
|
|
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ENCODED/challenges/widgets" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$BODY_JSON"
|
|
)"; then
|
|
echo "widget-create: Cloudflare API request failed" >&2
|
|
echo '{"status":"error","code":0,"message":"Cloudflare API request failed"}'
|
|
exit 1
|
|
fi
|
|
unset BODY_JSON
|
|
unset API_TOKEN
|
|
|
|
HTTP_CODE="${API_RESPONSE##*$'\n'}"
|
|
RESPONSE_BODY="${API_RESPONSE%$'\n'*}"
|
|
unset API_RESPONSE
|
|
|
|
if ! printf '%s' "$RESPONSE_BODY" | python3 -I -c '
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
http_code = sys.argv[1]
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
except Exception:
|
|
print(f"widget-create: non-JSON response (HTTP {http_code})", file=sys.stderr)
|
|
print(json.dumps({"status":"error","code":0,"message":"Cloudflare API returned an invalid response"}))
|
|
raise SystemExit(1)
|
|
|
|
errors = data.get("errors") if isinstance(data, dict) else []
|
|
first = errors[0] if isinstance(errors, list) and errors and isinstance(errors[0], dict) else {}
|
|
code = first.get("code", 0)
|
|
if not isinstance(data, dict) or data.get("success") is not True:
|
|
print(f"widget-create: request failed (HTTP {http_code}, code={code})", file=sys.stderr)
|
|
print(json.dumps({"status":"error","code":code,"message":"Cloudflare API request failed"}))
|
|
raise SystemExit(1)
|
|
|
|
result = data.get("result")
|
|
sitekey = result.get("sitekey") if isinstance(result, dict) else None
|
|
secret = result.get("secret") if isinstance(result, dict) else None
|
|
if not (
|
|
isinstance(sitekey, str)
|
|
and re.fullmatch(r"\S{1,256}", sitekey)
|
|
and isinstance(secret, str)
|
|
and re.fullmatch(r"\S{1,1024}", secret)
|
|
):
|
|
print("widget-create: API returned invalid widget credentials", file=sys.stderr)
|
|
print(json.dumps({"status":"error","code":0,"message":"Cloudflare API returned invalid widget credentials"}))
|
|
raise SystemExit(1)
|
|
|
|
print(json.dumps({"status":"ok","sitekey":sitekey,"secret":secret}))
|
|
' "$HTTP_CODE"; then
|
|
unset RESPONSE_BODY
|
|
exit 1
|
|
fi
|
|
unset RESPONSE_BODY
|