Skip to content

Instantly share code, notes, and snippets.

@favonia
Created April 2, 2026 15:11
Show Gist options
  • Select an option

  • Save favonia/98a2a710d9ffe60f78014f1c75204f35 to your computer and use it in GitHub Desktop.

Select an option

Save favonia/98a2a710d9ffe60f78014f1c75204f35 to your computer and use it in GitHub Desktop.
A debugging helper that mimics the token-loading logic in cloudflare-ddns 1.16.1
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"os"
pathpkg "path"
"regexp"
"strings"
"time"
"github.com/cloudflare/cloudflare-go"
)
var oauthBearerRegex = regexp.MustCompile(`^[-a-zA-Z0-9._~+/]+=*$`)
const (
tokenKey1 = "CLOUDFLARE_API_TOKEN"
tokenKey2 = "CF_API_TOKEN"
tokenFileKey1 = "CLOUDFLARE_API_TOKEN_FILE"
tokenFileKey2 = "CF_API_TOKEN_FILE"
)
const hintAuthTokenNewPrefix = "Cloudflare is switching to the `CLOUDFLARE_*` prefix for its tools. Use `CLOUDFLARE_API_TOKEN` or `CLOUDFLARE_API_TOKEN_FILE` instead of `CF_*` (fully supported until 2.0.0 and then minimally supported until 3.0.0)."
const authVerifyTimeout = time.Second
type debugResult struct {
token string
tokenKey string
ok bool
}
func bulletf(format string, args ...any) {
fmt.Printf("- %s\n", fmt.Sprintf(format, args...))
}
func subBulletf(format string, args ...any) {
fmt.Printf(" - %s\n", fmt.Sprintf(format, args...))
}
func headerf(level int, format string, args ...any) {
fmt.Println()
fmt.Printf("%s %s\n", strings.Repeat("#", level), fmt.Sprintf(format, args...))
fmt.Println()
}
func main() {
headerf(1, "cloudflare-ddns Token Debug Helper")
fmt.Println("This helper mimics the token-loading logic in cloudflare-ddns 1.16.1.")
fmt.Println("After loading the token, it also runs the same Cloudflare preflight verification call.")
fmt.Println()
fmt.Println("Unlike cloudflare-ddns, this helper WILL print your API token directly to the screen.")
fmt.Println("DO NOT post the full log on GitHub. If you do, rotate your API token immediately.")
headerf(2, "Environment Variables")
for _, key := range []string{tokenKey1, tokenKey2, tokenFileKey1, tokenFileKey2, "CF_ACCOUNT_ID"} {
describeEnv(key)
}
result := readAuthTokenDebug()
if !result.ok {
headerf(2, "Final Result")
bulletf("failed to load an API token")
os.Exit(1)
}
headerf(2, "Final Result")
bulletf("loaded token from `%s`", result.tokenKey)
subBulletf("loaded token summary: %s", summarizeSecret(result.token))
runCloudflarePreflight(result.token)
}
func describeEnv(key string) {
value, ok := os.LookupEnv(key)
if !ok {
bulletf("`%s`: unset", key)
return
}
bulletf("`%s`: set", key)
subBulletf("raw summary: %s", summarizeSecret(value))
subBulletf("Go-quoted raw value: `%q`", value)
}
func summarizeSecret(value string) string {
return fmt.Sprintf("len=%d quoted=%t oauth-bearer-like=%t", len(value), tokenHasMatchingQuotes(value), oauthBearerRegex.MatchString(value))
}
func tokenHasMatchingQuotes(token string) bool {
if len(token) < 2 {
return false
}
switch {
case token[0] == '"' && token[len(token)-1] == '"':
return true
case token[0] == '\'' && token[len(token)-1] == '\'':
return true
default:
return false
}
}
func ensureTokenNotQuoted(token, tokenKey string) bool {
bulletf("check for surrounding quotes in `%s`", tokenKey)
if !tokenHasMatchingQuotes(token) {
subBulletf("ok: token does not have matching surrounding quotes")
return true
}
subBulletf("error: the token provided by `%s` appears to include surrounding quotation marks; remove the extra quotes", tokenKey)
return false
}
func readPlainAuthTokensDebug() debugResult {
token1 := os.Getenv(tokenKey1)
token2 := os.Getenv(tokenKey2)
headerf(2, "Plain Token Variables")
bulletf("inspect plain token environment variables")
subBulletf("`%s` summary: %s", tokenKey1, summarizeSecret(token1))
subBulletf("`%s` summary: %s", tokenKey2, summarizeSecret(token2))
var token, tokenKey string
switch {
case token1 == "" && token2 == "":
subBulletf("result: neither plain token variable is set")
return debugResult{ok: true}
case token1 != "" && token2 != "" && token1 != token2:
subBulletf("error: the values of `%s` and `%s` do not match; they must specify the same token", tokenKey1, tokenKey2)
return debugResult{ok: false}
case token1 != "":
token, tokenKey = token1, tokenKey1
subBulletf("selected `%s`", tokenKey1)
case token2 != "":
subBulletf("hint: %s", hintAuthTokenNewPrefix)
token, tokenKey = token2, tokenKey2
subBulletf("selected `%s`", tokenKey2)
}
if token == "YOUR-CLOUDFLARE-API-TOKEN" {
subBulletf("error: you need to provide a real API token as `%s`", tokenKey)
return debugResult{ok: false}
}
if !ensureTokenNotQuoted(token, tokenKey) {
return debugResult{ok: false}
}
subBulletf("success: selected plain token summary: %s", summarizeSecret(token))
return debugResult{token: token, tokenKey: tokenKey, ok: true}
}
func processPathDebug(path string) (relPath string, fixedPath string, ok bool) {
bulletf("process path")
subBulletf("raw value: `%q`", path)
if !strings.HasPrefix(path, "/") {
subBulletf("error: the path `%q` is not absolute; to use an absolute path, prefix it with `/`", path)
return "", "/" + path, false
}
cleanPath := pathpkg.Clean(path)
relPath = cleanPath[1:]
subBulletf("clean absolute path: `%q`", cleanPath)
subBulletf("relative path used with `os.DirFS(\"/\")`: `%q`", relPath)
return relPath, cleanPath, true
}
func readFileDebug(path string) (string, bool) {
relPath, fixedPath, ok := processPathDebug(path)
if !ok {
subBulletf("suggested fixed path: `%q`", fixedPath)
return "", false
}
body, err := fs.ReadFile(os.DirFS("/"), relPath)
bulletf("read file")
if err != nil {
subBulletf("error: failed to read `%q`: %v", fixedPath, err)
return "", false
}
trimmed := string(bytes.TrimSpace(body))
subBulletf("raw file byte length: %d", len(body))
subBulletf("trimmed token summary: %s", summarizeSecret(trimmed))
subBulletf("raw bytes Go-quoted: `%q`", string(body))
subBulletf("trimmed bytes Go-quoted: `%q`", trimmed)
return trimmed, true
}
func readAuthTokenFileDebug(key string) debugResult {
tokenFile := os.Getenv(key)
headerf(2, "Token File Variable `%s`", key)
bulletf("inspect `%s`", key)
if tokenFile == "" {
subBulletf("variable unset or empty")
return debugResult{ok: true}
}
subBulletf("path summary: %s", summarizeSecret(tokenFile))
token, ok := readFileDebug(tokenFile)
if !ok {
return debugResult{ok: false}
}
if token == "" {
subBulletf("error: the file specified by `%s` does not contain an API token", key)
return debugResult{ok: false}
}
if !ensureTokenNotQuoted(token, key) {
return debugResult{ok: false}
}
subBulletf("success: selected file token summary: %s", summarizeSecret(token))
return debugResult{token: token, tokenKey: key, ok: true}
}
func readAuthTokenFilesDebug() debugResult {
token1 := readAuthTokenFileDebug(tokenFileKey1)
if !token1.ok {
return debugResult{ok: false}
}
token2 := readAuthTokenFileDebug(tokenFileKey2)
if !token2.ok {
return debugResult{ok: false}
}
headerf(2, "Token File Sources")
bulletf("combine file token sources")
switch {
case token1.token != "" && token2.token != "" && token1.token != token2.token:
subBulletf("error: the files specified by `%s` and `%s` have conflicting tokens; their content must match", tokenFileKey1, tokenFileKey2)
return debugResult{ok: false}
case token1.token != "":
subBulletf("selected `%s`", tokenFileKey1)
return token1
case token2.token != "":
subBulletf("hint: %s", hintAuthTokenNewPrefix)
subBulletf("selected `%s`", tokenFileKey2)
return token2
default:
subBulletf("result: neither file token variable is set")
return debugResult{ok: true}
}
}
func readAuthTokenDebug() debugResult {
plain := readPlainAuthTokensDebug()
if !plain.ok {
return debugResult{ok: false}
}
file := readAuthTokenFilesDebug()
if !file.ok {
return debugResult{ok: false}
}
headerf(2, "Combined Token Sources")
bulletf("combine plain and file token sources")
var token, tokenKey string
switch {
case plain.token != "" && file.token != "" && plain.token != file.token:
subBulletf("error: the value of `%s` does not match the token found in the file specified by `%s`; they must specify the same token", plain.tokenKey, file.tokenKey)
return debugResult{ok: false}
case plain.token != "":
token, tokenKey = plain.token, plain.tokenKey
subBulletf("selected plain token from `%s`", tokenKey)
case file.token != "":
token, tokenKey = file.token, file.tokenKey
subBulletf("selected file token from `%s`", tokenKey)
default:
subBulletf("error: requires either `%s` or `%s`", tokenKey1, tokenFileKey1)
return debugResult{ok: false}
}
if !oauthBearerRegex.MatchString(token) {
subBulletf("warning: the API token appears to be invalid; it does not follow the OAuth2 bearer token format")
} else {
subBulletf("token matches the OAuth2 bearer token format check")
}
if accountID := os.Getenv("CF_ACCOUNT_ID"); accountID != "" {
subBulletf("warning: `CF_ACCOUNT_ID` is ignored since 1.14.0; summary: %s", summarizeSecret(accountID))
}
return debugResult{token: token, tokenKey: tokenKey, ok: true}
}
func runCloudflarePreflight(token string) {
headerf(2, "Cloudflare SDK Verification")
sdkHTTPClient := &http.Client{
Transport: loggingRoundTripper{
label: "SDK transport",
base: http.DefaultTransport,
},
}
bulletf("construct Cloudflare client")
client, err := cloudflare.NewWithAPIToken(token, cloudflare.HTTPClient(sdkHTTPClient))
if err != nil {
subBulletf("error: failed to prepare the Cloudflare authentication: %v", err)
os.Exit(1)
}
subBulletf("`client.base_url`: `%s`", client.BaseURL)
subBulletf("`verify_endpoint`: `%s/user/tokens/verify`", client.BaseURL)
subBulletf("`timeout`: `%v`", authVerifyTimeout)
subBulletf("`client.user_agent`: `%q`", client.UserAgent)
subBulletf("about to call `VerifyAPIToken` with the loaded token")
quickCtx, cancel := context.WithTimeout(context.Background(), authVerifyTimeout)
defer cancel()
start := time.Now()
res, err := client.VerifyAPIToken(quickCtx)
elapsed := time.Since(start)
bulletf("run SDK verify request")
subBulletf("elapsed: `%v`", elapsed)
if err == nil {
subBulletf("verify call succeeded")
subBulletf("`result.id`: `%s`", res.ID)
subBulletf("`result.status`: `%s`", res.Status)
subBulletf("`result.not_before`: `%s`", res.NotBefore.Format(time.RFC3339Nano))
subBulletf("`result.expires_on`: `%s`", res.ExpiresOn.Format(time.RFC3339Nano))
// continuing to manual verification for cross-check
runManualVerify(client, token)
return
}
subBulletf("verify call returned error: `%T`: %v", err, err)
var authorizationError *cloudflare.AuthorizationError
var authenticationError *cloudflare.AuthenticationError
var requestError *cloudflare.RequestError
var ratelimitError *cloudflare.RatelimitError
var serviceError *cloudflare.ServiceError
var notFoundError *cloudflare.NotFoundError
switch {
case errors.As(err, &authorizationError):
subBulletf("classified as `cloudflare.AuthorizationError`")
dumpCloudflareError("authorization", authorizationError)
case errors.As(err, &authenticationError):
subBulletf("classified as `cloudflare.AuthenticationError`")
dumpCloudflareError("authentication", authenticationError)
case errors.As(err, &requestError):
subBulletf("classified as `cloudflare.RequestError`")
dumpCloudflareError("request", requestError)
case errors.As(err, &ratelimitError):
subBulletf("classified as `cloudflare.RatelimitError`")
dumpCloudflareError("rate-limit", ratelimitError)
case errors.As(err, &serviceError):
subBulletf("classified as `cloudflare.ServiceError`")
dumpCloudflareError("service", serviceError)
case errors.As(err, &notFoundError):
subBulletf("classified as `cloudflare.NotFoundError`")
dumpCloudflareError("not-found", notFoundError)
default:
subBulletf("classified as non-Cloudflare typed error")
}
if quickCtx.Err() != nil {
subBulletf("context ended with: `%v`", quickCtx.Err())
}
runManualVerify(client, token)
}
type cloudflareErrorView interface {
ErrorCodes() []int
ErrorMessages() []string
RayID() string
Type() cloudflare.ErrorType
}
func dumpCloudflareError(label string, err cloudflareErrorView) {
subBulletf("`%s.type`: `%s`", label, err.Type())
subBulletf("`ray_id`: `%q`", err.RayID())
subBulletf("`error_codes`: `%v`", err.ErrorCodes())
subBulletf("`error_messages`: `%v`", err.ErrorMessages())
}
func runManualVerify(client *cloudflare.API, token string) {
headerf(2, "Manual Verification Request")
bulletf("construct manual request")
url := client.BaseURL + "/user/tokens/verify"
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
subBulletf("error: failed to create manual request: %v", err)
return
}
req.Header.Set("Authorization", "Bearer "+token)
if client.UserAgent != "" {
req.Header.Set("User-Agent", client.UserAgent)
}
req.Header.Set("Content-Type", "application/json")
dump, err := httputil.DumpRequestOut(req, true)
if err != nil {
subBulletf("error: failed to dump manual request: %v", err)
} else {
printFencedBlock("manual request dump", string(dump))
}
manualHTTPClient := &http.Client{
Transport: http.DefaultTransport,
Timeout: authVerifyTimeout,
}
start := time.Now()
resp, err := manualHTTPClient.Do(req)
elapsed := time.Since(start)
bulletf("run manual request")
subBulletf("elapsed: `%v`", elapsed)
if err != nil {
subBulletf("manual request returned error: `%T`: %v", err, err)
return
}
defer resp.Body.Close()
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
subBulletf("error: failed to dump manual response: %v", err)
return
}
printFencedBlock("manual response dump", string(respDump))
}
type loggingRoundTripper struct {
label string
base http.RoundTripper
}
func (t loggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
base := t.base
if base == nil {
base = http.DefaultTransport
}
dump, err := httputil.DumpRequestOut(req, true)
if err != nil {
bulletf("[%s] failed to dump request: %v", t.label, err)
} else {
printFencedBlock(t.label+" request dump", string(dump))
}
resp, err := base.RoundTrip(req)
if err != nil {
bulletf("[%s] round trip error: `%T`: %v", t.label, err, err)
return nil, err
}
respDump, dumpErr := httputil.DumpResponse(resp, true)
if dumpErr != nil {
bulletf("[%s] failed to dump response: %v", t.label, dumpErr)
return resp, nil
}
printFencedBlock(t.label+" response dump", string(respDump))
return resp, nil
}
func printFencedBlock(title string, text string) {
headerf(3, "%s", title)
fmt.Println("````text")
fmt.Print(text)
if !strings.HasSuffix(text, "\n") {
fmt.Println()
}
fmt.Println("````")
fmt.Println()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment