245 lines
6.0 KiB
Go
245 lines
6.0 KiB
Go
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"`
|
|
}
|