Files
vps-manager/app/adapters/cloudflare.py
T

98 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Cloudflare 适配器(REST API v4Bearer Token
API 文档:https://developers.cloudflare.com/api/
所需配置:{"api_token": "..."}(建议用 API Token,权限含 Zone:Read / Account:Read / Registrar:Read
Cloudflare 无传统 VPS,主要同步托管域名(zones);Workers/R2/Tunnel 子资产留待后续阶段。
域名到期日通过 Registrar API/accounts/{id}/registrar/domains)补充获取。
"""
import httpx
from app.adapters.base import BaseAdapter, NormalizedDomain
from app.adapters.registry import register
@register("cloudflare-api")
class CloudflareAdapter(BaseAdapter):
required_config = ["api_token"]
BASE = "https://api.cloudflare.com/client/v4"
def _headers(self) -> dict:
return {"Authorization": f"Bearer {self.config.get('api_token', '')}"}
def _get(self, path: str) -> dict:
with httpx.Client(timeout=30) as client:
resp = client.get(self.BASE + path, headers=self._headers())
resp.raise_for_status()
data = resp.json()
if not data.get("success", True):
errors = data.get("errors", [])
raise RuntimeError(errors[0].get("message") if errors else "Cloudflare API 返回失败")
return data
def test_connection(self) -> dict:
try:
data = self._get("/user/tokens/verify")
status = data.get("result", {}).get("status")
if status == "active":
return {"ok": True, "message": "API Token 有效"}
return {"ok": False, "message": f"Token 状态异常:{status}"}
except httpx.HTTPStatusError as e:
return {"ok": False, "message": f"HTTP {e.response.status_code}Token 无效或权限不足"}
except Exception as e: # noqa: BLE001
return {"ok": False, "message": str(e)}
def list_domains(self) -> list:
# 先拉取 Registrar 域名到期日映射(domain_name -> expires_at ISO 日期)
expiry_map = self._registrar_expiry_map()
result = []
page = 1
while True:
data = self._get(f"/zones?per_page=50&page={page}")
for z in data.get("result", []):
name = z.get("name")
result.append(
NormalizedDomain(
external_id=z.get("id"),
domain_name=name,
registrar="cloudflare",
expiry_date=expiry_map.get(name),
status="active" if z.get("status") == "active" else (z.get("status") or "unknown"),
raw=z,
)
)
info = data.get("result_info", {})
if page >= info.get("total_pages", 1):
break
page += 1
return result
def _registrar_expiry_map(self) -> dict:
"""拉取 Cloudflare Registrar 域名到期日映射 {domain_name: YYYY-MM-DD}
流程:/accounts → 对每个 account 调 /accounts/{id}/registrar/domains。
若 Token 无 Registrar 权限或账号无 Registrar 域名,静默返回空映射。
"""
expiry = {}
try:
accounts = self._get("/accounts?per_page=50").get("result", [])
except Exception: # noqa: BLE001
return expiry
for acc in accounts:
acc_id = acc.get("id")
if not acc_id:
continue
try:
domains = self._get(f"/accounts/{acc_id}/registrar/domains").get("result", [])
except Exception: # noqa: BLE001
continue
for d in domains:
# Registrar API 返回的 id 是域名本身(如 example.com
name = d.get("id") or d.get("domain_name") or ""
expires_at = d.get("expires_at")
if name and expires_at:
# expires_at 为 ISO8601 时间戳,截取日期部分
expiry[name] = expires_at[:10]
return expiry