Files
Havline/internal/ddns/alidns.go
T

245 lines
6.0 KiB
Go
Raw Normal View History

2026-09-23 09:40:59 +08:00
package ddns
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
const alidnsEndpoint = "https://alidns.aliyuncs.com/"
type AliDNS struct {
client *http.Client
}
func NewAliDNS() *AliDNS {
return &AliDNS{client: &http.Client{Timeout: 15 * time.Second}}
}
func (a *AliDNS) Name() string { return "alidns" }
func (a *AliDNS) Verify(ctx context.Context, cred Credentials) error {
_, err := a.request(ctx, cred, map[string]string{
"Action": "DescribeDomains",
"PageNumber": "1",
"PageSize": "1",
})
return err
}
func (a *AliDNS) HasZone(ctx context.Context, cred Credentials, zone string) (bool, error) {
body, err := a.request(ctx, cred, map[string]string{
"Action": "DescribeDomains",
"KeyWord": zone,
"PageNumber": "1",
"PageSize": "50",
})
if err != nil {
ok, lookupErr := zoneLookupOK(err)
return ok, lookupErr
}
var resp alidnsDomainsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return false, err
}
zone = strings.ToLower(strings.TrimSpace(zone))
for _, item := range resp.Domains.Domain {
if strings.EqualFold(item.DomainName, zone) {
return true, nil
}
}
return false, nil
}
func (a *AliDNS) GetRecordIP(ctx context.Context, cred Credentials, rootDomain, recordName, recordType string) (string, error) {
subDomain := aliRR(recordName)
body, err := a.request(ctx, cred, map[string]string{
"Action": "DescribeDomainRecords",
"DomainName": rootDomain,
"RRKeyWord": subDomain,
"Type": recordType,
"PageNumber": "1",
"PageSize": "50",
})
if err != nil {
return "", err
}
var resp alidnsRecordsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return "", err
}
for _, rec := range resp.DomainRecords.Record {
if strings.EqualFold(rec.Type, recordType) && rec.RR == subDomain {
return rec.Value, nil
}
}
return "", nil
}
func (a *AliDNS) UpdateRecord(ctx context.Context, cred Credentials, rootDomain, recordName, recordType, ip string) error {
subDomain := aliRR(recordName)
existingID, err := a.findRecordID(ctx, cred, rootDomain, subDomain, recordType)
if err != nil {
return err
}
if existingID == "" {
_, err = a.request(ctx, cred, map[string]string{
"Action": "AddDomainRecord",
"DomainName": rootDomain,
"RR": subDomain,
"Type": recordType,
"Value": ip,
"TTL": "600",
})
return err
}
_, err = a.request(ctx, cred, map[string]string{
"Action": "UpdateDomainRecord",
"RecordId": existingID,
"RR": subDomain,
"Type": recordType,
"Value": ip,
"TTL": "600",
})
return err
}
func (a *AliDNS) findRecordID(ctx context.Context, cred Credentials, rootDomain, subDomain, recordType string) (string, error) {
body, err := a.request(ctx, cred, map[string]string{
"Action": "DescribeDomainRecords",
"DomainName": rootDomain,
"RRKeyWord": subDomain,
"Type": recordType,
"PageNumber": "1",
"PageSize": "50",
})
if err != nil {
return "", err
}
var resp alidnsRecordsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return "", err
}
for _, rec := range resp.DomainRecords.Record {
if strings.EqualFold(rec.Type, recordType) && rec.RR == subDomain {
return rec.RecordID, nil
}
}
return "", nil
}
func aliRR(recordName string) string {
subDomain := subDomainForRecord(recordName)
if subDomain == "@" {
return ""
}
return subDomain
}
func (a *AliDNS) request(ctx context.Context, cred Credentials, actionParams map[string]string) ([]byte, error) {
if cred.Token == "" || cred.Secret == "" {
return nil, fmt.Errorf("阿里云 DNS 凭证未配置")
}
query := map[string]string{
"Format": "JSON",
"Version": "2015-01-09",
"AccessKeyId": cred.Token,
"SignatureMethod": "HMAC-SHA1",
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
"SignatureVersion": "1.0",
"SignatureNonce": fmt.Sprintf("%d", time.Now().UnixNano()),
}
for k, v := range actionParams {
query[k] = v
}
query["Signature"] = signAliyun(query, cred.Secret)
values := url.Values{}
for k, v := range query {
values.Set(k, v)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, alidnsEndpoint+"?"+values.Encode(), nil)
if err != nil {
return nil, err
}
resp, err := a.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
var envelope alidnsEnvelope
if err := json.Unmarshal(body, &envelope); err == nil && envelope.Code != "" {
return nil, fmt.Errorf("阿里云 DNS API 错误:%s", envelope.Message)
}
return body, nil
}
func signAliyun(params map[string]string, secret string) string {
keys := make([]string, 0, len(params))
for k := range params {
if k == "Signature" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var canonical strings.Builder
for i, k := range keys {
if i > 0 {
canonical.WriteByte('&')
}
canonical.WriteString(percentEncode(k))
canonical.WriteByte('=')
canonical.WriteString(percentEncode(params[k]))
}
stringToSign := "GET&%2F&" + percentEncode(canonical.String())
mac := hmac.New(sha1.New, []byte(secret+"&"))
mac.Write([]byte(stringToSign))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func percentEncode(s string) string {
encoded := url.QueryEscape(s)
encoded = strings.ReplaceAll(encoded, "+", "%20")
encoded = strings.ReplaceAll(encoded, "*", "%2A")
encoded = strings.ReplaceAll(encoded, "%7E", "~")
return encoded
}
type alidnsEnvelope struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
type alidnsRecordsResponse struct {
DomainRecords struct {
Record []struct {
RecordID string `json:"RecordId"`
RR string `json:"RR"`
Type string `json:"Type"`
Value string `json:"Value"`
} `json:"Record"`
} `json:"DomainRecords"`
}
type alidnsDomainsResponse struct {
Domains struct {
Domain []struct {
DomainName string `json:"DomainName"`
} `json:"Domain"`
} `json:"Domains"`
}