修复阿里云限定域名查询权限及TXT主机名大小写验证

This commit is contained in:
xuans 2026-09-17 23:06:29 +08:00
parent db4161a2dc
commit b6d1b96426
6 changed files with 129 additions and 16 deletions

View File

@ -139,7 +139,7 @@ func (c *AliyunAuthorizationChecker) verifyResolved(ctx context.Context, request
result.Domains[0].Cleanup = true
}()
describeCtx, describeCancel := context.WithTimeout(ctx, aliyunAPICallTimeout)
info, err := api.Describe(describeCtx, recordID)
info, err := api.Describe(describeCtx, zone, rr, recordID)
describeCancel()
if err != nil {
resultErr = fmt.Errorf("查询授权探测 TXT 记录失败:%w", err)
@ -161,7 +161,7 @@ func (c *AliyunAuthorizationChecker) verifyResolved(ctx context.Context, request
func matchesAliyunAuthorizationRecord(info aliyunRecordInfo, recordID, zone, rr, value string) bool {
return strings.TrimSpace(info.RecordID) == strings.TrimSpace(recordID) &&
normalizeAuthorizationDomain(info.DomainName) == normalizeAuthorizationDomain(zone) &&
strings.TrimSpace(info.RR) == strings.TrimSpace(rr) &&
strings.EqualFold(strings.TrimSpace(info.RR), strings.TrimSpace(rr)) &&
strings.EqualFold(strings.TrimSpace(info.Type), "TXT") &&
info.Value == value
}

View File

@ -39,7 +39,10 @@ func (a *authorizationRecordAPI) Add(ctx context.Context, zone, rr, value string
return "authorization-record-id", nil
}
func (a *authorizationRecordAPI) Describe(ctx context.Context, recordID string) (aliyunRecordInfo, error) {
func (a *authorizationRecordAPI) Describe(ctx context.Context, zone, rr, recordID string) (aliyunRecordInfo, error) {
if zone != a.zone || rr != a.rr {
return aliyunRecordInfo{}, errors.New("查询必须限定本次创建记录的托管区和主机记录")
}
a.recordCall("describe", recordID, ctx)
if a.describeErr != nil {
return aliyunRecordInfo{}, a.describeErr

View File

@ -15,7 +15,7 @@ import (
type aliyunRecordAPI interface {
Add(context.Context, string, string, string) (string, error)
Describe(context.Context, string) (aliyunRecordInfo, error)
Describe(context.Context, string, string, string) (aliyunRecordInfo, error)
Delete(context.Context, string) error
}
@ -44,26 +44,40 @@ func (a aliyunRecords) Add(ctx context.Context, zone, rr, value string) (string,
return strings.TrimSpace(*response.Body.RecordId), nil
}
func (a aliyunRecords) Describe(ctx context.Context, recordID string) (aliyunRecordInfo, error) {
response, err := client.DescribeDomainRecordInfoWithContext(
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.DescribeDomainRecordInfoRequest).SetRecordId(recordID),
new(client.DescribeDomainRecordsRequest).SetDomainName(zone).SetRRKeyWord(rr).
SetTypeKeyWord("TXT").SetSearchMode("EXACT").SetPageNumber(1).SetPageSize(100),
&dara.RuntimeOptions{},
)
if err != nil {
return aliyunRecordInfo{}, err
}
if response == nil || response.Body == nil {
if response == nil || response.Body == nil || response.Body.DomainRecords == 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
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 {

View File

@ -0,0 +1,28 @@
package dnsprovider
import "testing"
func TestAliyunAuthorizationRecordDNSCaseAndProof(t *testing.T) {
base := aliyunRecordInfo{RecordID: "record-1", DomainName: "example.com", RR: "_scert-authorization-mixed.web", Type: "TXT", Value: "ExactProofAa"}
tests := []struct {
name string
change func(*aliyunRecordInfo)
want bool
}{
{"DNS名称大小写归一化", func(info *aliyunRecordInfo) {}, true},
{"不同记录ID拒绝", func(info *aliyunRecordInfo) { info.RecordID = "record-2" }, false},
{"不同域名拒绝", func(info *aliyunRecordInfo) { info.DomainName = "other.com" }, false},
{"不同主机记录拒绝", func(info *aliyunRecordInfo) { info.RR += ".other" }, false},
{"不同类型拒绝", func(info *aliyunRecordInfo) { info.Type = "CNAME" }, false},
{"TXT值大小写不可忽略", func(info *aliyunRecordInfo) { info.Value = "exactproofaa" }, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info := base
test.change(&info)
if got := matchesAliyunAuthorizationRecord(info, "record-1", "example.com", "_scert-authorization-MiXeD.web", "ExactProofAa"); got != test.want {
t.Fatalf("记录比对结果错误:got=%v want=%v", got, test.want)
}
})
}
}

View File

@ -0,0 +1,68 @@
package dnsprovider
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/go-acme/alidns-20150109/v5/client"
)
func TestAliyunRecordsDescribeUsesScopedExactQuery(t *testing.T) {
for _, found := range []bool{true, false} {
name := "记录存在"
if !found {
name = "拒绝其他记录"
}
t.Run(name, func(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if err := r.ParseForm(); err != nil {
t.Error(err)
}
if got := r.Header.Get("x-acs-action"); got != "DescribeDomainRecords" {
t.Errorf("调用了非限定查询接口:%q", got)
}
for key, expected := range map[string]string{
"DomainName": "example.com",
"RRKeyWord": "_scert-authorization-mixed.web", "TypeKeyWord": "TXT",
"SearchMode": "EXACT", "PageNumber": "1", "PageSize": "100",
} {
if got := r.Form.Get(key); got != expected {
t.Errorf("查询范围错误:%s=%q,预期 %q", key, got, expected)
}
}
records := []map[string]string{{"RecordId": "other-record", "DomainName": "example.com", "RR": "_scert-authorization-mixed.web", "Type": "TXT", "Value": "unrelated"}}
if found {
records = append(records, map[string]string{"RecordId": "record-1", "DomainName": "example.com", "RR": "_scert-authorization-mixed.web", "Type": "TXT", "Value": "ExactProofAa"})
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"TotalCount": len(records), "PageNumber": 1, "PageSize": 100, "DomainRecords": map[string]any{"Record": records}})
}))
defer server.Close()
sdk, err := client.NewClient(new(openapi.Config).SetAccessKeyId("test-id").SetAccessKeySecret("test-secret").SetEndpoint(strings.TrimPrefix(server.URL, "http://")).SetProtocol("http"))
if err != nil {
t.Fatal(err)
}
api := aliyunRecords{client: sdk}
info, err := api.Describe(t.Context(), "example.com", "_scert-authorization-mixed.web", "record-1")
if found {
if err != nil || !matchesAliyunAuthorizationRecord(info, "record-1", "example.com", "_scert-authorization-MiXeD.web", "ExactProofAa") {
t.Fatalf("限定范围查询未通过:%+v %v", info, err)
}
} else if err == nil {
t.Fatal("不得使用其他记录证明本次验证成功")
}
if calls != 1 {
t.Fatalf("应只执行一次限定查询,实际 %d 次", calls)
}
if _, err := api.Describe(t.Context(), "", "", "record-1"); err == nil || calls != 1 {
t.Fatal("缺少查询范围必须在调用 API 前拒绝")
}
})
}
}

View File

@ -128,7 +128,7 @@ func (f *fakeAliyunRecords) Add(_ context.Context, zone, rr, value string) (stri
return "created-record-id", nil
}
func (f *fakeAliyunRecords) Describe(_ context.Context, id string) (aliyunRecordInfo, error) {
func (f *fakeAliyunRecords) Describe(_ context.Context, zone, rr, id string) (aliyunRecordInfo, error) {
f.described = append(f.described, id)
if f.describeErr != nil {
return aliyunRecordInfo{}, f.describeErr