修复阿里云限定域名查询权限及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
|
result.Domains[0].Cleanup = true
|
||||||
}()
|
}()
|
||||||
describeCtx, describeCancel := context.WithTimeout(ctx, aliyunAPICallTimeout)
|
describeCtx, describeCancel := context.WithTimeout(ctx, aliyunAPICallTimeout)
|
||||||
info, err := api.Describe(describeCtx, recordID)
|
info, err := api.Describe(describeCtx, zone, rr, recordID)
|
||||||
describeCancel()
|
describeCancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resultErr = fmt.Errorf("查询授权探测 TXT 记录失败:%w", err)
|
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 {
|
func matchesAliyunAuthorizationRecord(info aliyunRecordInfo, recordID, zone, rr, value string) bool {
|
||||||
return strings.TrimSpace(info.RecordID) == strings.TrimSpace(recordID) &&
|
return strings.TrimSpace(info.RecordID) == strings.TrimSpace(recordID) &&
|
||||||
normalizeAuthorizationDomain(info.DomainName) == normalizeAuthorizationDomain(zone) &&
|
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") &&
|
strings.EqualFold(strings.TrimSpace(info.Type), "TXT") &&
|
||||||
info.Value == value
|
info.Value == value
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,10 @@ func (a *authorizationRecordAPI) Add(ctx context.Context, zone, rr, value string
|
|||||||
return "authorization-record-id", nil
|
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)
|
a.recordCall("describe", recordID, ctx)
|
||||||
if a.describeErr != nil {
|
if a.describeErr != nil {
|
||||||
return aliyunRecordInfo{}, a.describeErr
|
return aliyunRecordInfo{}, a.describeErr
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
type aliyunRecordAPI interface {
|
type aliyunRecordAPI interface {
|
||||||
Add(context.Context, string, string, string) (string, error)
|
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
|
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
|
return strings.TrimSpace(*response.Body.RecordId), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a aliyunRecords) Describe(ctx context.Context, recordID string) (aliyunRecordInfo, error) {
|
func (a aliyunRecords) Describe(ctx context.Context, zone, rr, recordID string) (aliyunRecordInfo, error) {
|
||||||
response, err := client.DescribeDomainRecordInfoWithContext(
|
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,
|
ctx,
|
||||||
a.client,
|
a.client,
|
||||||
new(client.DescribeDomainRecordInfoRequest).SetRecordId(recordID),
|
new(client.DescribeDomainRecordsRequest).SetDomainName(zone).SetRRKeyWord(rr).
|
||||||
|
SetTypeKeyWord("TXT").SetSearchMode("EXACT").SetPageNumber(1).SetPageSize(100),
|
||||||
&dara.RuntimeOptions{},
|
&dara.RuntimeOptions{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return aliyunRecordInfo{}, err
|
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{}, errors.New("阿里云未返回临时 TXT 记录详情")
|
||||||
}
|
}
|
||||||
return aliyunRecordInfo{
|
for _, record := range response.Body.DomainRecords.Record {
|
||||||
RecordID: strings.TrimSpace(dereferenceString(response.Body.RecordId)),
|
if record == nil || strings.TrimSpace(dereferenceString(record.RecordId)) != recordID {
|
||||||
DomainName: normalizeAuthorizationDomain(dereferenceString(response.Body.DomainName)),
|
continue
|
||||||
RR: strings.TrimSpace(dereferenceString(response.Body.RR)),
|
}
|
||||||
Type: strings.TrimSpace(dereferenceString(response.Body.Type)),
|
return aliyunRecordInfo{
|
||||||
Value: dereferenceString(response.Body.Value),
|
RecordID: recordID,
|
||||||
}, nil
|
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 {
|
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
|
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)
|
f.described = append(f.described, id)
|
||||||
if f.describeErr != nil {
|
if f.describeErr != nil {
|
||||||
return aliyunRecordInfo{}, f.describeErr
|
return aliyunRecordInfo{}, f.describeErr
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user