Scert/backend/internal/dnsprovider/aliyun_provider.go

164 lines
5.3 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) (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 "", 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, recordID string) (aliyunRecordInfo, error) {
response, err := client.DescribeDomainRecordInfoWithContext(
ctx,
a.client,
new(client.DescribeDomainRecordInfoRequest).SetRecordId(recordID),
&dara.RuntimeOptions{},
)
if err != nil {
return aliyunRecordInfo{}, err
}
if response == nil || response.Body == nil {
return aliyunRecordInfo{}, errors.New("阿里云未返回临时 TXT 记录详情")
}
return aliyunRecordInfo{
RecordID: strings.TrimSpace(dereferenceString(response.Body.RecordId)),
DomainName: normalizeAuthorizationDomain(dereferenceString(response.Body.DomainName)),
RR: strings.TrimSpace(dereferenceString(response.Body.RR)),
Type: strings.TrimSpace(dereferenceString(response.Body.Type)),
Value: dereferenceString(response.Body.Value),
}, nil
}
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
}
// 不使用自动发现托管区的上游适配器,也不沿 CNAME 自动扩大写入范围。
type scopedAliyunProvider struct {
scope AliyunScope
api aliyunRecordAPI
mu sync.Mutex
records map[[32]byte]string
challengeInfo func(context.Context, string, string) (string, string, string)
}
func newScopedAliyunProvider(credentials map[string]string) (*scopedAliyunProvider, error) {
scope, err := ParseAliyunScope(credentials)
if err != nil {
return nil, err
}
api, err := newAliyunRecordAPI(credentials)
if err != nil {
return nil, err
}
return &scopedAliyunProvider{scope: scope, api: api, records: make(map[[32]byte]string), challengeInfo: aliyunChallengeInfo}, 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)
zone, err := p.scope.Resolve(name)
// ACME 的通配符授权以去掉通配符的标识符进入 Provider;业务层已逐项验证 SAN。
if err != nil && !strings.HasPrefix(name, "*.") {
zone, err = p.scope.Resolve("*." + name)
}
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 挑战已有待清理记录,拒绝重复新增")
}
id, err := p.api.Add(ctx, zone, rr, value)
if err != nil {
return err
}
p.records[key] = id
return nil
}
func (p *scopedAliyunProvider) CleanUp(ctx context.Context, name, token, keyAuth string) error {
key := sha256.Sum256([]byte(normalizeAuthorizationDomain(name) + "\x00" + token + "\x00" + keyAuth))
p.mu.Lock()
defer p.mu.Unlock()
id, ok := p.records[key]
if !ok {
return nil
}
cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), aliyunAuthorizationTimeout)
defer cancel()
if err := p.api.Delete(cleanupCtx, 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
}