diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 1646dda..5f79021 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -81,11 +81,7 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string _ = logger.Close() return nil, fmt.Errorf("初始化敏感信息存储失败:%w", err) } - dnsTester := dnsprovider.NewTesterWithSettings( - dnsprovider.NewLegoFactory(), - time.Duration(cfg.ACME.PropagationTimeoutSecs)*time.Second, - time.Duration(cfg.ACME.PropagationPollingIntervalSecs)*time.Second, - ) + dnsTester := dnsprovider.NewTester(dnsprovider.NewLegoFactory()) dnsVerification := setup.NewDNSVerificationManager(dnsTester, secretStore) repo := repository.NewSQLDomainRepository(db.DB) serverRepo := repository.NewSQLServerRepository(db.DB) diff --git a/backend/internal/cli/init_certificate.go b/backend/internal/cli/init_certificate.go index 9b09058..45e9ff2 100644 --- a/backend/internal/cli/init_certificate.go +++ b/backend/internal/cli/init_certificate.go @@ -106,7 +106,7 @@ func verifyExistingDNSCredentials(ctx context.Context, cfg config.Config, input if err != nil { return func() {}, fmt.Errorf("读取已有 DNS 凭据失败:%w", err) } - verifyCtx, cancel := context.WithTimeout(ctx, dnsVerificationTimeout(cfg)) + verifyCtx, cancel := context.WithTimeout(ctx, dnsprovider.PermissionVerificationTimeout) defer cancel() if err := checker.VerifyDomain(verifyCtx, input.DNSProvider, target, credentials); err != nil { return func() {}, fmt.Errorf("已有 DNS 凭据在线权限校验失败:%w", err) diff --git a/backend/internal/cli/root.go b/backend/internal/cli/root.go index b11b434..effd771 100644 --- a/backend/internal/cli/root.go +++ b/backend/internal/cli/root.go @@ -476,26 +476,14 @@ func prepareDNSCredentials(ctx context.Context, cfg config.Config, input *setup. return func() {}, fmt.Errorf("初始化 DNS 凭据存储失败:%w", err) } manager := setup.NewDNSVerificationManager( - dnsprovider.NewTesterWithSettings( - dnsprovider.NewLegoFactory(), - time.Duration(cfg.ACME.PropagationTimeoutSecs)*time.Second, - time.Duration(cfg.ACME.PropagationPollingIntervalSecs)*time.Second, - ), + dnsprovider.NewTester(dnsprovider.NewLegoFactory()), store, ) - return prepareDNSCredentialsWithManagerAndTimeout(ctx, input, output, manager, store, dnsVerificationTimeout(cfg)) -} - -func dnsVerificationTimeout(cfg config.Config) time.Duration { - timeout := time.Duration(cfg.ACME.PropagationTimeoutSecs)*time.Second + 30*time.Second - if timeout <= 30*time.Second { - return 45 * time.Second - } - return timeout + return prepareDNSCredentialsWithManagerAndTimeout(ctx, input, output, manager, store, dnsprovider.PermissionVerificationTimeout) } func prepareDNSCredentialsWithManager(ctx context.Context, input *setup.Input, output io.Writer, manager *setup.DNSVerificationManager, store *secretstore.Store) (func(), error) { - return prepareDNSCredentialsWithManagerAndTimeout(ctx, input, output, manager, store, 45*time.Second) + return prepareDNSCredentialsWithManagerAndTimeout(ctx, input, output, manager, store, dnsprovider.PermissionVerificationTimeout) } func prepareDNSCredentialsWithManagerAndTimeout(ctx context.Context, input *setup.Input, output io.Writer, manager *setup.DNSVerificationManager, store *secretstore.Store, timeout time.Duration) (func(), error) { diff --git a/backend/internal/dnsprovider/tester.go b/backend/internal/dnsprovider/tester.go index 065c41a..dee86f3 100644 --- a/backend/internal/dnsprovider/tester.go +++ b/backend/internal/dnsprovider/tester.go @@ -32,16 +32,22 @@ type Tester struct { propagationInterval time.Duration } -const dnsCleanupTimeout = 30 * time.Second -const defaultDNSPropagationTimeout = 30 * time.Second -const defaultDNSPropagationInterval = 2 * time.Second +const ( + // PermissionVerificationTimeout 是 DNS API 权限验证的整体请求上限。 + // 它与 ACME DNS-01 的公网传播等待相互独立。 + PermissionVerificationTimeout = 60 * time.Second + + dnsCleanupTimeout = 30 * time.Second + defaultDNSPermissionTXTLookupTimeout = 30 * time.Second + defaultDNSPermissionTXTLookupInterval = 2 * time.Second +) func NewTester(factory Factory) *Tester { - return NewTesterWithResolverAndSettings(factory, legoTXTResolver{}, defaultDNSPropagationTimeout, defaultDNSPropagationInterval) + return NewTesterWithResolverAndSettings(factory, legoTXTResolver{}, defaultDNSPermissionTXTLookupTimeout, defaultDNSPermissionTXTLookupInterval) } func NewTesterWithResolver(factory Factory, resolver TXTResolver) *Tester { - return NewTesterWithResolverAndSettings(factory, resolver, defaultDNSPropagationTimeout, defaultDNSPropagationInterval) + return NewTesterWithResolverAndSettings(factory, resolver, defaultDNSPermissionTXTLookupTimeout, defaultDNSPermissionTXTLookupInterval) } func NewTesterWithSettings(factory Factory, timeout, interval time.Duration) *Tester { @@ -50,10 +56,10 @@ func NewTesterWithSettings(factory Factory, timeout, interval time.Duration) *Te func NewTesterWithResolverAndSettings(factory Factory, resolver TXTResolver, timeout, interval time.Duration) *Tester { if timeout <= 0 { - timeout = defaultDNSPropagationTimeout + timeout = defaultDNSPermissionTXTLookupTimeout } if interval <= 0 || interval > timeout { - interval = defaultDNSPropagationInterval + interval = defaultDNSPermissionTXTLookupInterval if interval > timeout { interval = timeout } @@ -192,10 +198,10 @@ func waitForTXT(ctx context.Context, resolver TXTResolver, fqdn, expected string return errors.New("TXT 查询服务未初始化") } if timeout <= 0 { - timeout = defaultDNSPropagationTimeout + timeout = defaultDNSPermissionTXTLookupTimeout } if interval <= 0 || interval > timeout { - interval = defaultDNSPropagationInterval + interval = defaultDNSPermissionTXTLookupInterval if interval > timeout { interval = timeout } diff --git a/backend/internal/httpapi/dns_api_test.go b/backend/internal/httpapi/dns_api_test.go index d6f2bfc..bc6196b 100644 --- a/backend/internal/httpapi/dns_api_test.go +++ b/backend/internal/httpapi/dns_api_test.go @@ -80,7 +80,7 @@ func TestAdminDNSTestRejectsUnavailableService(t *testing.T) { if response.Code != http.StatusServiceUnavailable { t.Fatalf("DNS 服务未初始化时应返回 503,实际为 %d:%s", response.Code, response.Body.String()) } - if !strings.Contains(response.Body.String(), `"verification_timeout_secs":930`) { + if !strings.Contains(response.Body.String(), `"verification_timeout_secs":60`) { t.Fatalf("DNS 服务未初始化响应应包含当前最大等待时间:%s", response.Body.String()) } } diff --git a/backend/internal/httpapi/operation_timeout.go b/backend/internal/httpapi/operation_timeout.go index 36d08c7..6c8bfeb 100644 --- a/backend/internal/httpapi/operation_timeout.go +++ b/backend/internal/httpapi/operation_timeout.go @@ -30,7 +30,7 @@ func (s *Server) longOperation(timeout time.Duration) gin.HandlerFunc { } func (s *Server) dnsOperation(c *gin.Context) { - s.longOperation(s.dnsVerificationTimeout())(c) + s.longOperation(dnsAPIPermissionVerificationTimeout())(c) } func (s *Server) requireSameOrigin(c *gin.Context) { diff --git a/backend/internal/httpapi/setup.go b/backend/internal/httpapi/setup.go index eef940f..b281707 100644 --- a/backend/internal/httpapi/setup.go +++ b/backend/internal/httpapi/setup.go @@ -157,7 +157,7 @@ func (s *Server) testAdminDNS(c *gin.Context) { } func (s *Server) testDNS(c *gin.Context) { - verificationTimeoutSecs := int(s.dnsVerificationTimeout().Seconds()) + verificationTimeoutSecs := int(dnsAPIPermissionVerificationTimeout().Seconds()) if s.setupDNS == nil { s.writeDNSTestError(c, http.StatusServiceUnavailable, "DNS 在线验证服务尚未初始化", nil, verificationTimeoutSecs) return @@ -167,7 +167,7 @@ func (s *Server) testDNS(c *gin.Context) { s.writeDNSTestError(c, http.StatusBadRequest, "DNS 测试请求参数无效", err, verificationTimeoutSecs) return } - ctx, cancel := context.WithTimeout(c.Request.Context(), s.dnsVerificationTimeout()) + ctx, cancel := context.WithTimeout(c.Request.Context(), dnsAPIPermissionVerificationTimeout()) defer cancel() result, err := s.setupDNS.Verify(ctx, setup.DNSVerificationInput{ ProviderID: request.ProviderID, TestDomain: request.TestDomain, Credentials: request.Credentials, @@ -216,20 +216,10 @@ func (s *Server) writeDNSTestError(c *gin.Context, status int, message string, e }) } -func (s *Server) dnsVerificationTimeout() time.Duration { - s.configMu.RLock() - cfg := s.config - s.configMu.RUnlock() - timeout := time.Duration(cfg.ACME.PropagationTimeoutSecs)*time.Second + dnsCleanupRequestBuffer - if timeout <= dnsCleanupRequestBuffer { - return defaultDNSRequestTimeout - } - return timeout +func dnsAPIPermissionVerificationTimeout() time.Duration { + return dnsprovider.PermissionVerificationTimeout } -const defaultDNSRequestTimeout = 45 * time.Second -const dnsCleanupRequestBuffer = 30 * time.Second - func (s *Server) applySetup(c *gin.Context) { if !s.setupMu.TryLock() { s.writeError(c, http.StatusConflict, "初始化正在执行,请等待当前操作完成", nil) diff --git a/backend/internal/httpapi/setup_test.go b/backend/internal/httpapi/setup_test.go index 7057546..572e46c 100644 --- a/backend/internal/httpapi/setup_test.go +++ b/backend/internal/httpapi/setup_test.go @@ -239,16 +239,16 @@ func TestSetupDNSProviderCatalogAndOnlineTest(t *testing.T) { !strings.Contains(testResponse.Body.String(), "清理记录") { t.Fatalf("DNS 在线测试响应应包含阶段报告:%s", testResponse.Body.String()) } - if !strings.Contains(testResponse.Body.String(), `"verification_timeout_secs":930`) { + if !strings.Contains(testResponse.Body.String(), `"verification_timeout_secs":60`) { t.Fatalf("DNS 在线测试响应应包含当前最大等待时间:%s", testResponse.Body.String()) } } -func TestSetupDNSTestUsesConfiguredRequestTimeout(t *testing.T) { +func TestSetupDNSTestUsesShortPermissionVerificationTimeout(t *testing.T) { baseDir := t.TempDir() cfg := config.Default(baseDir) - cfg.ACME.PropagationTimeoutSecs = 1 - cfg.ACME.PropagationPollingIntervalSecs = 1 + cfg.ACME.PropagationTimeoutSecs = 86400 + cfg.ACME.PropagationPollingIntervalSecs = 60 verifier := &fakeSetupDNSVerifier{verifyErr: errors.New("测试 DNS 验证失败")} server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{}, SetupDNS: verifier}) @@ -265,15 +265,15 @@ func TestSetupDNSTestUsesConfiguredRequestTimeout(t *testing.T) { if response.Code != http.StatusUnprocessableEntity { t.Fatalf("模拟 DNS 验证失败应返回 422,实际为 %d:%s", response.Code, response.Body.String()) } - if !strings.Contains(response.Body.String(), `"verification_timeout_secs":31`) { + if !strings.Contains(response.Body.String(), `"verification_timeout_secs":60`) { t.Fatalf("DNS 验证失败响应应包含当前最大等待时间:%s", response.Body.String()) } if !verifier.verifyHasDeadline { t.Fatal("DNS 在线测试应收到带截止时间的上下文") } remaining := verifier.verifyDeadline.Sub(started) - if remaining < 30*time.Second || remaining > 32*time.Second { - t.Fatalf("DNS 在线测试请求超时未按配置计算:剩余时间=%s", remaining) + if remaining < 59*time.Second || remaining > 61*time.Second { + t.Fatalf("DNS 在线测试请求超时不应继承 ACME 传播配置:剩余时间=%s", remaining) } } func TestSetupApplyPersistsConfigurationWithoutPlainPassword(t *testing.T) { @@ -527,7 +527,7 @@ func TestSetupDNSTestRejectsRemoteAndOversizedRequests(t *testing.T) { if oversizedResponse.Code != http.StatusBadRequest { t.Fatalf("超大 DNS 测试请求应被拒绝:%d %s", oversizedResponse.Code, oversizedResponse.Body.String()) } - if !strings.Contains(oversizedResponse.Body.String(), `"verification_timeout_secs":930`) { + if !strings.Contains(oversizedResponse.Body.String(), `"verification_timeout_secs":60`) { t.Fatalf("DNS 参数错误响应应包含当前最大等待时间:%s", oversizedResponse.Body.String()) } } @@ -547,7 +547,7 @@ func TestSetupDNSTestReturnsNotImplementedForUnavailableProvider(t *testing.T) { if strings.Contains(response.Body.String(), "access_key_secret") || strings.Contains(response.Body.String(), "secret") { t.Fatal("未开放 Provider 响应不应包含提交的凭据") } - if !strings.Contains(response.Body.String(), `"verification_timeout_secs":930`) { + if !strings.Contains(response.Body.String(), `"verification_timeout_secs":60`) { t.Fatalf("未开放 Provider 响应应包含当前最大等待时间:%s", response.Body.String()) } } diff --git a/frontend/src/views/Dashboard/Dashboard.js b/frontend/src/views/Dashboard/Dashboard.js index 2e1e330..3b80e18 100644 --- a/frontend/src/views/Dashboard/Dashboard.js +++ b/frontend/src/views/Dashboard/Dashboard.js @@ -1,7 +1,6 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; -const defaultDNSVerificationTimeoutSeconds = 45; -const dnsVerificationCleanupBufferSeconds = 30; +const defaultDNSVerificationTimeoutSeconds = 60; export function useDashboard() { const scertLogo = "/Scert.png"; @@ -218,14 +217,6 @@ export function useDashboard() { new Date(dns.expires_at).getTime() > Date.now(); }); - function resolveDNSVerificationTimeoutSeconds(value) { - const propagationTimeout = Number(value); - if (!Number.isFinite(propagationTimeout) || propagationTimeout <= 0) { - return defaultDNSVerificationTimeoutSeconds; - } - return Math.floor(propagationTimeout) + dnsVerificationCleanupBufferSeconds; - } - function updateDNSVerificationTimeoutSeconds(value) { const timeout = Number(value); if (Number.isFinite(timeout) && timeout > 0) { @@ -244,9 +235,7 @@ export function useDashboard() { function startDNSVerificationTimer(requestVersion) { stopDNSVerificationTimer(); - dnsVerificationTimeoutSeconds.value = resolveDNSVerificationTimeoutSeconds( - settingsForm.value.acme.propagation_timeout_secs, - ); + dnsVerificationTimeoutSeconds.value = defaultDNSVerificationTimeoutSeconds; const startedAt = Date.now(); const updateElapsedSeconds = () => { if (requestVersion !== dnsRequestVersion) return; diff --git a/frontend/src/views/Dashboard/Dashboard.test.js b/frontend/src/views/Dashboard/Dashboard.test.js index b6b56d7..5183139 100644 --- a/frontend/src/views/Dashboard/Dashboard.test.js +++ b/frontend/src/views/Dashboard/Dashboard.test.js @@ -103,7 +103,7 @@ test("管理设置 DNS 验证显示等待进度,并在结束后停止计时", assert.equal(state.dnsTesting.value, true); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 930); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 60); advanceTime(3200); runIntervals(); assert.equal(state.dnsVerificationElapsedSeconds.value, 3); @@ -113,12 +113,12 @@ test("管理设置 DNS 验证显示等待进度,并在结束后停止计时", credential_ref: "secret://test/only", verification_token: "test-token", expires_at: new Date(Date.now() + 60000).toISOString(), - verification_timeout_secs: 31, + verification_timeout_secs: 60, })); await task; assert.equal(state.dnsTesting.value, false); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 31); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 60); advanceTime(1000); runIntervals(); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); diff --git a/frontend/src/views/SetupWizard/SetupWizard.js b/frontend/src/views/SetupWizard/SetupWizard.js index cf2ecfd..598e411 100644 --- a/frontend/src/views/SetupWizard/SetupWizard.js +++ b/frontend/src/views/SetupWizard/SetupWizard.js @@ -1,7 +1,6 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue"; -const defaultDNSVerificationTimeoutSeconds = 45; -const dnsVerificationCleanupBufferSeconds = 30; +const defaultDNSVerificationTimeoutSeconds = 60; const commonEmailDomains = [ { label: "常用邮箱", value: "" }, @@ -155,14 +154,6 @@ export function useSetupWizard(props, emit) { dnsTestError.value = ""; } - function resolveDNSVerificationTimeoutSeconds(value) { - const propagationTimeout = Number(value); - if (!Number.isFinite(propagationTimeout) || propagationTimeout <= 0) { - return defaultDNSVerificationTimeoutSeconds; - } - return Math.floor(propagationTimeout) + dnsVerificationCleanupBufferSeconds; - } - function updateDNSVerificationTimeoutSeconds(value) { const timeout = Number(value); if (Number.isFinite(timeout) && timeout > 0) { @@ -181,9 +172,7 @@ export function useSetupWizard(props, emit) { function startDNSVerificationTimer(requestVersion) { stopDNSVerificationTimer(); - dnsVerificationTimeoutSeconds.value = resolveDNSVerificationTimeoutSeconds( - form.value.propagation_timeout_secs, - ); + dnsVerificationTimeoutSeconds.value = defaultDNSVerificationTimeoutSeconds; const startedAt = Date.now(); const updateElapsedSeconds = () => { if (requestVersion !== dnsRequestVersion) return; diff --git a/frontend/src/views/SetupWizard/SetupWizard.test.js b/frontend/src/views/SetupWizard/SetupWizard.test.js index b42a573..92ad944 100644 --- a/frontend/src/views/SetupWizard/SetupWizard.test.js +++ b/frontend/src/views/SetupWizard/SetupWizard.test.js @@ -34,7 +34,7 @@ test("初始化 DNS 验证显示等待进度,并采用服务端返回的最长 assert.equal(state.dnsTesting.value, true); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 930); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 60); advanceTime(2500); runIntervals(); @@ -45,12 +45,12 @@ test("初始化 DNS 验证显示等待进度,并采用服务端返回的最长 credential_ref: "secret://test/only", verification_token: "test-token", expires_at: new Date(Date.now() + 60000).toISOString(), - verification_timeout_secs: 31, + verification_timeout_secs: 60, })); await task; assert.equal(state.dnsTesting.value, false); assert.equal(state.dnsVerificationElapsedSeconds.value, 0); - assert.equal(state.dnsVerificationTimeoutSeconds.value, 31); + assert.equal(state.dnsVerificationTimeoutSeconds.value, 60); advanceTime(1000); runIntervals();