118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
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
|
||
}
|