package certificate import ( "context" "errors" "sync" "time" "git.pcux.cn/xuans/Scert/backend/internal/domain" "github.com/go-acme/lego/v5/challenge" ) type DNSExecutionLeaseStore interface { AcquireDNSExecutionLease(context.Context, domain.Domain, domain.DNSExecutionIdentity) (domain.DNSExecutionLease, error) ValidateDNSExecutionLease(context.Context, domain.DNSExecutionLease) error ReleaseDNSExecutionLease(context.Context, string) error } type dnsExecutionContextKey struct{} func WithDNSExecutionIdentity(ctx context.Context, identity domain.DNSExecutionIdentity) context.Context { return context.WithValue(ctx, dnsExecutionContextKey{}, identity) } func (s *Service) SetDNSExecutionLeaseStore(store DNSExecutionLeaseStore) { if s != nil { s.dnsLeases = store } } // 授权探测也会写临时 TXT;错误可能表示远端写入或清理结果未知。 // 不以缺少 ACME 挑战记录作为释放依据,成功探测才确认本次引用可以收尾。 type leasedDNSAuthorization struct { checker DNSAuthorizationChecker store DNSExecutionLeaseStore lease domain.DNSExecutionLease mu sync.Mutex pending int } func (a *leasedDNSAuthorization) VerifyDomain(ctx context.Context, providerID, name string, credentials map[string]string) error { if err := ctx.Err(); err != nil { return err } if err := a.store.ValidateDNSExecutionLease(ctx, a.lease); err != nil { return errors.New("DNS 执行授权已失效,未执行授权探测") } a.mu.Lock() a.pending++ a.mu.Unlock() if err := a.checker.VerifyDomain(ctx, providerID, name, credentials); err != nil { return err } a.mu.Lock() a.pending-- a.mu.Unlock() return nil } func (a *leasedDNSAuthorization) clean() bool { a.mu.Lock() defer a.mu.Unlock() return a.pending == 0 } // 每次写挑战前再次检查任务和权限;清理只能使用原 Provider 和固定凭据。 // 失败的 Present 也可能已写入远端,因此必须等待匹配的 CleanUp 成功才释放引用。 type leasedDNSProvider struct { challenge.Provider store DNSExecutionLeaseStore lease domain.DNSExecutionLease mu sync.Mutex pending map[[3]string]bool } func (p *leasedDNSProvider) Present(ctx context.Context, name, token, keyAuth string) error { if err := ctx.Err(); err != nil { return err } if err := p.store.ValidateDNSExecutionLease(ctx, p.lease); err != nil { return errors.New("DNS 执行授权已失效,未写入挑战记录") } key := [3]string{name, token, keyAuth} p.mu.Lock() p.pending[key] = true p.mu.Unlock() return p.Provider.Present(ctx, name, token, keyAuth) } func (p *leasedDNSProvider) CleanUp(ctx context.Context, name, token, keyAuth string) error { // 任务取消不取消必要的挑战清理;清理自身仍有明确的时间上限。 cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute) defer cancel() if err := p.Provider.CleanUp(cleanupCtx, name, token, keyAuth); err != nil { return err } p.mu.Lock() delete(p.pending, [3]string{name, token, keyAuth}) p.mu.Unlock() return nil } func (p *leasedDNSProvider) clean() bool { p.mu.Lock() defer p.mu.Unlock() return len(p.pending) == 0 } // 不覆盖服务商自己的传播等待参数。 func (p *leasedDNSProvider) Timeout() (time.Duration, time.Duration) { if timed, ok := p.Provider.(interface { Timeout() (time.Duration, time.Duration) }); ok { return timed.Timeout() } return time.Minute, 2 * time.Second }