Setting the file. One moment. Validate · Turnstile Spin · cloudflare/skills · Skills Docs11.11
README
scripts/validate.sh
scripts/validate.sh
Shell·137 lines·4 KB
13
need_arg
() {
14 if [[ -z "${2-}" || "$2" == --* ]]; then
15 usage
16 fi
17}
18
19SITEKEY=""
20ACCOUNT_ID=""
21EXPECTED_DOMAINS_JSON=""
22
23while [[ $# -gt 0 ]]; do
24 case "$1" in
25 --sitekey)
26 need_arg "$1" "${2-}"
27 SITEKEY="$2"
28 shift 2
29 ;;
30 --account-id)
31 need_arg "$1" "${2-}"
32 ACCOUNT_ID="$2"
33 shift 2
34 ;;
35 --expected-domains)
36 need_arg "$1" "${2-}"
37 EXPECTED_DOMAINS_JSON="$2"
38 shift 2
39 ;;
40 *) usage ;;
41 esac
42done
43
44[[ -n "$SITEKEY" && -n "$ACCOUNT_ID" && -n "$EXPECTED_DOMAINS_JSON" ]] || usage
45: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN must be set}"
46API_TOKEN="$CLOUDFLARE_API_TOKEN"
47unset CLOUDFLARE_API_TOKEN
48[[ "$API_TOKEN" =~ ^[A-Za-z0-9_-]+$ ]] || {
49 echo "validate: CLOUDFLARE_API_TOKEN has an invalid format" >&2
50 exit 1
51}
52
53for command_name in curl jq python3; do
54 command -v "$command_name" >/dev/null 2>&1 || {
55 echo "validate: $command_name is required" >&2
56 exit 1
57 }
58done
59
60if ! jq -e '
61 type == "array" and
62 length > 0 and
63 all(.[]; type == "string" and length > 0)
64' <<<"$EXPECTED_DOMAINS_JSON" >/dev/null; then
65 echo "validate: --expected-domains must be a non-empty JSON array of domains" >&2
66 exit 2
67fi
68
69WIDGET_SECRET=""
70IFS= read -r -d '' WIDGET_SECRET || true
71trap 'unset API_TOKEN WIDGET_SECRET WIDGET_API_SECRET WIDGET_RESPONSE SITEVERIFY_RESPONSE' EXIT
72
73if [[ -z "$WIDGET_SECRET" || "$WIDGET_SECRET" =~ [[:space:]] ]]; then
74 echo "validate: standard input must contain one non-empty secret without whitespace" >&2
75 exit 1
76fi
77
78ACCOUNT_ENCODED="$(python3 -I -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$ACCOUNT_ID")"
79SITEKEY_ENCODED="$(python3 -I -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$SITEKEY")"
80
81if ! WIDGET_RESPONSE="$(
82 printf 'header = "Authorization: Bearer %s"\n' "$API_TOKEN" |
83 curl --disable --config - --fail --silent --show-error \
84 "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ENCODED/challenges/widgets/$SITEKEY_ENCODED"
85)"; then
86 echo "validate: widget metadata lookup failed" >&2
87 exit 1
88fi
89
90if ! printf '%s' "$WIDGET_RESPONSE" | jq -e --arg sitekey "$SITEKEY" --argjson expected "$EXPECTED_DOMAINS_JSON" '
91 . as $widget
92 | (.success == true) and
93 (.result.sitekey == $sitekey) and
94 ((.result.clearance_level | type) == "string") and
95 (.result.clearance_level as $clearance | ["no_clearance", "interactive", "managed", "jschallenge"] | index($clearance) != null) and
96 ((.result.domains | type) == "array") and
97 (all($expected[]; . as $domain | $widget.result.domains | index($domain) != null))
98' >/dev/null; then
99 echo "validate: widget sitekey, domains, or clearance level was invalid" >&2
100 exit 1
101fi
102
103if ! WIDGET_API_SECRET="$(printf '%s' "$WIDGET_RESPONSE" | jq -er '.result.secret | select(type == "string" and test("^\\S+$"))')"; then
104 echo "validate: widget metadata did not include a valid secret" >&2
105 exit 1
106fi
107if [[ "$WIDGET_API_SECRET" != "$WIDGET_SECRET" ]]; then
108 echo "validate: secret does not belong to the requested sitekey" >&2
109 exit 1
110fi
111unset WIDGET_API_SECRET
112unset WIDGET_RESPONSE
113
114if ! SITEVERIFY_RESPONSE="$(
115 printf '%s' "$WIDGET_SECRET" |
116 python3 -I -c 'import sys,urllib.parse; print(urllib.parse.urlencode({"secret":sys.stdin.read(),"response":"XXXX.DUMMY.TOKEN.XXXX"}),end="")' |
117 curl --disable --fail --silent --show-error \
118 "https://challenges.cloudflare.com/turnstile/v0/siteverify" \
119 -H "Content-Type: application/x-www-form-urlencoded" \
120 --data-binary @-
121)"; then
122 echo "validate: dummy-token siteverify request failed" >&2
123 exit 1
124fi
125
126if ! jq -e '
127 (.success == false) and
128 ((.["error-codes"] | type) == "array") and
129 ((.["error-codes"] | index("invalid-input-response")) != null) and
130 ((.["error-codes"] | index("invalid-input-secret")) == null)
131' <<<"$SITEVERIFY_RESPONSE" >/dev/null; then
132 echo "validate: siteverify did not confirm the widget secret" >&2
133 exit 1
134fi
135
136unset WIDGET_SECRET SITEVERIFY_RESPONSE
137echo '{"status":"ok","metadata_check":"ran","dummy_siteverify":"ran"}'