#!/usr/bin/env bash # 批量删除 Cloudflare 中 _acme-challenge 相关的 DNS 记录 # 用法:环境变量 或 脚本内配置 二选一,环境变量优先 # CF_API_TOKEN=xxx ZONE_NAME=jackadam.top ./scripts/cloudflare-delete-acme-challenge-dns.sh [--dry-run] # 或在下方配置中填写,直接运行 ./scripts/cloudflare-delete-acme-challenge-dns.sh [--dry-run] set -e # ---------- 脚本内配置(环境变量未设置时生效)---------- # Cloudflare API Token:需 Zone → DNS → Read + Edit # (勿将含真实 Token 的脚本提交到 Git) DEFAULT_CF_API_TOKEN="" # 区域:填 ZONE_NAME 或 ZONE_ID 其一 DEFAULT_ZONE_NAME="jackadam.top" DEFAULT_ZONE_ID="" # ------------------------------------ # 环境变量优先于脚本配置 CF_API_TOKEN="${CF_API_TOKEN:-$DEFAULT_CF_API_TOKEN}" ZONE_NAME="${ZONE_NAME:-$DEFAULT_ZONE_NAME}" ZONE_ID="${ZONE_ID:-$DEFAULT_ZONE_ID}" API_BASE="https://api.cloudflare.com/client/v4" DRY_RUN=false BATCH_SIZE=200 usage() { echo "用法: $0 [--dry-run]" echo " 方式一:环境变量 CF_API_TOKEN=xxx ZONE_NAME=jackadam.top $0" echo " 方式二:脚本内配置 在 DEFAULT_* 变量中填写后直接运行" echo " --dry-run 仅列出待删除记录,不执行删除" exit 1 } for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=true ;; -h|--help) usage ;; esac done if [[ -z "${CF_API_TOKEN}" ]]; then echo "[ERROR] 请设置 CF_API_TOKEN(环境变量或脚本内 DEFAULT_CF_API_TOKEN)" >&2 usage fi # 若未填 ZONE_ID,用 ZONE_NAME 查询 if [[ -n "${ZONE_NAME}" && -z "${ZONE_ID}" ]]; then echo "[INFO] 查询区域 ${ZONE_NAME} 的 ZONE_ID..." resp=$(curl -s -X GET "${API_BASE}/zones?name=${ZONE_NAME}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json") if ! echo "$resp" | jq -e '.success == true' >/dev/null 2>&1; then echo "[ERROR] 查询区域失败: $(echo "$resp" | jq -r '.errors[0].message // .')" >&2 exit 1 fi ZONE_ID=$(echo "$resp" | jq -r '.result[0].id // empty') if [[ -z "$ZONE_ID" ]]; then echo "[ERROR] 未找到区域: ${ZONE_NAME}" >&2 exit 1 fi echo "[INFO] ZONE_ID=${ZONE_ID}" fi if [[ -z "${ZONE_ID}" ]]; then echo "[ERROR] 请设置 ZONE_NAME 或 ZONE_ID(环境变量或脚本内 DEFAULT_ZONE_*)" >&2 usage fi # 分页获取所有 DNS 记录,筛选 _acme-challenge echo "[INFO] 获取 DNS 记录列表..." all_ids=() page=1 per_page=100 while true; do resp=$(curl -s -X GET "${API_BASE}/zones/${ZONE_ID}/dns_records?per_page=${per_page}&page=${page}" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json") if ! echo "$resp" | jq -e '.success == true' >/dev/null 2>&1; then echo "[ERROR] 获取记录失败: $(echo "$resp" | jq -r '.errors[0].message // .')" >&2 exit 1 fi # 筛选 name 包含 _acme-challenge 的记录 ids=$(echo "$resp" | jq -r '.result[] | select(.name | contains("_acme-challenge")) | .id') names=$(echo "$resp" | jq -r '.result[] | select(.name | contains("_acme-challenge")) | "\(.type) \(.name) -> \(.content)"') while IFS= read -r id; do [[ -n "$id" ]] && all_ids+=("$id") done <<< "$ids" if [[ -n "$names" ]]; then echo "$names" | while read -r line; do [[ -n "$line" ]] && echo " - $line" done fi fetched=$(echo "$resp" | jq -r '.result | length') [[ "$fetched" -lt "$per_page" ]] && break ((page++)) || true done count=${#all_ids[@]} echo "[INFO] 共找到 ${count} 条 _acme-challenge 相关记录" if [[ $count -eq 0 ]]; then echo "[INFO] 无需删除" exit 0 fi if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY-RUN] 未执行删除,以上 ${count} 条记录将在去掉 --dry-run 后被删除" exit 0 fi # 分批删除(使用 jq 构建 JSON 避免转义问题) deleted=0 for ((i=0; i0)) | {deletes: map({id: .})}') echo "[INFO] 删除第 $((i/BATCH_SIZE + 1)) 批,共 ${#batch[@]} 条..." resp=$(curl -s -X POST "${API_BASE}/zones/${ZONE_ID}/dns_records/batch" \ -H "Authorization: Bearer ${CF_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "$body") if ! echo "$resp" | jq -e '.success == true' >/dev/null 2>&1; then echo "[ERROR] 批量删除失败: $(echo "$resp" | jq -r '.errors[0].message // .')" >&2 echo "$resp" | jq '.' >&2 exit 1 fi deleted=$((deleted + ${#batch[@]})) echo "[OK] 已删除 ${deleted}/${count} 条" # 避免 API 限流 [[ $((i + BATCH_SIZE)) -lt $count ]] && sleep 1 done echo "[DONE] 共删除 ${deleted} 条 _acme-challenge 记录"