243 lines
12 KiB
Bash
Executable File
243 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
||
# Cloudflare 固定域名 + 隧道一键初始化(全自动)
|
||
#
|
||
# 功能:自动创建/确认(全部幂等):
|
||
# 1. Cloudflare zone(paygateway.de5.net 全托管)
|
||
# 2. DNS CNAME 记录(proxied → 隧道)
|
||
# 3. hostname route(域名 → 隧道映射)
|
||
# 4. DNSHe 子域名(不存在时自动注册) + NS 委派给 Cloudflare
|
||
# 5. 等待 zone 激活
|
||
# 6. Universal SSL 证书(自动签发,toggle 触发)
|
||
# 7. HTTPS 公网验证
|
||
#
|
||
# 前置条件:
|
||
# 1. go build -o gateway ./cmd/gateway/ (编译产物)
|
||
# 2. .env 包含以下变量:
|
||
# CF_EMAIL, CF_GLOBAL_KEY, CF_TUNNEL_HOSTNAME, CF_TUNNEL_CREDENTIALS
|
||
# DNSHE_API_KEY, DNSHE_API_SECRET (de5.net 免费子域名场景)
|
||
#
|
||
# 使用:
|
||
# bash scripts/setup-domain.sh
|
||
#
|
||
# 换环境流程:复制 .env → go build → 本脚本 → ./start.sh
|
||
# 全新域名:把 CF_TUNNEL_HOSTNAME 换成新前缀(如 myapp.de5.net),脚本全自动注册+配置。
|
||
|
||
set -euo pipefail
|
||
|
||
cd "$(dirname "$0")/.."
|
||
|
||
# 加载 .env
|
||
if [ -f .env ]; then
|
||
set -a
|
||
source .env
|
||
set +a
|
||
fi
|
||
|
||
: "${CF_EMAIL:?请设置 CF_EMAIL}"
|
||
: "${CF_GLOBAL_KEY:?请设置 CF_GLOBAL_KEY}"
|
||
: "${CF_TUNNEL_HOSTNAME:?请设置 CF_TUNNEL_HOSTNAME}"
|
||
: "${CF_TUNNEL_CREDENTIALS:?请设置 CF_TUNNEL_CREDENTIALS}"
|
||
|
||
# 从凭据中提取 TunnelID
|
||
TUNNEL_ID=$(echo "$CF_TUNNEL_CREDENTIALS" | base64 -d 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['TunnelID'])" 2>/dev/null || echo "")
|
||
if [ -z "$TUNNEL_ID" ]; then
|
||
echo "❌ 无法从 CF_TUNNEL_CREDENTIALS 中提取 TunnelID,请确认格式正确"
|
||
exit 1
|
||
fi
|
||
|
||
CF_AUTH=(-H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_KEY" -H "Content-Type: application/json")
|
||
echo "=== Cloudflare 初始化: $CF_TUNNEL_HOSTNAME (隧道 $TUNNEL_ID) ==="
|
||
|
||
# 1. 确保 zone 存在
|
||
echo "--- 1/7 检查 zone: $CF_TUNNEL_HOSTNAME ---"
|
||
ZONE_ID=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones?name=$CF_TUNNEL_HOSTNAME" "${CF_AUTH[@]}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result'][0]['id'] if d.get('result') else '')" 2>/dev/null || echo "")
|
||
if [ -z "$ZONE_ID" ]; then
|
||
echo " zone 不存在,正在创建..."
|
||
ZONE_ID=$(curl -sS -m 20 -X POST "https://api.cloudflare.com/client/v4/zones" \
|
||
"${CF_AUTH[@]}" \
|
||
-d "{\"name\":\"$CF_TUNNEL_HOSTNAME\",\"type\":\"full\"}" | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['result']['id'] if d.get('success') else '')" 2>/dev/null || echo "")
|
||
if [ -z "$ZONE_ID" ]; then
|
||
echo "❌ zone 创建失败(可能是 TLD 或重复),跳过"
|
||
else
|
||
echo " ✅ zone 已创建 (ID: $ZONE_ID)"
|
||
fi
|
||
else
|
||
echo " ✅ zone 已存在 (ID: $ZONE_ID)"
|
||
fi
|
||
|
||
if [ -z "$ZONE_ID" ]; then
|
||
echo "❌ 无 zone ID,无法继续。请检查域名是否可被 Cloudflare 托管"
|
||
exit 1
|
||
fi
|
||
|
||
# 2. 确保 DNS CNAME 记录(proxied=true)
|
||
echo "--- 2/7 检查 CNAME 记录 ---"
|
||
EXISTING=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?type=CNAME&name=$CF_TUNNEL_HOSTNAME" "${CF_AUTH[@]}" | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',[]); print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
|
||
if [ -z "$EXISTING" ]; then
|
||
echo " CNAME 不存在,创建中..."
|
||
curl -sS -m 15 -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
|
||
"${CF_AUTH[@]}" \
|
||
-d "{\"type\":\"CNAME\",\"name\":\"@\",\"content\":\"$TUNNEL_ID.cfargotunnel.com\",\"proxied\":true}" > /dev/null
|
||
echo " ✅ CNAME 已创建 (proxied=true)"
|
||
else
|
||
echo " ✅ CNAME 已存在"
|
||
fi
|
||
|
||
# 3. 确保 hostname route
|
||
echo "--- 3/7 检查 hostname route ---"
|
||
ACT_ID=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/accounts?limit=1" "${CF_AUTH[@]}" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])" 2>/dev/null || echo "")
|
||
if [ -n "$ACT_ID" ]; then
|
||
ROUTE=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/accounts/$ACT_ID/zerotrust/routes/hostname" "${CF_AUTH[@]}" | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x['hostname']=='$CF_TUNNEL_HOSTNAME']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
|
||
if [ -z "$ROUTE" ]; then
|
||
echo " hostname route 不存在,创建中..."
|
||
curl -sS -m 15 -X POST "https://api.cloudflare.com/client/v4/accounts/$ACT_ID/zerotrust/routes/hostname" \
|
||
"${CF_AUTH[@]}" \
|
||
-d "{\"hostname\":\"$CF_TUNNEL_HOSTNAME\",\"tunnel_id\":\"$TUNNEL_ID\",\"tun_type\":\"cfd_tunnel\",\"comment\":\"setup-domain.sh\"}" > /dev/null
|
||
echo " ✅ hostname route 已创建"
|
||
else
|
||
echo " ✅ hostname route 已存在"
|
||
fi
|
||
fi
|
||
|
||
# 4. DNSHe NS 委派(de5.net 免费子域名场景)
|
||
# de5.net 的 DNS 在 DNSHe,需把 paygateway.de5.net 的 NS 委派给 Cloudflare。
|
||
# 若 DNSHE_API_KEY 已配置则自动处理;未配置则提示手动。
|
||
echo "--- 4/7 DNSHe NS 委派 ---"
|
||
if [ -n "${DNSHE_API_KEY:-}" ] && [ -n "${DNSHE_API_SECRET:-}" ]; then
|
||
DNSHE_BASE="${DNSHE_API_BASE:-https://api005.dnshe.com}"
|
||
DNSHE_AUTH=(-H "X-API-Key: $DNSHE_API_KEY" -H "X-API-Secret: $DNSHE_API_SECRET")
|
||
# 提取子域前缀(paygateway.de5.net → paygateway)
|
||
SUB_PREFIX="${CF_TUNNEL_HOSTNAME%%.*}"
|
||
ROOT_DOMAIN="${CF_TUNNEL_HOSTNAME#*.}"
|
||
echo " 子域: $SUB_PREFIX (根域: $ROOT_DOMAIN)"
|
||
# 列出子域名,找 subdomain_id
|
||
SUB_ID=$(curl -sS -m 15 "${DNSHE_AUTH[@]}" \
|
||
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=subdomains&action=list" 2>/dev/null | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('subdomains',[]) if x['full_domain']=='$CF_TUNNEL_HOSTNAME']; print(r[0]['id'] if r else '')" 2>/dev/null || echo "")
|
||
if [ -z "$SUB_ID" ]; then
|
||
# 子域名不存在 → 自动注册(DNSHe API 支持)
|
||
echo " 子域名 $CF_TUNNEL_HOSTNAME 不存在,自动注册..."
|
||
SUB_ID=$(curl -sS -m 20 -X POST "${DNSHE_AUTH[@]}" \
|
||
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=subdomains&action=create" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"subdomain\":\"$SUB_PREFIX\",\"domain\":\"$ROOT_DOMAIN\"}" 2>/dev/null | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('subdomain_id',''))" 2>/dev/null || echo "")
|
||
if [ -n "$SUB_ID" ]; then
|
||
echo " ✅ 子域名已注册: $CF_TUNNEL_HOSTNAME (id=$SUB_ID)"
|
||
else
|
||
echo " ❌ 子域名注册失败(配额可能已满,DNSHe 免费额度 3 个)"
|
||
fi
|
||
fi
|
||
if [ -n "$SUB_ID" ]; then
|
||
# 取 Cloudflare 分配的 NS(kevin/reza.ns.cloudflare.com)
|
||
CF_NS=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" 2>/dev/null | \
|
||
python3 -c "import sys,json; r=json.load(sys.stdin).get('result',{}); ns=r.get('name_servers',[]); print(' '.join(ns) if ns else '')" 2>/dev/null || echo "")
|
||
if [ -z "$CF_NS" ]; then
|
||
echo " ⚠️ 无法获取 Cloudflare NS,跳过 DNSHe 自动委派"
|
||
else
|
||
echo " Cloudflare NS: $CF_NS"
|
||
# 检查当前 NS 记录是否已是 Cloudflare 的
|
||
NS_CHECK=$(curl -sS -m 15 "${DNSHE_AUTH[@]}" \
|
||
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=list&subdomain_id=$SUB_ID" 2>/dev/null | \
|
||
python3 -c "
|
||
import sys,json
|
||
d=json.load(sys.stdin)
|
||
ns1,ns2='$CF_NS'.split()[:2]
|
||
ok=all(any(r.get('type')=='NS' and r.get('content')==ns for r in d.get('records',[])) for ns in (ns1,ns2))
|
||
print('ok' if ok else 'need')
|
||
" 2>/dev/null || echo "need")
|
||
if [ "$NS_CHECK" = "ok" ]; then
|
||
echo " ✅ NS 委派已就绪 ($CF_NS)"
|
||
else
|
||
echo " NS 委派缺失,自动配置中..."
|
||
# 删除现有 CNAME/非 NS 记录(NS 委派与 CNAME 冲突)
|
||
curl -sS -m 15 "${DNSHE_AUTH[@]}" \
|
||
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=list&subdomain_id=$SUB_ID" 2>/dev/null | \
|
||
python3 -c "
|
||
import sys,json
|
||
d=json.load(sys.stdin)
|
||
for r in d.get('records',[]):
|
||
print(r.get('id'))
|
||
" 2>/dev/null | while read rid; do
|
||
curl -sS -m 15 -X POST "${DNSHE_AUTH[@]}" \
|
||
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=delete" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"record_id\":\"$rid\"}" > /dev/null 2>&1
|
||
done
|
||
# 添加两条 NS 记录
|
||
for ns in $CF_NS; do
|
||
curl -sS -m 20 -X POST "${DNSHE_AUTH[@]}" \
|
||
"https://${DNSHE_BASE#https://}/index.php?m=domain_hub&endpoint=dns_records&action=create" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"subdomain_id\": $SUB_ID, \"type\": \"NS\", \"content\": \"$ns\", \"ttl\": 600}" > /dev/null 2>&1
|
||
done
|
||
echo " ✅ NS 委派已配置: $CF_NS"
|
||
fi
|
||
fi
|
||
else
|
||
echo " ⚠️ DNSHe 未找到子域名 $CF_TUNNEL_HOSTNAME,跳过(请确认已注册)"
|
||
fi
|
||
else
|
||
NS_HINT=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" 2>/dev/null | \
|
||
python3 -c "import sys,json; print(' '.join(json.load(sys.stdin).get('result',{}).get('name_servers',[])))" 2>/dev/null || echo "")
|
||
echo " ⚠️ 未配置 DNSHE_API_KEY/SECRET,跳过自动委派"
|
||
echo " 如需手动:dns.he.net → $CF_TUNNEL_HOSTNAME → NS 指向 $NS_HINT"
|
||
fi
|
||
|
||
# 5. 确保 DNS 委派传播 + zone active
|
||
echo "--- 5/7 等待 zone 激活 (最多 60s) ---"
|
||
for i in $(seq 1 12); do
|
||
STATUS=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID" "${CF_AUTH[@]}" | \
|
||
python3 -c "import sys,json; print(json.load(sys.stdin)['result']['status'])" 2>/dev/null || echo "pending")
|
||
if [ "$STATUS" = "active" ]; then
|
||
echo " ✅ zone 已激活"
|
||
break
|
||
fi
|
||
sleep 5
|
||
done
|
||
|
||
# 6. 确保 Universal SSL
|
||
echo "--- 6/7 检查 Universal SSL ---"
|
||
CERT=$(curl -sS -m 15 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/certificate_packs?status=all" "${CF_AUTH[@]}" | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x.get('type')=='universal' and x.get('status')=='active']; print('ok' if r else '')" 2>/dev/null || echo "")
|
||
if [ -z "$CERT" ]; then
|
||
echo " SSL 未签发,尝试 toggle 触发..."
|
||
curl -sS -m 10 -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/universal/settings" \
|
||
"${CF_AUTH[@]}" -d '{"enabled":false}' > /dev/null 2>&1
|
||
sleep 10
|
||
curl -sS -m 10 -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/universal/settings" \
|
||
"${CF_AUTH[@]}" -d '{"enabled":true}' > /dev/null 2>&1
|
||
echo " ⏳ Universal SSL toggle 完成,等待签发(通常 1-5 分钟)..."
|
||
for i in $(seq 1 30); do
|
||
STATUS=$(curl -sS -m 10 "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/ssl/certificate_packs?status=all" "${CF_AUTH[@]}" | \
|
||
python3 -c "import sys,json; d=json.load(sys.stdin); r=[x for x in d.get('result',[]) if x.get('status')=='active']; print(r[0]['status'] if r else 'pending')" 2>/dev/null || echo "pending")
|
||
if [ "$STATUS" = "active" ]; then
|
||
echo " ✅ Universal SSL 已签发!"
|
||
break
|
||
fi
|
||
sleep 10
|
||
done
|
||
else
|
||
echo " ✅ Universal SSL 已签发"
|
||
fi
|
||
|
||
# 7. 验证 HTTPS
|
||
echo "--- 7/7 验证 HTTPS ---"
|
||
sleep 5
|
||
HTTP_CODE=$(curl -sS -m 15 -o /dev/null -w "%{http_code}" "https://$CF_TUNNEL_HOSTNAME/health" 2>/dev/null || echo "000")
|
||
if [ "$HTTP_CODE" = "200" ]; then
|
||
echo " ✅ https://$CF_TUNNEL_HOSTNAME/health → HTTP 200"
|
||
echo ""
|
||
echo "============================================"
|
||
echo " 初始化完成! 固定域名已就绪"
|
||
echo " URL: https://$CF_TUNNEL_HOSTNAME"
|
||
echo " 隧道: $TUNNEL_ID"
|
||
echo "============================================"
|
||
else
|
||
echo " ⚠️ HTTPS 返回 HTTP $HTTP_CODE,可能需等待 DNS 传播"
|
||
echo " 隧道进程需手动确认: ps aux | grep cloudflared"
|
||
fi
|