164 lines
5.3 KiB
Go
164 lines
5.3 KiB
Go
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
|
||
}
|