diff --git a/backend/services/setup/ssl/dns_automatic.go b/backend/services/setup/ssl/dns_automatic.go index ded80e8..3374896 100644 --- a/backend/services/setup/ssl/dns_automatic.go +++ b/backend/services/setup/ssl/dns_automatic.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/url" + "strconv" "strings" "sync" "time" @@ -26,6 +27,13 @@ type aliDNSRecord struct { Value string } +type aliDNSListedRecord struct { + ID string `json:"RecordId"` + Name string `json:"RR"` + Type string `json:"Type"` + Value string `json:"Value"` +} + // 每次签发独立记录创建结果,清理时只删除本实例创建且内容未被修改的记录。 type aliDNSChallenge struct { mu sync.Mutex @@ -121,17 +129,11 @@ func (provider *aliDNSChallenge) CleanUp(domain, token, keyAuth string) error { if !exists { return nil } - var current struct { - RecordID string `json:"RecordId"` - Domain string `json:"DomainName"` - Name string `json:"RR"` - Type string `json:"Type"` - Value string `json:"Value"` - } - if err := provider.call("DescribeDomainRecordInfo", url.Values{"RecordId": {created.ID}}, ¤t); err != nil { + current, err := provider.findCreatedRecord(created) + if err != nil { return err } - if current.RecordID != created.ID || current.Domain != provider.zone || current.Name != created.Name || current.Type != "TXT" || current.Value != created.Value { + if current.ID != created.ID || current.Name != created.Name || current.Type != "TXT" || current.Value != created.Value { return errors.New("DNS 挑战记录已被修改,保留记录并停止自动清理") } if err := provider.call("DeleteDomainRecord", url.Values{"RecordId": {created.ID}}, nil); err != nil { @@ -141,6 +143,34 @@ func (provider *aliDNSChallenge) CleanUp(domain, token, keyAuth string) error { return nil } +// 使用指定托管区域的记录列表复核,不要求额外的跨区域单记录查询权限。 +func (provider *aliDNSChallenge) findCreatedRecord(created aliDNSRecord) (aliDNSListedRecord, error) { + const pageSize = 500 + for page := 1; page <= 20; page++ { + var response struct { + Total int `json:"TotalCount"` + Records struct { + Record []aliDNSListedRecord `json:"Record"` + } `json:"DomainRecords"` + } + if err := provider.call("DescribeDomainRecords", url.Values{ + "DomainName": {provider.zone}, "RRKeyWord": {created.Name}, "Type": {"TXT"}, + "PageSize": {strconv.Itoa(pageSize)}, "PageNumber": {strconv.Itoa(page)}, + }, &response); err != nil { + return aliDNSListedRecord{}, err + } + for _, record := range response.Records.Record { + if record.ID == created.ID { + return record, nil + } + } + if page*pageSize >= response.Total || len(response.Records.Record) == 0 { + break + } + } + return aliDNSListedRecord{}, errors.New("无法核对本次 DNS 挑战记录,已停止自动清理") +} + func aliDNSPercentEncode(value string) string { return strings.ReplaceAll(url.QueryEscape(value), "+", "%20") } diff --git a/backend/services/setup/ssl/dns_automatic_test.go b/backend/services/setup/ssl/dns_automatic_test.go index 789c202..334eab7 100644 --- a/backend/services/setup/ssl/dns_automatic_test.go +++ b/backend/services/setup/ssl/dns_automatic_test.go @@ -33,12 +33,18 @@ func TestAutomaticDNSCleansOnlyOwnedUnchangedRecords(t *testing.T) { id = fmt.Sprint(adds) records[id] = aliDNSRecord{ID: id, Name: r.Form.Get("RR"), Value: r.Form.Get("Value")} _ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id}) - case "DescribeDomainRecordInfo": - record := records[id] - if tamper { - record.Value = "changed-by-user" + case "DescribeDomainRecords": + if r.Form.Get("DomainName") != "example.com" || r.Form.Get("Type") != "TXT" || r.Form.Get("PageNumber") == "" { + t.Error("record lookup must be scoped to the configured zone") } - _ = json.NewEncoder(w).Encode(map[string]string{"RecordId": id, "DomainName": "example.com", "RR": record.Name, "Type": "TXT", "Value": record.Value}) + listed := make([]aliDNSListedRecord, 0, len(records)) + for _, record := range records { + if tamper { + record.Value = "changed-by-user" + } + listed = append(listed, aliDNSListedRecord{ID: record.ID, Name: record.Name, Type: "TXT", Value: record.Value}) + } + _ = json.NewEncoder(w).Encode(map[string]any{"TotalCount": len(listed), "DomainRecords": map[string]any{"Record": listed}}) case "DeleteDomainRecord": if failDelete { w.WriteHeader(503)