141 lines
4.6 KiB
Bash
141 lines
4.6 KiB
Bash
#!/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; i<count; i+=BATCH_SIZE)); do
|
||
batch=("${all_ids[@]:i:BATCH_SIZE}")
|
||
body=$(printf '%s\n' "${batch[@]}" | jq -R -s 'split("\n") | map(select(length>0)) | {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 记录"
|