Files
mrlee 278f8267b7 chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听)
- 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP)
- 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点)
- 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
2026-09-23 09:40:59 +08:00

165 lines
4.5 KiB
Go

package ddns
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Cloudflare struct {
client *http.Client
}
func NewCloudflare() *Cloudflare {
return &Cloudflare{client: &http.Client{Timeout: 15 * time.Second}}
}
func (c *Cloudflare) Name() string { return "cloudflare" }
func (c *Cloudflare) Verify(ctx context.Context, cred Credentials) error {
_, err := c.request(ctx, cred.Token, http.MethodGet, "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
return err
}
func (c *Cloudflare) HasZone(ctx context.Context, cred Credentials, zone string) (bool, error) {
_, err := c.zoneID(ctx, cred.Token, zone)
return zoneLookupOK(err)
}
func (c *Cloudflare) GetRecordIP(ctx context.Context, cred Credentials, rootDomain, recordName, recordType string) (string, error) {
zoneID, err := c.zoneID(ctx, cred.Token, rootDomain)
if err != nil {
return "", err
}
fqdn := fqdnForRecord(rootDomain, recordName)
body, err := c.request(ctx, cred.Token, http.MethodGet, fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, recordType, fqdn), nil)
if err != nil {
return "", err
}
var resp cfRecordsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return "", err
}
if len(resp.Result) == 0 {
return "", nil
}
return resp.Result[0].Content, nil
}
func (c *Cloudflare) UpdateRecord(ctx context.Context, cred Credentials, rootDomain, recordName, recordType, ip string) error {
zoneID, err := c.zoneID(ctx, cred.Token, rootDomain)
if err != nil {
return err
}
fqdn := fqdnForRecord(rootDomain, recordName)
existingID, _, err := c.findRecord(ctx, cred.Token, zoneID, recordType, fqdn)
if err != nil {
return err
}
payload := map[string]any{
"type": recordType,
"name": fqdn,
"content": ip,
"ttl": 120,
"proxied": false,
}
if existingID == "" {
_, err = c.request(ctx, cred.Token, http.MethodPost, fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID), payload)
return err
}
_, err = c.request(ctx, cred.Token, http.MethodPut, fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, existingID), payload)
return err
}
func (c *Cloudflare) zoneID(ctx context.Context, token, rootDomain string) (string, error) {
body, err := c.request(ctx, token, http.MethodGet, fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", rootDomain), nil)
if err != nil {
return "", err
}
var resp cfZonesResponse
if err := json.Unmarshal(body, &resp); err != nil {
return "", err
}
if len(resp.Result) == 0 {
return "", fmt.Errorf("未找到域名 %s 对应的 Cloudflare Zone", rootDomain)
}
return resp.Result[0].ID, nil
}
func (c *Cloudflare) findRecord(ctx context.Context, token, zoneID, recordType, fqdn string) (string, string, error) {
body, err := c.request(ctx, token, http.MethodGet, fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, recordType, fqdn), nil)
if err != nil {
return "", "", err
}
var resp cfRecordsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return "", "", err
}
if len(resp.Result) == 0 {
return "", "", nil
}
return resp.Result[0].ID, resp.Result[0].Content, nil
}
func (c *Cloudflare) request(ctx context.Context, token, method, url string, payload any) ([]byte, error) {
var body io.Reader
if payload != nil {
raw, err := json.Marshal(payload)
if err != nil {
return nil, err
}
body = bytes.NewReader(raw)
}
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
var envelope cfEnvelope
if err := json.Unmarshal(raw, &envelope); err != nil {
return nil, err
}
if !envelope.Success {
if len(envelope.Errors) > 0 {
return nil, fmt.Errorf("Cloudflare API 错误:%s", envelope.Errors[0].Message)
}
return nil, fmt.Errorf("Cloudflare API 请求失败")
}
return raw, nil
}
type cfEnvelope struct {
Success bool `json:"success"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}
type cfZonesResponse struct {
Result []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}
type cfRecordsResponse struct {
Result []struct {
ID string `json:"id"`
Content string `json:"content"`
} `json:"result"`
}