Scert/backend/internal/dnsprovider/aliyun_provider.go

216 lines
7.1 KiB
Go
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.

package dnsprovider
import (
"context"
"crypto/sha256"
"errors"
"strings"
"sync"
"time"
"github.com/alibabacloud-go/tea/dara"
"github.com/go-acme/alidns-20150109/v5/client"
"github.com/go-acme/lego/v5/challenge/dns01"
)
type aliyunRecordAPI interface {
Add(context.Context, string, string, string) (string, error)
Describe(context.Context, string, string, string) (aliyunRecordInfo, error)
Delete(context.Context, string) error
}
type aliyunRecordInfo struct {
RecordID string
DomainName string
RR string
Type string
Value string
}
type aliyunRecords struct{ client *client.Client }
func (a aliyunRecords) Add(ctx context.Context, zone, rr, value string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, aliyunAuthorizationTimeout)
defer cancel()
request := new(client.AddDomainRecordRequest).SetDomainName(zone).SetRR(rr).
SetType("TXT").SetValue(value).SetTTL(aliyunAuthorizationTTL)
response, err := client.AddDomainRecordWithContext(ctx, a.client, request, &dara.RuntimeOptions{})
if err != nil {
return "", a.reconcileAuthorizationAdd(ctx, zone, rr, value, err)
}
if response == nil || response.Body == nil || response.Body.RecordId == nil || strings.TrimSpace(*response.Body.RecordId) == "" {
return "", errors.New("阿里云未返回新增记录 ID,状态未确认,请人工核对本次记录")
}
return strings.TrimSpace(*response.Body.RecordId), nil
}
func (a aliyunRecords) Describe(ctx context.Context, zone, rr, recordID string) (aliyunRecordInfo, error) {
zone = normalizeAuthorizationDomain(zone)
rr = strings.TrimSpace(rr)
recordID = strings.TrimSpace(recordID)
if zone == "" || rr == "" || recordID == "" {
return aliyunRecordInfo{}, errors.New("精确查询临时记录缺少托管区、主机记录或记录 ID")
}
// 按唯一托管区查询,兼容仅授予域名资源权限的 RAM 策略,不扩大账号可见范围。
response, err := client.DescribeDomainRecordsWithContext(
ctx,
a.client,
new(client.DescribeDomainRecordsRequest).SetDomainName(zone).SetRRKeyWord(rr).
SetTypeKeyWord("TXT").SetSearchMode("COMBINATION").SetPageNumber(1).SetPageSize(100),
&dara.RuntimeOptions{},
)
if err != nil {
return aliyunRecordInfo{}, err
}
if response == nil || response.Body == nil || response.Body.DomainRecords == nil {
return aliyunRecordInfo{}, errors.New("阿里云未返回临时 TXT 记录详情")
}
for _, record := range response.Body.DomainRecords.Record {
if record == nil || strings.TrimSpace(dereferenceString(record.RecordId)) != recordID {
continue
}
return aliyunRecordInfo{
RecordID: recordID,
DomainName: normalizeAuthorizationDomain(dereferenceString(record.DomainName)),
RR: strings.TrimSpace(dereferenceString(record.RR)),
Type: strings.TrimSpace(dereferenceString(record.Type)),
Value: dereferenceString(record.Value),
}, nil
}
return aliyunRecordInfo{}, errors.New("限定托管区查询未返回本次创建的临时 TXT 记录")
}
func (a aliyunRecords) Delete(ctx context.Context, id string) error {
return deleteAuthorizationRecord(ctx, a.client, id)
}
func newAliyunRecordAPI(credentials map[string]string) (aliyunRecordAPI, error) {
dnsClient, err := newAliyunClient(credentials)
if err != nil {
return nil, err
}
return aliyunRecords{client: dnsClient}, nil
}
// newAliyunAuthorizationRecordAPI 仅供短时权限探测使用,不改变 ACME DNS-01 的客户端策略。
func newAliyunAuthorizationRecordAPI(credentials map[string]string) (aliyunRecordAPI, error) {
dnsClient, err := newAliyunAuthorizationClient(credentials)
if err != nil {
return nil, err
}
return aliyunRecords{client: dnsClient}, nil
}
// 不使用自动发现托管区的上游适配器,也不沿 CNAME 自动扩大写入范围。
type scopedAliyunProvider struct {
credentials map[string]string
newAPI func(map[string]string) (aliyunRecordAPI, error)
api aliyunRecordAPI
mu sync.Mutex
records map[[32]byte]aliyunPendingRecord
challengeInfo func(context.Context, string, string) (string, string, string)
zoneResolver AliyunZoneResolver
}
type aliyunPendingRecord struct {
ID string
Zone string
}
func newScopedAliyunProvider(credentials map[string]string) (*scopedAliyunProvider, error) {
cleaned, err := ValidateCredentials(Aliyun, credentials)
if err != nil {
return nil, err
}
return &scopedAliyunProvider{
credentials: cleaned, newAPI: newAliyunRecordAPI,
records: make(map[[32]byte]aliyunPendingRecord),
challengeInfo: aliyunChallengeInfo,
zoneResolver: newDefaultAliyunZoneResolver(),
}, nil
}
func aliyunChallengeInfo(ctx context.Context, domainName, keyAuth string) (string, string, string) {
info := dns01.GetChallengeInfo(ctx, domainName, keyAuth)
return info.FQDN, info.EffectiveFQDN, info.Value
}
func (p *scopedAliyunProvider) Present(ctx context.Context, name, token, keyAuth string) error {
name = normalizeAuthorizationDomain(name)
if p == nil {
return errors.New("阿里云 DNS Provider 未初始化")
}
if ctx == nil {
ctx = context.Background()
}
zone, err := resolveAliyunTargetDomain(ctx, name, p.zoneResolver)
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
base := strings.TrimPrefix(name, "*.")
fqdn, effective, value := p.challengeInfo(ctx, base, keyAuth)
expected := "_acme-challenge." + base
if normalizeAuthorizationDomain(fqdn) != expected || normalizeAuthorizationDomain(effective) != expected {
return errors.New("DNS 挑战存在 CNAME 重定向,已拒绝自动跟随;请显式配置受限的 CNAME 委派服务商")
}
rr := strings.TrimSuffix(expected, "."+zone)
if rr == expected {
return errors.New("DNS 挑战记录不属于显式托管区")
}
key := sha256.Sum256([]byte(name + "\x00" + token + "\x00" + keyAuth))
p.mu.Lock()
defer p.mu.Unlock()
if _, ok := p.records[key]; ok {
return errors.New("同一 DNS 挑战已有待清理记录,拒绝重复新增")
}
if p.api == nil {
if p.newAPI == nil {
return errors.New("阿里云 DNS 客户端工厂未初始化")
}
api, err := p.newAPI(p.credentials)
if err != nil {
return err
}
p.api = api
}
id, err := p.api.Add(ctx, zone, rr, value)
if err != nil {
return err
}
p.records[key] = aliyunPendingRecord{ID: id, Zone: zone}
return nil
}
func (p *scopedAliyunProvider) CleanUp(ctx context.Context, name, token, keyAuth string) error {
if p == nil {
return errors.New("阿里云 DNS Provider 未初始化")
}
if ctx == nil {
ctx = context.Background()
}
key := sha256.Sum256([]byte(normalizeAuthorizationDomain(name) + "\x00" + token + "\x00" + keyAuth))
p.mu.Lock()
defer p.mu.Unlock()
record, ok := p.records[key]
if !ok {
return nil
}
if p.api == nil {
return errors.New("阿里云 DNS 临时记录状态不完整,已拒绝清理请求")
}
cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), aliyunAuthorizationTimeout)
defer cancel()
if err := p.api.Delete(cleanupCtx, record.ID); err != nil {
return err
}
delete(p.records, key)
return nil
}
func (p *scopedAliyunProvider) Timeout() (time.Duration, time.Duration) {
return 120 * time.Second, 2 * time.Second
}