将阿里云 DNS 权限验证限制为六秒

This commit is contained in:
xuans 2026-09-16 01:28:06 +08:00
parent 3c1fce99be
commit 0e4ed9b2d9
13 changed files with 73 additions and 20 deletions

View File

@ -85,7 +85,7 @@ func TestDNSCheckReportsLiveResultWithoutPersistingConfiguration(t *testing.T) {
t.Fatal("权限请求必须设置超时")
}
remaining := time.Until(deadline)
if remaining < 9*time.Second || remaining > 11*time.Second {
if remaining < 5*time.Second || remaining > 7*time.Second {
t.Fatalf("阿里云 CLI 权限验证时限错误:%s", remaining)
}
report := dnsprovider.AuthorizationReport{Provider: "aliyun", RequestedDomain: name, Verified: outcome == "allowed"}

View File

@ -20,7 +20,7 @@ const (
aliyunDefaultRegion = "cn-hangzhou"
aliyunAuthorizationTTL = 600
aliyunAuthorizationTimeout = 30 * time.Second
aliyunAPICallTimeout = 3 * time.Second
aliyunAPICallTimeout = 2 * time.Second
)
// AuthorizationDomain 描述一个阿里云 DNS 根域名的授权探测结果。
@ -54,7 +54,7 @@ type AliyunAuthorizationChecker struct {
}
func NewAliyunAuthorizationChecker() *AliyunAuthorizationChecker {
return &AliyunAuthorizationChecker{now: time.Now, recordAPI: newAliyunRecordAPI}
return &AliyunAuthorizationChecker{now: time.Now, recordAPI: newAliyunAuthorizationRecordAPI}
}
func (c *AliyunAuthorizationChecker) Supports(providerID string) bool {
@ -270,6 +270,19 @@ func (c *AliyunAuthorizationChecker) currentTime() time.Time {
}
func newAliyunClient(credentials map[string]string) (*client.Client, error) {
return newAliyunClientWithRetryOptions(credentials, nil)
}
// newAliyunAuthorizationClient 为权限探测建立独立的单次调用客户端。
// 探测流程有严格的阶段时限,不能因 SDK 重试或退避拉长用户等待;证书签发仍使用普通客户端策略。
func newAliyunAuthorizationClient(credentials map[string]string) (*client.Client, error) {
return newAliyunClientWithRetryOptions(credentials, &dara.RetryOptions{
Retryable: false,
MaxAttempts: 1,
})
}
func newAliyunClientWithRetryOptions(credentials map[string]string, retryOptions *dara.RetryOptions) (*client.Client, error) {
cleaned, err := ValidateCredentials(Aliyun, credentials)
if err != nil {
return nil, err
@ -280,6 +293,9 @@ func newAliyunClient(credentials map[string]string) (*client.Client, error) {
if token := cleaned["security_token"]; token != "" {
config.SetSecurityToken(token)
}
if retryOptions != nil {
config.SetRetryOptions(retryOptions)
}
result, err := client.NewClient(config)
if err != nil {
return nil, fmt.Errorf("创建阿里云 DNS 客户端失败:%w", err)

View File

@ -7,6 +7,34 @@ import (
"time"
)
func TestAliyunAuthorizationRecordAPIUsesSingleAttemptWithoutRetry(t *testing.T) {
api, err := newAliyunAuthorizationRecordAPI(scopeCredentials())
if err != nil {
t.Fatalf("创建阿里云权限探测客户端失败:%v", err)
}
records, ok := api.(aliyunRecords)
if !ok || records.client == nil || records.client.RetryOptions == nil {
t.Fatalf("权限探测必须使用带显式重试策略的专用客户端:%T", api)
}
if records.client.RetryOptions.Retryable || records.client.RetryOptions.MaxAttempts != 1 {
t.Fatalf("权限探测客户端不得自动重试:%+v", records.client.RetryOptions)
}
}
func TestAliyunACMERecordAPIDoesNotUseAuthorizationRetryPolicy(t *testing.T) {
api, err := newAliyunRecordAPI(scopeCredentials())
if err != nil {
t.Fatalf("创建阿里云 ACME 客户端失败:%v", err)
}
records, ok := api.(aliyunRecords)
if !ok || records.client == nil {
t.Fatalf("ACME 客户端类型错误:%T", api)
}
if records.client.RetryOptions != nil {
t.Fatalf("ACME 客户端不应复用权限探测专用重试策略:%+v", records.client.RetryOptions)
}
}
func TestAliyunAuthorizationCheckerRechecksEveryRequestAndDetectsRevocation(t *testing.T) {
now := time.Date(2026, 9, 14, 12, 0, 0, 0, time.UTC)
calls := 0

View File

@ -207,7 +207,7 @@ func TestAliyunAuthorizationVerifyCleansUpAfterRequestCancellation(t *testing.T)
}
func TestPermissionVerificationTimeoutForAliyunIsIndependentFromPropagation(t *testing.T) {
if got := PermissionVerificationTimeoutFor(Aliyun); got != 10*time.Second {
if got := PermissionVerificationTimeoutFor(Aliyun); got != 6*time.Second {
t.Fatalf("阿里云权限验证时限错误:%s", got)
}
if got := PermissionVerificationTimeoutFor(Cloudflare); got != 60*time.Second {

View File

@ -78,6 +78,15 @@ func newAliyunRecordAPI(credentials map[string]string) (aliyunRecordAPI, error)
return aliyunRecords{client: dnsClient}, nil
}
// newAliyunAuthorizationRecordAPI 仅供短时权限探测使用,不改变 ACME DNS-01 的客户端策略。
func newAliyunAuthorizationRecordAPI(credentials map[string]string) (aliyunRecordAPI, error) {
dnsClient, err := newAliyunAuthorizationClient(credentials)
if err != nil {
return nil, err
}
return aliyunRecords{client: dnsClient}, nil
}
// 不使用自动发现托管区的上游适配器,也不沿 CNAME 自动扩大写入范围。
type scopedAliyunProvider struct {
scope AliyunScope

View File

@ -40,7 +40,7 @@ const (
// AliyunPermissionVerificationTimeout 限制阿里云账号内的创建、查询和清理闭环。
// 该流程不等待公网 DNS 传播。
AliyunPermissionVerificationTimeout = 10 * time.Second
AliyunPermissionVerificationTimeout = 6 * time.Second
dnsCleanupTimeout = 30 * time.Second
defaultDNSPermissionTXTLookupTimeout = 30 * time.Second

View File

@ -81,8 +81,8 @@ func TestAdminAliyunDNSTestReportsFastVerificationTimeout(t *testing.T) {
}`))
request.Header.Set("Content-Type", "application/json")
serveAdminHTTP(server, response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"verification_timeout_secs":10`) {
t.Fatalf("管理端阿里云 DNS 在线测试未返回 10 秒等待上限:%d %s", response.Code, response.Body.String())
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) {
t.Fatalf("管理端阿里云 DNS 在线测试未返回 6 秒等待上限:%d %s", response.Code, response.Body.String())
}
}

View File

@ -298,14 +298,14 @@ func TestSetupAliyunDNSTestUsesFastAccountAPITimeout(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":10`) {
t.Fatalf("阿里云 DNS 验证失败响应应包含 10 秒等待上限:%s", response.Body.String())
if !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) {
t.Fatalf("阿里云 DNS 验证失败响应应包含 6 秒等待上限:%s", response.Body.String())
}
if !verifier.verifyHasDeadline {
t.Fatal("阿里云 DNS 在线测试应收到带截止时间的上下文")
}
remaining := verifier.verifyDeadline.Sub(started)
if remaining < 9*time.Second || remaining > 11*time.Second {
if remaining < 5*time.Second || remaining > 7*time.Second {
t.Fatalf("阿里云权限验证不应继承 ACME 传播配置:剩余时间=%s", remaining)
}
}
@ -580,7 +580,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":10`) {
if !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) {
t.Fatalf("未开放 Provider 响应应包含当前最大等待时间:%s", response.Body.String())
}
}

View File

@ -1,7 +1,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
const defaultDNSVerificationTimeoutSeconds = 60;
const aliyunDNSVerificationTimeoutSeconds = 10;
const aliyunDNSVerificationTimeoutSeconds = 6;
function dnsVerificationTimeoutFor(providerID) {
return String(providerID || "").trim().toLowerCase() === "aliyun"

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, 10);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 6);
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: 10,
verification_timeout_secs: 6,
}));
await task;
assert.equal(state.dnsTesting.value, false);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 10);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 6);
advanceTime(1000);
runIntervals();
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);

View File

@ -1,7 +1,7 @@
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
const defaultDNSVerificationTimeoutSeconds = 60;
const aliyunDNSVerificationTimeoutSeconds = 10;
const aliyunDNSVerificationTimeoutSeconds = 6;
function dnsVerificationTimeoutFor(providerID) {
return String(providerID || "").trim().toLowerCase() === "aliyun"

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, 10);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 6);
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: 10,
verification_timeout_secs: 6,
}));
await task;
assert.equal(state.dnsTesting.value, false);
assert.equal(state.dnsVerificationElapsedSeconds.value, 0);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 10);
assert.equal(state.dnsVerificationTimeoutSeconds.value, 6);
advanceTime(1000);
runIntervals();

View File

@ -706,7 +706,7 @@ const {
:max="form.propagation_timeout_secs"
required
/>
<small>默认 10 秒,不能大于最大等待时间。</small>
<small>默认 6 秒,不能大于最大等待时间。</small>
</label>
</div>
<p v-if="dnsCatalogError" class="inline-error">