修复阿里云限定域名查询权限及TXT主机名大小写验证
This commit is contained in:
parent
db4161a2dc
commit
b6d1b96426
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
28
backend/internal/dnsprovider/aliyun_record_match_test.go
Normal file
28
backend/internal/dnsprovider/aliyun_record_match_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
68
backend/internal/dnsprovider/aliyun_records_test.go
Normal file
68
backend/internal/dnsprovider/aliyun_records_test.go
Normal 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 前拒绝")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user