165 lines
4.5 KiB
Go
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"`
|
|
}
|