拆分 DNS 权限验证与证书传播等待

This commit is contained in:
xuans 2026-09-16 00:33:14 +08:00
parent d872d7eaaf
commit f64f0d778f
12 changed files with 45 additions and 87 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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) {

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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())
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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();