领取初始化验证凭据并防止过期清理误删
This commit is contained in:
parent
8615296565
commit
efb405ea11
@ -82,16 +82,12 @@ type CompleteResult struct {
|
||||
}
|
||||
|
||||
type completionDatabaseSessions interface {
|
||||
Resolve(string) (DatabaseVerificationSession, error)
|
||||
Consume(string, string) (config.DatabaseConfig, error)
|
||||
Discard(string) error
|
||||
Claim(string) (DatabaseVerificationSession, func() error, error)
|
||||
DiscardAllExcept(...string)
|
||||
}
|
||||
|
||||
type completionDNSSessions interface {
|
||||
Resolve(string) (setup.DNSVerificationSession, error)
|
||||
Consume(string, string, string, string) error
|
||||
Discard(string) error
|
||||
Claim(string) (setup.DNSVerificationSession, func() error, error)
|
||||
DiscardAllExcept(...string)
|
||||
}
|
||||
|
||||
@ -220,30 +216,28 @@ func (completer *Completer) Complete(ctx context.Context, request CompleteReques
|
||||
return result, completeError(CompleteErrorInput, "初始化参数无效", err)
|
||||
}
|
||||
|
||||
databaseSession, err := completer.databaseSessions.Resolve(request.DatabaseVerificationToken)
|
||||
databaseSession, discardDatabase, err := completer.databaseSessions.Claim(request.DatabaseVerificationToken)
|
||||
if err != nil {
|
||||
return result, completeError(CompleteErrorVerification, "数据库验证结果无效或已经过期,请重新测试", err)
|
||||
}
|
||||
databaseResolved := true
|
||||
dnsResolved := false
|
||||
var discardDNS func() error
|
||||
committed := false
|
||||
defer func() {
|
||||
if committed {
|
||||
return
|
||||
}
|
||||
if dnsResolved {
|
||||
completer.discardDNS(request.DNSVerificationToken)
|
||||
if discardDNS != nil {
|
||||
completer.cleanupCredential(discardDNS, "失败初始化的 DNS 临时凭据回收未确认")
|
||||
}
|
||||
if databaseResolved {
|
||||
completer.discardDatabase(request.DatabaseVerificationToken)
|
||||
if discardDatabase != nil {
|
||||
completer.cleanupCredential(discardDatabase, "失败初始化的数据库临时凭据回收未确认")
|
||||
}
|
||||
}()
|
||||
|
||||
dnsSession, err := completer.dnsSessions.Resolve(request.DNSVerificationToken)
|
||||
dnsSession, discardDNS, err := completer.dnsSessions.Claim(request.DNSVerificationToken)
|
||||
if err != nil {
|
||||
return result, completeError(CompleteErrorVerification, "DNS 验证结果无效或已经过期,请重新测试", err)
|
||||
}
|
||||
dnsResolved = true
|
||||
if err := validateResolvedDNS(request, dnsSession); err != nil {
|
||||
return result, completeError(CompleteErrorVerification, "DNS 验证结果与管理入口不匹配,请重新验证", err)
|
||||
}
|
||||
@ -340,7 +334,7 @@ func (completer *Completer) Complete(ctx context.Context, request CompleteReques
|
||||
completer.completed = true
|
||||
completer.base = candidate
|
||||
result = CompleteResult{Completed: true, ManagementURL: managementURL(candidate)}
|
||||
result.Warnings = completer.afterCommit(request, candidate, dnsSession, databaseSession)
|
||||
result.Warnings = completer.afterCommit(candidate)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@ -462,7 +456,7 @@ func validateCompletionCertificatePaths(saved completionCertificate) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (completer *Completer) afterCommit(request CompleteRequest, candidate config.Config, dnsSession setup.DNSVerificationSession, databaseSession DatabaseVerificationSession) []string {
|
||||
func (completer *Completer) afterCommit(candidate config.Config) []string {
|
||||
warnings := make([]string, 0, 4)
|
||||
appendWarning := func(message string) {
|
||||
warnings = append(warnings, message)
|
||||
@ -470,12 +464,6 @@ func (completer *Completer) afterCommit(request CompleteRequest, candidate confi
|
||||
completer.warn(message)
|
||||
}
|
||||
}
|
||||
if _, err := completer.databaseSessions.Consume(request.DatabaseVerificationToken, databaseSession.CredentialRef); err != nil {
|
||||
appendWarning("正式配置已保存,但数据库验证令牌清理未确认")
|
||||
}
|
||||
if err := completer.dnsSessions.Consume(request.DNSVerificationToken, dnsSession.ProviderID, dnsSession.CredentialRef, dnsSession.TestDomain); err != nil {
|
||||
appendWarning("正式配置已保存,但 DNS 验证令牌清理未确认")
|
||||
}
|
||||
completer.sessions.RevokeAll()
|
||||
completer.databaseSessions.DiscardAllExcept(candidate.Database.CredentialRef)
|
||||
completer.dnsSessions.DiscardAllExcept(candidate.DNS.CredentialRef)
|
||||
@ -487,15 +475,9 @@ func (completer *Completer) afterCommit(request CompleteRequest, candidate confi
|
||||
return warnings
|
||||
}
|
||||
|
||||
func (completer *Completer) discardDatabase(token string) {
|
||||
if err := completer.databaseSessions.Discard(token); err != nil && completer.warn != nil {
|
||||
completer.warn("失败初始化的数据库临时凭据回收未确认")
|
||||
}
|
||||
}
|
||||
|
||||
func (completer *Completer) discardDNS(token string) {
|
||||
if err := completer.dnsSessions.Discard(token); err != nil && completer.warn != nil {
|
||||
completer.warn("失败初始化的 DNS 临时凭据回收未确认")
|
||||
func (completer *Completer) cleanupCredential(cleanup func() error, message string) {
|
||||
if err := cleanup(); err != nil && completer.warn != nil {
|
||||
completer.warn(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"database/sql"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
@ -22,6 +23,7 @@ import (
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/dnsverification"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/firewall"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/setup"
|
||||
)
|
||||
|
||||
@ -169,6 +171,12 @@ func newCompletionFixture(t *testing.T, domain string) completionFixture {
|
||||
t.Helper()
|
||||
baseDir := t.TempDir()
|
||||
base := config.Default(baseDir)
|
||||
// Linux 默认路径指向真实安装目录,测试必须显式隔离所有可写路径。
|
||||
base.Paths = config.PathsConfig{
|
||||
Install: baseDir, Config: filepath.Join(baseDir, "conf"), Data: filepath.Join(baseDir, "data"),
|
||||
Certificate: filepath.Join(baseDir, "ssl"), Log: filepath.Join(baseDir, "log"), Backup: filepath.Join(baseDir, "backups"),
|
||||
}
|
||||
base.Database.DSN = filepath.Join(base.Paths.Data, "scert.db")
|
||||
configPath := filepath.Join(base.Paths.Config, "scert.json")
|
||||
store := newCompletionMemorySecrets()
|
||||
databaseManager := NewDatabaseVerificationManager(base, store)
|
||||
@ -377,6 +385,59 @@ func TestCompleterSupportsPublicIPManagementCertificate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleterOwnsCredentialsDuringSlowSubmission(t *testing.T) {
|
||||
for _, failSave := range []bool{false, true} {
|
||||
name := "success"
|
||||
if failSave {
|
||||
name = "failure"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
fixture := newCompletionFixture(t, "admin.example.test")
|
||||
verified, err := fixture.database.Verify(t.Context(), DatabaseInput{
|
||||
Driver: "mysql", Host: "db.example.test", Database: "scert", Username: "owner", Password: "test-password",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := fixture.request("admin.example.test")
|
||||
request.DatabaseVerificationToken = verified.VerificationToken
|
||||
result, err := fixture.completer(func(dependencies *CompleteDependencies) {
|
||||
// 外部数据库仅模拟连接适配;这里检验凭据生命周期,不冒充 MySQL 实机验收。
|
||||
dependencies.OpenDatabase = func(ctx context.Context, _ config.DatabaseConfig) (*database.Database, error) {
|
||||
return database.Open(ctx, fixture.base.Database)
|
||||
}
|
||||
dependencies.Migrate = func(ctx context.Context, db *sql.DB, _ string) error {
|
||||
return database.Migrate(ctx, db, "sqlite")
|
||||
}
|
||||
dependencies.EnsureAdmin = func(ctx context.Context, db *sql.DB, _ string, user repository.User) error {
|
||||
return repository.NewSQLUserRepository(db, "sqlite").EnsureSuperAdmin(ctx, user)
|
||||
}
|
||||
dependencies.SaveConfig = func(candidate config.Config, path string) error {
|
||||
fixture.database.now = func() time.Time { return verified.ExpiresAt.Add(time.Second) }
|
||||
_, _ = fixture.database.Resolve(verified.VerificationToken)
|
||||
fixture.database.DiscardAll()
|
||||
fixture.dns.DiscardAll()
|
||||
if !fixture.store.has(verified.CredentialRef) || !fixture.store.has(fixture.dnsRef) {
|
||||
t.Fatal("提交期间清理删除了事务持有的凭据")
|
||||
}
|
||||
if failSave {
|
||||
return errors.New("save failed")
|
||||
}
|
||||
return candidate.Save(path)
|
||||
}
|
||||
}).Complete(t.Context(), request)
|
||||
if failSave {
|
||||
if err == nil || fixture.store.has(verified.CredentialRef) || fixture.store.has(fixture.dnsRef) {
|
||||
t.Fatalf("失败提交必须回收已领取凭据:%v", err)
|
||||
}
|
||||
} else if err != nil || !result.Completed || len(result.Warnings) != 0 ||
|
||||
!fixture.store.has(verified.CredentialRef) || !fixture.store.has(fixture.dnsRef) {
|
||||
t.Fatalf("成功提交未保留凭据:%+v,%v", result, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func completionDomainResource(name string) certificate.Resource {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
|
||||
@ -160,7 +160,7 @@ func (m *DatabaseVerificationManager) Consume(token, credentialRef string) (conf
|
||||
|
||||
// Resolve 按验证令牌读取完整初始化所需的服务端状态,但不消费令牌。
|
||||
func (m *DatabaseVerificationManager) Resolve(token string) (DatabaseVerificationSession, error) {
|
||||
session, err := m.resolveSession(token)
|
||||
session, err := m.resolveSession(token, false)
|
||||
if err != nil {
|
||||
return DatabaseVerificationSession{}, err
|
||||
}
|
||||
@ -171,6 +171,19 @@ func (m *DatabaseVerificationManager) Resolve(token string) (DatabaseVerificatio
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Claim 原子领取验证结果,并把凭据回收责任移交给初始化事务。
|
||||
// 领取后令牌立即失效,不再受过期或批量清理影响;提交失败时必须调用返回的回收函数。
|
||||
func (m *DatabaseVerificationManager) Claim(token string) (DatabaseVerificationSession, func() error, error) {
|
||||
session, err := m.resolveSession(token, true)
|
||||
if err != nil {
|
||||
return DatabaseVerificationSession{}, nil, err
|
||||
}
|
||||
cleanup := sync.OnceValue(func() error { return m.deleteCredential(session.CredentialRef) })
|
||||
return DatabaseVerificationSession{
|
||||
Config: session.Config, CredentialRef: session.CredentialRef, ExpiresAt: session.ExpiresAt,
|
||||
}, cleanup, nil
|
||||
}
|
||||
|
||||
// Discard 按验证令牌精确回收尚未消费的数据库凭据,不影响其他初始化会话。
|
||||
func (m *DatabaseVerificationManager) Discard(token string) error {
|
||||
if m == nil {
|
||||
@ -228,7 +241,7 @@ func (m *DatabaseVerificationManager) session(token, credentialRef string, consu
|
||||
return session.Config, nil
|
||||
}
|
||||
|
||||
func (m *DatabaseVerificationManager) resolveSession(token string) (databaseVerificationSession, error) {
|
||||
func (m *DatabaseVerificationManager) resolveSession(token string, claim bool) (databaseVerificationSession, error) {
|
||||
if m == nil {
|
||||
return databaseVerificationSession{}, errors.New("数据库验证服务尚未初始化")
|
||||
}
|
||||
@ -240,6 +253,9 @@ func (m *DatabaseVerificationManager) resolveSession(token string) (databaseVeri
|
||||
m.mu.Lock()
|
||||
expired := m.clearExpiredLocked(now)
|
||||
session, ok := m.sessions[token]
|
||||
if ok && claim {
|
||||
delete(m.sessions, token)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.deleteCredentials(expired)
|
||||
if !ok {
|
||||
|
||||
100
backend/internal/bootstrap/database_claim_test.go
Normal file
100
backend/internal/bootstrap/database_claim_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
)
|
||||
|
||||
func TestDatabaseClaimProtectsCredentialUntilTransactionCleanup(t *testing.T) {
|
||||
store := newCompletionMemorySecrets()
|
||||
manager := NewDatabaseVerificationManager(config.Default(t.TempDir()), store)
|
||||
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
|
||||
result, err := manager.Verify(t.Context(), DatabaseInput{
|
||||
Driver: "mysql", Host: "db.example.test", Database: "scert", Username: "owner", Password: "test-password",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session, cleanup, err := manager.Claim(result.VerificationToken)
|
||||
if err != nil || session.CredentialRef != result.CredentialRef || cleanup == nil {
|
||||
t.Fatalf("领取数据库凭据失败:%v", err)
|
||||
}
|
||||
manager.now = func() time.Time { return result.ExpiresAt.Add(time.Second) }
|
||||
if _, err := manager.Resolve(result.VerificationToken); err == nil {
|
||||
t.Fatal("已领取令牌仍可读取")
|
||||
}
|
||||
if _, _, err := manager.Claim(result.VerificationToken); err == nil {
|
||||
t.Fatal("已领取令牌仍可重复领取")
|
||||
}
|
||||
manager.DiscardAll()
|
||||
if !store.has(result.CredentialRef) {
|
||||
t.Fatal("过期或批量清理误删了事务持有的数据库凭据")
|
||||
}
|
||||
var workers sync.WaitGroup
|
||||
for range 32 {
|
||||
workers.Go(func() {
|
||||
if err := cleanup(); err != nil {
|
||||
t.Errorf("事务回收失败:%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
workers.Wait()
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
if len(store.values) != 0 || len(store.deleted) != 1 {
|
||||
t.Fatalf("事务回收必须且只能删除一次:剩余=%d,删除=%d", len(store.values), len(store.deleted))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseClaimHasOnlyOneConcurrentOwner(t *testing.T) {
|
||||
manager := NewDatabaseVerificationManager(config.Default(t.TempDir()), newCompletionMemorySecrets())
|
||||
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
|
||||
result, err := manager.Verify(t.Context(), DatabaseInput{Driver: "sqlite"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owners := make(chan func() error, 32)
|
||||
var workers sync.WaitGroup
|
||||
for range 32 {
|
||||
workers.Go(func() {
|
||||
if _, cleanup, err := manager.Claim(result.VerificationToken); err == nil {
|
||||
owners <- cleanup
|
||||
}
|
||||
})
|
||||
}
|
||||
workers.Wait()
|
||||
close(owners)
|
||||
count := 0
|
||||
for cleanup := range owners {
|
||||
count++
|
||||
if err := cleanup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("数据库验证结果存在多个事务拥有者:%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseClaimRejectsExpiredToken(t *testing.T) {
|
||||
store := newCompletionMemorySecrets()
|
||||
manager := NewDatabaseVerificationManager(config.Default(t.TempDir()), store)
|
||||
manager.verify = func(context.Context, config.DatabaseConfig) error { return nil }
|
||||
result, err := manager.Verify(t.Context(), DatabaseInput{
|
||||
Driver: "postgres", Host: "db.example.test", Database: "scert", Username: "owner", Password: "test-password",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manager.now = func() time.Time { return result.ExpiresAt }
|
||||
if _, cleanup, err := manager.Claim(result.VerificationToken); err == nil || cleanup != nil {
|
||||
t.Fatal("已过期的数据库令牌不得被领取")
|
||||
}
|
||||
if store.has(result.CredentialRef) {
|
||||
t.Fatal("过期且未领取的数据库凭据未回收")
|
||||
}
|
||||
}
|
||||
104
backend/internal/setup/dns_claim_test.go
Normal file
104
backend/internal/setup/dns_claim_test.go
Normal file
@ -0,0 +1,104 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDNSClaimProtectsCredentialUntilTransactionCleanup(t *testing.T) {
|
||||
store := &memorySecretWriter{}
|
||||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||||
ProviderID: "aliyun", TestDomain: "admin.example.test",
|
||||
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session, cleanup, err := manager.Claim(result.VerificationToken)
|
||||
if err != nil || cleanup == nil || session.CredentialRef != result.CredentialRef || session.TestDomain != "admin.example.test" {
|
||||
t.Fatalf("领取 DNS 凭据失败:%v", err)
|
||||
}
|
||||
manager.now = func() time.Time { return result.ExpiresAt.Add(time.Second) }
|
||||
if _, err := manager.Resolve(result.VerificationToken); err == nil {
|
||||
t.Fatal("已领取 DNS 令牌仍可读取")
|
||||
}
|
||||
if _, _, err := manager.Claim(result.VerificationToken); err == nil {
|
||||
t.Fatal("已领取 DNS 令牌仍可重复领取")
|
||||
}
|
||||
manager.DiscardAll()
|
||||
store.mu.Lock()
|
||||
_, exists := store.values[result.CredentialRef]
|
||||
store.mu.Unlock()
|
||||
if !exists {
|
||||
t.Fatal("过期或批量清理误删了事务持有的 DNS 凭据")
|
||||
}
|
||||
var workers sync.WaitGroup
|
||||
for range 32 {
|
||||
workers.Go(func() {
|
||||
if err := cleanup(); err != nil {
|
||||
t.Errorf("事务回收失败:%v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
workers.Wait()
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
if len(store.values) != 0 || len(store.deleted) != 1 {
|
||||
t.Fatalf("DNS 事务回收必须且只能删除一次:剩余=%d,删除=%d", len(store.values), len(store.deleted))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSClaimHasOnlyOneConcurrentOwner(t *testing.T) {
|
||||
manager := NewDNSVerificationManager(fakeDNSTester{}, &memorySecretWriter{})
|
||||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||||
ProviderID: "aliyun", TestDomain: "admin.example.test",
|
||||
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owners := make(chan func() error, 32)
|
||||
var workers sync.WaitGroup
|
||||
for range 32 {
|
||||
workers.Go(func() {
|
||||
if _, cleanup, err := manager.Claim(result.VerificationToken); err == nil {
|
||||
owners <- cleanup
|
||||
}
|
||||
})
|
||||
}
|
||||
workers.Wait()
|
||||
close(owners)
|
||||
count := 0
|
||||
for cleanup := range owners {
|
||||
count++
|
||||
if err := cleanup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("DNS 验证结果存在多个事务拥有者:%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSClaimRejectsExpiredToken(t *testing.T) {
|
||||
store := &memorySecretWriter{}
|
||||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||||
ProviderID: "aliyun", TestDomain: "admin.example.test",
|
||||
Credentials: map[string]string{"access_key_id": "test-id", "access_key_secret": "test-secret"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manager.now = func() time.Time { return result.ExpiresAt }
|
||||
if _, cleanup, err := manager.Claim(result.VerificationToken); err == nil || cleanup != nil {
|
||||
t.Fatal("已过期的 DNS 令牌不得被领取")
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
if len(store.values) != 0 {
|
||||
t.Fatal("过期且未领取的 DNS 凭据未回收")
|
||||
}
|
||||
}
|
||||
@ -183,7 +183,7 @@ func (m *DNSVerificationManager) Consume(token, providerID, credentialRef, testD
|
||||
|
||||
// Resolve 按验证令牌读取完整初始化所需的服务端状态,但不消费令牌。
|
||||
func (m *DNSVerificationManager) Resolve(token string) (DNSVerificationSession, error) {
|
||||
session, err := m.resolveSession(token)
|
||||
session, err := m.resolveSession(token, false)
|
||||
if err != nil {
|
||||
return DNSVerificationSession{}, err
|
||||
}
|
||||
@ -195,6 +195,20 @@ func (m *DNSVerificationManager) Resolve(token string) (DNSVerificationSession,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Claim 原子领取验证结果,并把凭据回收责任移交给初始化事务。
|
||||
// 领取后令牌立即失效,不再受过期或批量清理影响;提交失败时必须调用返回的回收函数。
|
||||
func (m *DNSVerificationManager) Claim(token string) (DNSVerificationSession, func() error, error) {
|
||||
session, err := m.resolveSession(token, true)
|
||||
if err != nil {
|
||||
return DNSVerificationSession{}, nil, err
|
||||
}
|
||||
cleanup := sync.OnceValue(func() error { return m.deleteCredential(session.CredentialRef) })
|
||||
return DNSVerificationSession{
|
||||
ProviderID: session.ProviderID, TestDomain: session.TestDomain,
|
||||
CredentialRef: session.CredentialRef, ExpiresAt: session.ExpiresAt,
|
||||
}, cleanup, nil
|
||||
}
|
||||
|
||||
// Discard 按验证令牌精确回收尚未消费的 DNS 临时凭据,不影响其他初始化会话。
|
||||
func (m *DNSVerificationManager) Discard(token string) error {
|
||||
if m == nil {
|
||||
@ -283,7 +297,7 @@ func (m *DNSVerificationManager) validateSession(token, providerID, credentialRe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) resolveSession(token string) (dnsVerificationSession, error) {
|
||||
func (m *DNSVerificationManager) resolveSession(token string, claim bool) (dnsVerificationSession, error) {
|
||||
if m == nil {
|
||||
return dnsVerificationSession{}, errors.New("DNS 在线验证服务未初始化")
|
||||
}
|
||||
@ -295,6 +309,9 @@ func (m *DNSVerificationManager) resolveSession(token string) (dnsVerificationSe
|
||||
m.mu.Lock()
|
||||
expiredRefs := m.clearExpiredLocked(now)
|
||||
session, ok := m.sessions[token]
|
||||
if ok && claim {
|
||||
delete(m.sessions, token)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.deleteCredentials(expiredRefs)
|
||||
if !ok {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user