diff --git a/backend/internal/setup/dns_verification.go b/backend/internal/setup/dns_verification.go index 6226565..4cbcd5a 100644 --- a/backend/internal/setup/dns_verification.go +++ b/backend/internal/setup/dns_verification.go @@ -123,6 +123,9 @@ func (m *DNSVerificationManager) Verify(ctx context.Context, input DNSVerificati if err != nil { return DNSVerificationResult{}, err } + if err := ctx.Err(); err != nil { + return DNSVerificationResult{}, err + } if err := m.reserveVerificationSlot(); err != nil { return DNSVerificationResult{}, err } @@ -139,6 +142,12 @@ func (m *DNSVerificationManager) Verify(ctx context.Context, input DNSVerificati Err: err, } } + if err := ctx.Err(); err != nil { + return DNSVerificationResult{TestReport: testReport}, &DNSVerificationError{ + Report: testReport, + Err: err, + } + } verificationToken, err := secureToken(32) if err != nil { return DNSVerificationResult{}, fmt.Errorf("生成 DNS 验证令牌失败:%w", err) @@ -156,6 +165,20 @@ func (m *DNSVerificationManager) Verify(ctx context.Context, input DNSVerificati } return DNSVerificationResult{}, fmt.Errorf("加密保存 DNS 凭据失败:%w", err) } + if err := ctx.Err(); err != nil { + cleanupErr := m.deleteCredential(credentialRef) + if cleanupErr != nil { + m.logCleanupFailure() + return DNSVerificationResult{TestReport: testReport}, errors.Join( + err, + fmt.Errorf("DNS 在线验证取消后临时凭据回收失败:%w", cleanupErr), + ) + } + return DNSVerificationResult{TestReport: testReport}, &DNSVerificationError{ + Report: testReport, + Err: err, + } + } expiresAt := m.now().UTC().Add(dnsVerificationLifetime) m.mu.Lock() expiredRefs := m.clearExpiredLocked(m.now().UTC()) diff --git a/backend/internal/setup/dns_verification_test.go b/backend/internal/setup/dns_verification_test.go index 297a04f..11fad47 100644 --- a/backend/internal/setup/dns_verification_test.go +++ b/backend/internal/setup/dns_verification_test.go @@ -56,6 +56,19 @@ type memorySecretWriter struct { deleteErr error } +type cancellingSecretWriter struct { + memorySecretWriter + cancel context.CancelFunc +} + +func (s *cancellingSecretWriter) Put(ctx context.Context, reference string, value map[string]string) error { + if err := s.memorySecretWriter.Put(ctx, reference, value); err != nil { + return err + } + s.cancel() + return nil +} + func (m *memorySecretWriter) Put(_ context.Context, reference string, value map[string]string) error { m.mu.Lock() defer m.mu.Unlock() @@ -265,6 +278,48 @@ func TestDNSVerificationManagerDeletesCredentialWhenPutReportsFailure(t *testing } } +func TestDNSVerificationManagerRejectsAlreadyCancelledContextBeforeTesting(t *testing.T) { + tester := &countingDNSTester{} + store := &memorySecretWriter{} + manager := NewDNSVerificationManager(tester, store) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + _, err := manager.Verify(ctx, DNSVerificationInput{ + ProviderID: "cloudflare", TestDomain: "example.com", + Credentials: map[string]string{"api_token": "secret"}, + }) + if !errors.Is(err, context.Canceled) { + t.Fatalf("已取消上下文应返回 context.Canceled:%v", err) + } + if tester.CallCount() != 0 || len(store.values) != 0 { + t.Fatalf("已取消上下文不应执行测试或保存凭据:calls=%d values=%v", tester.CallCount(), store.values) + } +} + +func TestDNSVerificationManagerCleansCredentialWhenContextCancelsAfterStore(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + store := &cancellingSecretWriter{cancel: cancel} + manager := NewDNSVerificationManager(fakeDNSTester{}, store) + + result, err := manager.Verify(ctx, DNSVerificationInput{ + ProviderID: "cloudflare", TestDomain: "example.com", + Credentials: map[string]string{"api_token": "secret"}, + }) + if !errors.Is(err, context.Canceled) { + t.Fatalf("保存凭据后取消应返回 context.Canceled:%v", err) + } + if result.CredentialRef != "" || result.VerificationToken != "" { + t.Fatalf("取消的 DNS 验证不应发布令牌:%+v", result) + } + if len(store.values) != 0 { + t.Fatalf("保存后取消未回收临时凭据:%v", store.values) + } + if len(store.deleted) != 1 { + t.Fatalf("保存后取消未执行临时凭据回收:%v", store.deleted) + } +} + type countingDNSTester struct { mu sync.Mutex calls int diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 15685eb..3c5cd54 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -3747,3 +3747,14 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无 - [~] 进行中:DNS 验证任务、其他 P0-10 任务链、跨数据库实机矩阵、崩溃恢复、重试幂等和统一复审。 - [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署及最终生产门禁。 - [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,没有新增 `[✓]`;提交推送遇到 `Authentication failed`。 + +## 本轮开发进度(2026-09-30,P0-10 DNS 验证取消与临时凭据回收) + +总体验收继续保持 **P0-03:完成,待复审**,本轮继续先开发、后统一测试。 + +- [x] 已完成,等待复审:DNS 在线验证在进入测试器前响应上下文取消,阻止已取消请求继续执行。 +- [x] 已完成,等待复审:凭据写入后检测取消状态,失败时删除临时凭据,不创建验证会话或一次性令牌。 +- [x] 已完成:新增取消前置和保存后回收回归测试;后端全量测试、静态检查和差异检查均通过。 +- [~] 进行中:DNS 清理失败与初始化回滚矩阵、其他 P0-10 任务链、跨数据库实机验证和统一复审。 +- [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。 +- [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 0cd0258..f8a64e4 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1052,3 +1052,14 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全 - [~] 进行中:P0-10 其他任务链、DNS、跨数据库实机、崩溃恢复、重试幂等及统一复审。 - [ ] 未开始:P0-12 备份恢复、浏览器缓存清理、密码 SSH、远端部署及最终生产门禁。 - [!] 当前不得升级验收状态,仍为 **P0-03:完成,待复审**,不得新增 `[✓]`。 + +## 本轮执行记录(2026-09-30,P0-10 DNS 验证取消与临时凭据回收) + +本轮按“先开发、后统一测试”执行,总体验收仍保持 **P0-03:完成,待复审**。 + +- [x] DNS 验证上下文在测试前已取消时不调用服务商适配器,且不保存临时凭据。 +- [x] DNS 凭据保存成功后请求被取消时,已删除临时凭据并阻止令牌发布,避免未绑定密钥残留。 +- [x] `go test ./...`、`go vet ./...`、`git diff --check` 已通过;本轮仅完成本地开发与自动化回归。 +- [~] 继续执行:DNS 清理失败、令牌生命周期、初始化回滚、任务链、三数据库实机矩阵和统一复审。 +- [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。 +- [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index 2dd038b..38f0bbe 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -1700,3 +1700,14 @@ P0-11 生产验收门禁 - [~] 进行中:P0-10 其他任务入口、DNS 验证、跨数据库实机矩阵、崩溃恢复、重试幂等和统一复审仍需继续。 - [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 DNS/ACME 和最终生产门禁仍未完成。 - [!] 总体验收不得升级:当前仍为 **P0-03:完成,待复审**,没有新增 `[✓]` 复审通过项;`415d504` 推送时遇到 `Authentication failed`,不作为测试失败依据。 + +## 本轮开发进度(2026-09-30,P0-10 DNS 验证取消与临时凭据回收) + +总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅补齐 DNS 在线验证的请求取消边界,不代表 P0-10 统一复审通过或生产部署验收通过。 + +- [x] 已完成,等待复审:DNS 验证开始前检查请求上下文,已取消请求不会调用 DNS 服务商测试器,也不会写入临时凭据。 +- [x] 已完成,等待复审:DNS 测试成功但请求在凭据写入后取消时,回收临时凭据且不发布验证令牌,避免遗留可用的临时密钥。 +- [x] 已完成:新增两条 DNS 生命周期回归测试;后端 `go test ./...`、`go vet ./...` 和 `git diff --check` 通过。 +- [~] 进行中:P0-10 DNS 清理失败、令牌消费、任务链、跨数据库实机矩阵、崩溃恢复、重试幂等和统一复审仍需继续。 +- [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁仍未完成。 +- [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。 diff --git a/doc/基础整改任务规格.md b/doc/基础整改任务规格.md index 22233d5..1e01f84 100644 --- a/doc/基础整改任务规格.md +++ b/doc/基础整改任务规格.md @@ -627,3 +627,13 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收 - [x] `go test ./...`、`go vet ./...`、定向任务测试和 `git diff --check` 均通过,代码提交 `415d504`。 - [~] P0-10 其他任务入口、DNS 验证、跨数据库实机行为、崩溃恢复、重试幂等和统一复审继续进行。 - [!] 本地结果不得替代真实 DNS/ACME、多服务器推送、密码 SSH 和最终生产门禁;总体验收不得离开 **P0-03:完成,待复审**。 + +## 本轮规格执行记录(2026-09-30,P0-10 DNS 验证取消与临时凭据回收) + +本轮仅补齐 DNS 在线验证请求取消边界,总体验收继续保持 **P0-03:完成,待复审**。 + +- [x] DNS 验证必须在开始服务商调用前检查上下文取消,取消请求不得触发权限测试或临时凭据写入。 +- [x] 临时凭据写入后若上下文取消,必须回收凭据并阻止一次性验证令牌进入后续初始化流程。 +- [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。 +- [~] DNS 清理失败、初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。 +- [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。