按权威托管区验证阿里云域名权限

This commit is contained in:
xuans 2026-09-16 11:09:42 +08:00
parent bb389ece32
commit 9f8a5fe2e4
6 changed files with 340 additions and 91 deletions

View File

@ -49,78 +49,86 @@ type AliyunAuthorizationChecker struct {
now func() time.Time
verifyDomainFunc func(context.Context, string, map[string]string) error
recordAPI func(map[string]string) (aliyunRecordAPI, error)
zoneResolver AliyunZoneResolver
}
func NewAliyunAuthorizationChecker() *AliyunAuthorizationChecker {
return &AliyunAuthorizationChecker{now: time.Now, recordAPI: newAliyunAuthorizationRecordAPI}
return &AliyunAuthorizationChecker{
now: time.Now,
recordAPI: newAliyunAuthorizationRecordAPI,
zoneResolver: newDefaultAliyunZoneResolver(),
}
}
func (c *AliyunAuthorizationChecker) Supports(providerID string) bool {
return strings.EqualFold(strings.TrimSpace(providerID), Aliyun)
}
// Verify 仅针对本次输入所推导的唯一注册根域执行一次写入和精确清理。
// Verify 仅针对本次 SOA 识别出的唯一权威托管区执行一次写入和精确清理。
func (c *AliyunAuthorizationChecker) Verify(ctx context.Context, requestedDomain string, credentials map[string]string) (report AuthorizationReport, resultErr error) {
requestedDomain = normalizeAuthorizationDomain(requestedDomain)
report = AuthorizationReport{Provider: Aliyun, RequestedDomain: requestedDomain, CheckedAt: c.currentTime().UTC()}
zone, err := ResolveAliyunTargetDomain(requestedDomain)
if c == nil {
return report, errors.New("阿里云权限探测器未初始化")
}
verificationCtx, cancel := permissionVerificationContext(ctx)
defer cancel()
zone, err := c.resolveTargetDomain(verificationCtx, requestedDomain)
if err != nil {
return report, err
}
return c.verifyResolved(verificationCtx, requestedDomain, zone, credentials, report)
}
func (c *AliyunAuthorizationChecker) verifyResolved(ctx context.Context, requestedDomain, zone string, credentials map[string]string, report AuthorizationReport) (result AuthorizationReport, resultErr error) {
result = report
if err := ctx.Err(); err != nil {
return report, err
return result, err
}
verificationCtx, cancel := context.WithTimeout(ctx, PermissionVerificationTimeoutFor(Aliyun))
defer cancel()
report.Domains = []AuthorizationDomain{{Domain: zone, Status: "待验证"}}
result.Domains = []AuthorizationDomain{{Domain: zone, Status: "待验证"}}
if c == nil || c.recordAPI == nil {
return report, errors.New("阿里云权限探测器未初始化")
return result, errors.New("阿里云权限探测器未初始化")
}
cleaned, err := ValidateCredentials(Aliyun, credentials)
if err != nil {
return report, err
return result, err
}
api, err := c.recordAPI(cleaned)
if err != nil {
return report, err
return result, err
}
suffix, err := randomAuthorizationValue(18)
if err != nil {
return report, err
return result, err
}
value, err := randomAuthorizationValue(32)
if err != nil {
return report, err
return result, err
}
rr := "_scert-authorization-" + suffix
base := strings.TrimPrefix(requestedDomain, "*.")
if base != zone {
rr += "." + strings.TrimSuffix(base, "."+zone)
}
report.RecordName = rr + "." + zone + "."
addCtx, addCancel := context.WithTimeout(verificationCtx, aliyunAPICallTimeout)
result.RecordName = rr + "." + zone + "."
addCtx, addCancel := context.WithTimeout(ctx, aliyunAPICallTimeout)
recordID, err := api.Add(addCtx, zone, rr, value)
addCancel()
if err != nil {
markAuthorizationFailure(&report, zone, err)
report.Diagnosis = DiagnoseAliyunError(err, zone)
return report, err
markAuthorizationFailure(&result, zone, err)
result.Diagnosis = DiagnoseAliyunError(err, zone)
return result, err
}
report.Domains[0].Writable = true
report.MatchedDomain = zone
result.Domains[0].Writable = true
result.MatchedDomain = zone
defer func() {
cleanupBase := context.WithoutCancel(verificationCtx)
if deadline, ok := verificationCtx.Deadline(); ok {
var cleanupDeadlineCancel context.CancelFunc
cleanupBase, cleanupDeadlineCancel = context.WithDeadline(cleanupBase, deadline)
defer cleanupDeadlineCancel()
}
cleanupBase := context.WithoutCancel(ctx)
cleanupContext, cleanupCancel := context.WithTimeout(cleanupBase, aliyunAPICallTimeout)
defer cleanupCancel()
if cleanupErr := api.Delete(cleanupContext, recordID); cleanupErr != nil {
cleanupErr = fmt.Errorf("清理授权探测 TXT 记录失败:%w", cleanupErr)
markAuthorizationFailure(&report, zone, cleanupErr)
report.Verified = false
markAuthorizationFailure(&result, zone, cleanupErr)
result.Verified = false
if resultErr == nil {
resultErr = cleanupErr
} else {
@ -128,26 +136,26 @@ func (c *AliyunAuthorizationChecker) Verify(ctx context.Context, requestedDomain
}
return
}
report.Domains[0].Cleanup = true
result.Domains[0].Cleanup = true
}()
describeCtx, describeCancel := context.WithTimeout(verificationCtx, aliyunAPICallTimeout)
describeCtx, describeCancel := context.WithTimeout(ctx, aliyunAPICallTimeout)
info, err := api.Describe(describeCtx, recordID)
describeCancel()
if err != nil {
resultErr = fmt.Errorf("查询授权探测 TXT 记录失败:%w", err)
markAuthorizationFailure(&report, zone, resultErr)
report.Diagnosis = DiagnoseAliyunError(err, zone)
return report, resultErr
markAuthorizationFailure(&result, zone, resultErr)
result.Diagnosis = DiagnoseAliyunError(err, zone)
return result, resultErr
}
if !matchesAliyunAuthorizationRecord(info, recordID, zone, rr, value) {
resultErr = errors.New("阿里云返回的临时 TXT 记录与本次验证不一致")
markAuthorizationFailure(&report, zone, resultErr)
return report, resultErr
markAuthorizationFailure(&result, zone, resultErr)
return result, resultErr
}
report.Domains[0].Readable = true
report.Domains[0].Status = "可操作"
report.Verified = true
return report, nil
result.Domains[0].Readable = true
result.Domains[0].Status = "可操作"
result.Verified = true
return result, nil
}
func matchesAliyunAuthorizationRecord(info aliyunRecordInfo, recordID, zone, rr, value string) bool {
@ -166,20 +174,24 @@ func (c *AliyunAuthorizationChecker) VerifyDomain(ctx context.Context, providerI
if c == nil {
return errors.New("阿里云权限探测器未初始化")
}
if err := ctx.Err(); err != nil {
verificationCtx, cancel := permissionVerificationContext(ctx)
defer cancel()
if err := verificationCtx.Err(); err != nil {
return err
}
requestedDomain = normalizeAuthorizationDomain(requestedDomain)
if _, err := ResolveAliyunTargetDomain(requestedDomain); err != nil {
zone, err := c.resolveTargetDomain(verificationCtx, requestedDomain)
if err != nil {
return err
}
if _, err := ValidateCredentials(Aliyun, credentials); err != nil {
return err
}
if c.verifyDomainFunc != nil {
return c.verifyDomainFunc(ctx, requestedDomain, credentials)
return c.verifyDomainFunc(verificationCtx, requestedDomain, credentials)
}
report, err := c.Verify(ctx, requestedDomain, credentials)
report := AuthorizationReport{Provider: Aliyun, RequestedDomain: requestedDomain, CheckedAt: c.currentTime().UTC()}
report, err = c.verifyResolved(verificationCtx, requestedDomain, zone, credentials, report)
if err != nil {
return err
}
@ -189,6 +201,20 @@ func (c *AliyunAuthorizationChecker) VerifyDomain(ctx context.Context, providerI
return nil
}
func (c *AliyunAuthorizationChecker) resolveTargetDomain(ctx context.Context, requestedDomain string) (string, error) {
if c == nil {
return "", errors.New("阿里云权限探测器未初始化")
}
return resolveAliyunTargetDomain(ctx, requestedDomain, c.zoneResolver)
}
func permissionVerificationContext(ctx context.Context) (context.Context, context.CancelFunc) {
if ctx == nil {
ctx = context.Background()
}
return context.WithTimeout(ctx, PermissionVerificationTimeoutFor(Aliyun))
}
// InvalidateDomain 保留旧调用方接口;授权检查已不再缓存,无需额外失效操作。
func (c *AliyunAuthorizationChecker) InvalidateDomain(providerID, requestedDomain string, credentials map[string]string) {
}
@ -277,6 +303,9 @@ func DiagnoseAliyunError(err error, domainName string) string {
}
msg := err.Error()
lowered := strings.ToLower(msg)
if strings.Contains(msg, "权威 DNS 托管区") || strings.Contains(lowered, "soa lookup") {
return "未能从公开 DNS 的 SOA 记录识别该域名唯一的权威托管区,系统未构造阿里云客户端,也未创建临时 TXT 记录。请检查域名是否已正确委派并可被公网 DNS 查询后重试"
}
if strings.Contains(lowered, "forbidden.nopermission") || strings.Contains(lowered, "forbidden.ram") || strings.Contains(lowered, "accessdenied") || strings.Contains(lowered, "not authorized") {
return fmt.Sprintf("当前阿里云 RAM 账号未获得域名 %s 所需的 DNS 操作权限。请管理员核对实际托管区资源 acs:alidns:*:*:domain/%s,按失败操作补充必要的读取、新增或删除权限;不要直接扩大为全部操作或全部域名权限", domainName, domainName)
}

View File

@ -39,7 +39,7 @@ func TestAliyunAuthorizationCheckerRechecksEveryRequestAndDetectsRevocation(t *t
now := time.Date(2026, 9, 14, 12, 0, 0, 0, time.UTC)
calls := 0
revoked := false
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
checker.now = func() time.Time { return now }
checker.verifyDomainFunc = func(context.Context, string, map[string]string) error {
calls++
@ -76,7 +76,7 @@ func TestAliyunAuthorizationCheckerRechecksEveryRequestAndDetectsRevocation(t *t
func TestAliyunAuthorizationCheckerInvalidatesAfterPermissionFailure(t *testing.T) {
calls := 0
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
checker.verifyDomainFunc = func(context.Context, string, map[string]string) error {
calls++
return nil
@ -102,7 +102,7 @@ func TestAliyunAuthorizationCheckerInvalidatesAfterPermissionFailure(t *testing.
func TestAliyunAuthorizationCheckerDoesNotCacheFailedAuthorization(t *testing.T) {
calls := 0
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
checker.verifyDomainFunc = func(context.Context, string, map[string]string) error {
calls++
return errors.New("权限不足")
@ -126,7 +126,7 @@ func TestAliyunAuthorizationCheckerDoesNotCacheFailedAuthorization(t *testing.T)
}
func TestAliyunAuthorizationCheckerCacheIsSafeForConcurrentUse(t *testing.T) {
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
checker.verifyDomainFunc = func(context.Context, string, map[string]string) error {
return nil
}
@ -175,6 +175,11 @@ func TestDiagnoseAliyunError(t *testing.T) {
domain: "solit.cn",
expected: "阿里云 AccessKey ID 或 AccessKey Secret 无效,请检查系统 DNS 凭据配置",
},
{
err: errors.New("无法识别域名 www.example.com 的权威 DNS 托管区,已停止验证:SOA lookup failed"),
domain: "www.example.com",
expected: "未能从公开 DNS 的 SOA 记录识别该域名唯一的权威托管区,系统未构造阿里云客户端,也未创建临时 TXT 记录。请检查域名是否已正确委派并可被公网 DNS 查询后重试",
},
{
err: nil,
domain: "solit.cn",

View File

@ -75,7 +75,7 @@ func (a *authorizationRecordAPI) recordCall(name, id string, ctx context.Context
}
func newAuthorizationCheckerForTest(api aliyunRecordAPI) *AliyunAuthorizationChecker {
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
return api, nil
}

View File

@ -93,8 +93,14 @@ type scopedAliyunProvider struct {
newAPI func(map[string]string) (aliyunRecordAPI, error)
api aliyunRecordAPI
mu sync.Mutex
records map[[32]byte]string
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) {
@ -104,7 +110,9 @@ func newScopedAliyunProvider(credentials map[string]string) (*scopedAliyunProvid
}
return &scopedAliyunProvider{
credentials: cleaned, newAPI: newAliyunRecordAPI,
records: make(map[[32]byte]string), challengeInfo: aliyunChallengeInfo,
records: make(map[[32]byte]aliyunPendingRecord),
challengeInfo: aliyunChallengeInfo,
zoneResolver: newDefaultAliyunZoneResolver(),
}, nil
}
@ -115,7 +123,13 @@ func aliyunChallengeInfo(ctx context.Context, domainName, keyAuth string) (strin
func (p *scopedAliyunProvider) Present(ctx context.Context, name, token, keyAuth string) error {
name = normalizeAuthorizationDomain(name)
zone, err := ResolveAliyunTargetDomain(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
}
@ -152,18 +166,21 @@ func (p *scopedAliyunProvider) Present(ctx context.Context, name, token, keyAuth
if err != nil {
return err
}
p.records[key] = id
p.records[key] = aliyunPendingRecord{ID: id, Zone: zone}
return nil
}
func (p *scopedAliyunProvider) CleanUp(ctx context.Context, name, token, keyAuth string) error {
if _, err := ResolveAliyunTargetDomain(name); err != nil {
return err
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()
id, ok := p.records[key]
record, ok := p.records[key]
if !ok {
return nil
}
@ -172,7 +189,7 @@ func (p *scopedAliyunProvider) CleanUp(ctx context.Context, name, token, keyAuth
}
cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), aliyunAuthorizationTimeout)
defer cancel()
if err := p.api.Delete(cleanupCtx, id); err != nil {
if err := p.api.Delete(cleanupCtx, record.ID); err != nil {
return err
}
delete(p.records, key)

View File

@ -1,19 +1,109 @@
package dnsprovider
import (
"context"
"errors"
"fmt"
"strings"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"golang.org/x/net/publicsuffix"
"github.com/go-acme/lego/v5/challenge/dns01"
)
const protectedAliyunDomain = "pcux.cn"
const (
protectedAliyunDomain = "pcux.cn"
aliyunZoneResolutionTimeout = 2 * time.Second
)
// ResolveAliyunTargetDomain 从本次输入中得到唯一可写入的注册根域。
// 它不枚举账号域名、不猜测托管区,也绝不因一次失败改写为父域或其他候选域名。
// AliyunZoneResolver 只读识别用户域名当前的权威 DNS 托管区。
// 它绝不能枚举 RAM 账号域名,也不能触发任何阿里云写操作。
type AliyunZoneResolver interface {
Resolve(context.Context, string) (string, error)
}
type aliyunZoneResolverFunc func(context.Context, string) (string, error)
func (f aliyunZoneResolverFunc) Resolve(ctx context.Context, requested string) (string, error) {
return f(ctx, requested)
}
type authoritativeAliyunZoneResolver struct {
findZone func(context.Context, string) (string, error)
timeout time.Duration
}
func newDefaultAliyunZoneResolver() AliyunZoneResolver {
return authoritativeAliyunZoneResolver{
findZone: func(ctx context.Context, fqdn string) (string, error) {
client := dns01.NewClient(&dns01.Options{Timeout: aliyunZoneResolutionTimeout})
return client.FindZoneByFqdn(ctx, fqdn)
},
timeout: aliyunZoneResolutionTimeout,
}
}
func (r authoritativeAliyunZoneResolver) Resolve(ctx context.Context, requested string) (string, error) {
base, err := validateAliyunRequestedDomain(requested)
if err != nil {
return "", err
}
if r.findZone == nil {
return "", errors.New("阿里云权威 DNS 托管区识别器未初始化")
}
if ctx == nil {
ctx = context.Background()
}
timeout := r.timeout
if timeout <= 0 {
timeout = aliyunZoneResolutionTimeout
}
resolveCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
zone, err := r.findZone(resolveCtx, base+".")
if err != nil {
return "", fmt.Errorf("无法识别域名 %s 的权威 DNS 托管区,已停止验证:%w", requested, err)
}
return normalizeAuthorizationDomain(zone), nil
}
// ResolveAliyunTargetDomain 根据公开 DNS 的 SOA 结果识别唯一权威托管区。
// SOA 查询可能读取父标签以定位权威区,但整个过程从不向候选父域写入 TXT 记录。
func ResolveAliyunTargetDomain(requested string) (string, error) {
return resolveAliyunTargetDomain(context.Background(), requested, newDefaultAliyunZoneResolver())
}
func resolveAliyunTargetDomain(ctx context.Context, requested string, resolver AliyunZoneResolver) (string, error) {
base, err := validateAliyunRequestedDomain(requested)
if err != nil {
return "", err
}
if resolver == nil {
return "", errors.New("阿里云权威 DNS 托管区识别器未初始化")
}
if ctx == nil {
ctx = context.Background()
}
zone, err := resolver.Resolve(ctx, base)
if err != nil {
return "", err
}
zone = normalizeAuthorizationDomain(zone)
if !domain.IsValidName(zone) || strings.HasPrefix(zone, "*.") {
return "", fmt.Errorf("权威 DNS 托管区识别结果无效:%s", zone)
}
if isProtectedAliyunDomain(zone) {
return "", fmt.Errorf("域名 %s 的权威 DNS 托管区 %s 受本机保护策略限制", requested, zone)
}
if base != zone && !strings.HasSuffix(base, "."+zone) {
return "", fmt.Errorf("权威 DNS 托管区 %s 不属于待操作域名 %s,已停止验证", zone, requested)
}
return zone, nil
}
// validateAliyunRequestedDomain 仅做不访问网络的本地格式和保护策略检查。
// 这使受保护域在读取凭据、构造客户端或进行 SOA 查询前被拒绝。
func validateAliyunRequestedDomain(requested string) (string, error) {
requested = normalizeAuthorizationDomain(requested)
base := strings.TrimPrefix(requested, "*.")
if !domain.IsValidName(requested) || base == "" {
@ -22,11 +112,7 @@ func ResolveAliyunTargetDomain(requested string) (string, error) {
if isProtectedAliyunDomain(base) {
return "", fmt.Errorf("域名 %s 受本机保护策略限制,未构造阿里云客户端或调用云 API", requested)
}
zone, err := publicsuffix.EffectiveTLDPlusOne(base)
if err != nil {
return "", fmt.Errorf("待操作域名必须包含可注册根域:%s", requested)
}
return normalizeAuthorizationDomain(zone), nil
return base, nil
}
func isProtectedAliyunDomain(name string) bool {
@ -44,7 +130,7 @@ func ValidateAliyunTargetDomains(providerID string, domains []string) error {
return errors.New("至少需要一个待操作域名")
}
for _, name := range domains {
if _, err := ResolveAliyunTargetDomain(name); err != nil {
if _, err := validateAliyunRequestedDomain(name); err != nil {
return err
}
}

View File

@ -5,6 +5,7 @@ import (
"errors"
"strings"
"testing"
"time"
)
func scopeCredentials() map[string]string {
@ -14,34 +15,100 @@ func scopeCredentials() map[string]string {
}
}
func TestResolveAliyunTargetDomainUsesOnlyRegistrableRoot(t *testing.T) {
tests := []struct {
name string
want string
}{
{name: "www.sub.example.com", want: "example.com"},
{name: "WWW.EXAMPLE.COM.", want: "example.com"},
{name: "*.example.com", want: "example.com"},
{name: "example.co.uk", want: "example.co.uk"},
func staticAliyunZoneResolver(zone string) AliyunZoneResolver {
return aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return zone, nil
})
}
func newAliyunAuthorizationCheckerForTest() *AliyunAuthorizationChecker {
checker := NewAliyunAuthorizationChecker()
checker.zoneResolver = staticAliyunZoneResolver("example.com")
return checker
}
func TestResolveAliyunTargetDomainUsesOnlyAuthoritativeZone(t *testing.T) {
resolverCalls := make([]string, 0, 1)
resolver := aliyunZoneResolverFunc(func(_ context.Context, requested string) (string, error) {
resolverCalls = append(resolverCalls, requested)
if requested != "www.sub.example.com" {
t.Fatalf("托管区识别收到了错误域名:%s", requested)
}
return "sub.example.com.", nil
})
got, err := resolveAliyunTargetDomain(t.Context(), "www.sub.example.com", resolver)
if err != nil || got != "sub.example.com" {
t.Fatalf("权威托管区识别结果错误:got=%q err=%v", got, err)
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := ResolveAliyunTargetDomain(test.name)
if err != nil || got != test.want {
t.Fatalf("ResolveAliyunTargetDomain(%q) = %q, %v; want %q", test.name, got, err, test.want)
}
})
if len(resolverCalls) != 1 {
t.Fatalf("不应枚举候选父域:resolverCalls=%v", resolverCalls)
}
}
func TestResolveAliyunTargetDomainRejectsProtectedAndInvalidDomains(t *testing.T) {
func TestAuthoritativeAliyunZoneResolverUsesFQDNAndBoundedDeadline(t *testing.T) {
const timeout = 80 * time.Millisecond
called := false
resolver := authoritativeAliyunZoneResolver{
timeout: timeout,
findZone: func(ctx context.Context, fqdn string) (string, error) {
called = true
if fqdn != "www.sub.example.com." {
t.Fatalf("权威托管区查询必须使用完整域名:%q", fqdn)
}
deadline, ok := ctx.Deadline()
if !ok {
t.Fatal("权威托管区查询必须具有截止时间")
}
remaining := time.Until(deadline)
if remaining <= 0 || remaining > timeout {
t.Fatalf("权威托管区查询截止时间未受限:%s", remaining)
}
return "sub.example.com.", nil
},
}
zone, err := resolver.Resolve(t.Context(), "WWW.Sub.Example.Com.")
if err != nil || zone != "sub.example.com" {
t.Fatalf("权威托管区解析结果错误:zone=%q err=%v", zone, err)
}
if !called {
t.Fatal("未执行权威托管区查询")
}
}
func TestResolveAliyunTargetDomainRejectsProtectedAndInvalidDomainsBeforeResolver(t *testing.T) {
resolverCalls := 0
resolver := aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
resolverCalls++
return "example.com", nil
})
for _, name := range []string{"pcux.cn", "*.pcux.cn", "api.pcux.cn", "com", "*.com", ""} {
t.Run(name, func(t *testing.T) {
if _, err := ResolveAliyunTargetDomain(name); err == nil {
t.Fatalf("ResolveAliyunTargetDomain(%q) unexpectedly succeeded", name)
if _, err := resolveAliyunTargetDomain(t.Context(), name, resolver); err == nil {
t.Fatalf("resolveAliyunTargetDomain(%q) unexpectedly succeeded", name)
}
})
}
if resolverCalls != 0 {
t.Fatalf("受保护或无效域名不应发起 SOA 查询:%d", resolverCalls)
}
}
func TestResolveAliyunTargetDomainFailsClosedWhenAuthoritativeZoneCannotBeIdentified(t *testing.T) {
resolver := aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return "", errors.New("SOA lookup failed")
})
if _, err := resolveAliyunTargetDomain(t.Context(), "www.example.com", resolver); err == nil || !strings.Contains(err.Error(), "SOA lookup failed") {
t.Fatalf("权威托管区识别失败必须阻止后续操作:%v", err)
}
}
func TestResolveAliyunTargetDomainRejectsUnrelatedAuthoritativeZone(t *testing.T) {
resolver := staticAliyunZoneResolver("unrelated.example.net")
if _, err := resolveAliyunTargetDomain(t.Context(), "www.example.com", resolver); err == nil || !strings.Contains(err.Error(), "不属于") {
t.Fatalf("无关的权威托管区必须失败关闭:%v", err)
}
}
type fakeAliyunRecords struct {
@ -87,9 +154,10 @@ func (f *fakeAliyunRecords) Delete(ctx context.Context, id string) error {
return f.deleteErr
}
func TestAliyunProbeUsesTheSingleRegistrableRootWithoutFallback(t *testing.T) {
func TestAliyunProbeUsesTheSingleAuthoritativeZoneWithoutFallback(t *testing.T) {
api := &fakeAliyunRecords{addErr: errors.New("Forbidden.RAM")}
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
checker.zoneResolver = staticAliyunZoneResolver("sub.example.com")
constructed := 0
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
@ -99,8 +167,8 @@ func TestAliyunProbeUsesTheSingleRegistrableRootWithoutFallback(t *testing.T) {
if _, err := checker.Verify(t.Context(), "www.sub.example.com", scopeCredentials()); err == nil {
t.Fatal("RAM denial unexpectedly succeeded")
}
if constructed != 1 || len(api.zones) != 1 || api.zones[0] != "example.com" {
t.Fatalf("probe did not use only the registrable root: constructed=%d zones=%v", constructed, api.zones)
if constructed != 1 || len(api.zones) != 1 || api.zones[0] != "sub.example.com" {
t.Fatalf("探测未严格使用唯一权威托管区:constructed=%d zones=%v", constructed, api.zones)
}
if len(api.deleted) != 0 {
t.Fatalf("failed Add must not guess a record ID to delete: %v", api.deleted)
@ -109,7 +177,7 @@ func TestAliyunProbeUsesTheSingleRegistrableRootWithoutFallback(t *testing.T) {
func TestAliyunProtectedDomainDoesNotConstructClientOrCallCloud(t *testing.T) {
api := &fakeAliyunRecords{}
checker := NewAliyunAuthorizationChecker()
checker := newAliyunAuthorizationCheckerForTest()
constructed := 0
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
@ -126,6 +194,26 @@ func TestAliyunProtectedDomainDoesNotConstructClientOrCallCloud(t *testing.T) {
}
}
func TestAliyunAuthorizationDoesNotConstructClientWhenZoneResolutionFails(t *testing.T) {
api := &fakeAliyunRecords{}
checker := newAliyunAuthorizationCheckerForTest()
checker.zoneResolver = aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return "", errors.New("SOA lookup failed")
})
constructed := 0
checker.recordAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return api, nil
}
if _, err := checker.Verify(t.Context(), "www.example.com", scopeCredentials()); err == nil {
t.Fatal("权威托管区识别失败时不应继续探测")
}
if constructed != 0 || len(api.zones) != 0 || len(api.described) != 0 || len(api.deleted) != 0 {
t.Fatalf("权威托管区识别失败时触碰了云端:constructed=%d zones=%v described=%v deleted=%v", constructed, api.zones, api.described, api.deleted)
}
}
func TestAliyunProviderDefersClientCreationAndPinsCleanupIdentity(t *testing.T) {
provider, err := newScopedAliyunProvider(scopeCredentials())
if err != nil {
@ -137,6 +225,7 @@ func TestAliyunProviderDefersClientCreationAndPinsCleanupIdentity(t *testing.T)
constructed++
return api, nil
}
provider.zoneResolver = staticAliyunZoneResolver("sub.example.com")
provider.challengeInfo = func(_ context.Context, name, _ string) (string, string, string) {
fqdn := "_acme-challenge." + name + "."
return fqdn, fqdn, "txt-value"
@ -148,8 +237,8 @@ func TestAliyunProviderDefersClientCreationAndPinsCleanupIdentity(t *testing.T)
if err := provider.Present(t.Context(), "www.sub.example.com", "token", "key"); err != nil {
t.Fatal(err)
}
if constructed != 1 || len(api.zones) != 1 || api.zones[0] != "example.com" || api.rrs[0] != "_acme-challenge.www.sub" {
t.Fatalf("provider wrote outside its single registrable root: constructed=%d zones=%v rrs=%v", constructed, api.zones, api.rrs)
if constructed != 1 || len(api.zones) != 1 || api.zones[0] != "sub.example.com" || api.rrs[0] != "_acme-challenge.www" {
t.Fatalf("provider 写入位置不属于唯一权威托管区:constructed=%d zones=%v rrs=%v", constructed, api.zones, api.rrs)
}
if err := provider.Present(t.Context(), "www.sub.example.com", "token", "key"); err == nil {
t.Fatal("duplicate challenge unexpectedly overwrote a pending record")
@ -181,6 +270,7 @@ func TestAliyunProviderRejectsCNAMEBeforeClientConstruction(t *testing.T) {
constructed++
return &fakeAliyunRecords{}, nil
}
provider.zoneResolver = staticAliyunZoneResolver("example.com")
provider.challengeInfo = func(context.Context, string, string) (string, string, string) {
return "_acme-challenge.www.sub.example.com.", "_acme-challenge.delegated.example.net.", "txt-value"
}
@ -192,3 +282,25 @@ func TestAliyunProviderRejectsCNAMEBeforeClientConstruction(t *testing.T) {
t.Fatalf("CNAME redirection constructed a cloud client: %d", constructed)
}
}
func TestAliyunProviderDoesNotConstructClientWhenZoneResolutionFails(t *testing.T) {
provider, err := newScopedAliyunProvider(scopeCredentials())
if err != nil {
t.Fatal(err)
}
provider.zoneResolver = aliyunZoneResolverFunc(func(context.Context, string) (string, error) {
return "", errors.New("SOA lookup failed")
})
constructed := 0
provider.newAPI = func(map[string]string) (aliyunRecordAPI, error) {
constructed++
return &fakeAliyunRecords{}, nil
}
if err := provider.Present(t.Context(), "www.example.com", "token", "key"); err == nil {
t.Fatal("权威托管区识别失败时不应继续写入")
}
if constructed != 0 {
t.Fatalf("权威托管区识别失败时不应构造阿里云客户端:%d", constructed)
}
}