mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-15 06:43:35 +01:00
Multi-certificate, SNI matching with exact map and suffix tree Add support for multiple TLS certificates with SNI-based selection. The root provider maintains a single centralized SNI matcher that uses an exact match map for O(1) lookups, falling back to a suffix tree for wildcard matching. Key features: - Add `Extra []Config` field to autocert.Config for additional certificates - Each extra entry must specify unique `cert_path` and `key_path` - Extra certs inherit main config (except `email` and `extra` fields) - Extra certs participate in ACME obtain/renew cycles independently - SNI selection precedence: exact match > wildcard match, main > extra - Single centralized SNI matcher on root provider rebuilt after cert changes The SNI matcher structure: - Exact match map: O(1) lookup for exact domain matches - Suffix tree: Efficient wildcard matching (e.g., *.example.com) Implementation details: - Provider.GetCert() now uses SNI from ClientHelloInfo for selection - Main cert is returned as fallback when no SNI match is found - Extra providers are created as child providers with merged configs - SNI matcher is rebuilt after Setup() and after ObtainCert() completes
102 lines
2.2 KiB
Go
102 lines
2.2 KiB
Go
package autocert
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
gperr "github.com/yusing/goutils/errs"
|
|
strutils "github.com/yusing/goutils/strings"
|
|
)
|
|
|
|
func (p *Provider) Setup() (err error) {
|
|
if err = p.LoadCert(); err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) { // ignore if cert doesn't exist
|
|
return err
|
|
}
|
|
log.Debug().Msg("obtaining cert due to error loading cert")
|
|
if err = p.ObtainCert(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err = p.setupExtraProviders(); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, expiry := range p.GetExpiries() {
|
|
log.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
|
|
break
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Provider) setupExtraProviders() error {
|
|
p.extraProviders = nil
|
|
p.sniMatcher = sniMatcher{}
|
|
if len(p.cfg.Extra) == 0 {
|
|
p.rebuildSNIMatcher()
|
|
return nil
|
|
}
|
|
|
|
for i := range p.cfg.Extra {
|
|
merged := mergeExtraConfig(p.cfg, &p.cfg.Extra[i])
|
|
user, legoCfg, err := merged.GetLegoConfig()
|
|
if err != nil {
|
|
return err.Subjectf("extra[%d]", i)
|
|
}
|
|
ep := NewProvider(&merged, user, legoCfg)
|
|
if err := ep.Setup(); err != nil {
|
|
return gperr.PrependSubject(fmt.Sprintf("extra[%d]", i), err)
|
|
}
|
|
p.extraProviders = append(p.extraProviders, ep)
|
|
}
|
|
p.rebuildSNIMatcher()
|
|
return nil
|
|
}
|
|
|
|
func mergeExtraConfig(mainCfg *Config, extraCfg *Config) Config {
|
|
merged := *mainCfg
|
|
merged.Extra = nil
|
|
merged.CertPath = extraCfg.CertPath
|
|
merged.KeyPath = extraCfg.KeyPath
|
|
|
|
if merged.Email == "" {
|
|
merged.Email = mainCfg.Email
|
|
}
|
|
|
|
if len(extraCfg.Domains) > 0 {
|
|
merged.Domains = extraCfg.Domains
|
|
}
|
|
if extraCfg.ACMEKeyPath != "" {
|
|
merged.ACMEKeyPath = extraCfg.ACMEKeyPath
|
|
}
|
|
if extraCfg.Provider != "" {
|
|
merged.Provider = extraCfg.Provider
|
|
}
|
|
if len(extraCfg.Options) > 0 {
|
|
merged.Options = extraCfg.Options
|
|
}
|
|
if len(extraCfg.Resolvers) > 0 {
|
|
merged.Resolvers = extraCfg.Resolvers
|
|
}
|
|
if extraCfg.CADirURL != "" {
|
|
merged.CADirURL = extraCfg.CADirURL
|
|
}
|
|
if len(extraCfg.CACerts) > 0 {
|
|
merged.CACerts = extraCfg.CACerts
|
|
}
|
|
if extraCfg.EABKid != "" {
|
|
merged.EABKid = extraCfg.EABKid
|
|
}
|
|
if extraCfg.EABHmac != "" {
|
|
merged.EABHmac = extraCfg.EABHmac
|
|
}
|
|
if extraCfg.HTTPClient != nil {
|
|
merged.HTTPClient = extraCfg.HTTPClient
|
|
}
|
|
return merged
|
|
}
|