Files
godoxy-yusing/internal/autocert/sni_matcher.go
Yuzerion c00854a124 feat(autocert): add multi-certificate support (#185)
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
2026-01-04 00:37:26 +08:00

130 lines
2.4 KiB
Go

package autocert
import (
"crypto/x509"
"strings"
)
type sniMatcher struct {
exact map[string]*Provider
root sniTreeNode
}
type sniTreeNode struct {
children map[string]*sniTreeNode
wildcard *Provider
}
func (m *sniMatcher) match(serverName string) *Provider {
if m == nil {
return nil
}
serverName = normalizeServerName(serverName)
if serverName == "" {
return nil
}
if m.exact != nil {
if p, ok := m.exact[serverName]; ok {
return p
}
}
return m.matchSuffixTree(serverName)
}
func (m *sniMatcher) matchSuffixTree(serverName string) *Provider {
n := &m.root
labels := strings.Split(serverName, ".")
var best *Provider
for i := len(labels) - 1; i >= 0; i-- {
if n.children == nil {
break
}
next := n.children[labels[i]]
if next == nil {
break
}
n = next
consumed := len(labels) - i
remaining := len(labels) - consumed
if remaining == 1 && n.wildcard != nil {
best = n.wildcard
}
}
return best
}
func normalizeServerName(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimSuffix(s, ".")
return strings.ToLower(s)
}
func (m *sniMatcher) addProvider(p *Provider) {
if p == nil || p.tlsCert == nil || len(p.tlsCert.Certificate) == 0 {
return
}
leaf, err := x509.ParseCertificate(p.tlsCert.Certificate[0])
if err != nil {
return
}
addName := func(name string) {
name = normalizeServerName(name)
if name == "" {
return
}
if after, ok := strings.CutPrefix(name, "*."); ok {
suffix := after
if suffix == "" {
return
}
m.insertWildcardSuffix(suffix, p)
return
}
m.insertExact(name, p)
}
if leaf.Subject.CommonName != "" {
addName(leaf.Subject.CommonName)
}
for _, n := range leaf.DNSNames {
addName(n)
}
}
func (m *sniMatcher) insertExact(name string, p *Provider) {
if name == "" || p == nil {
return
}
if m.exact == nil {
m.exact = make(map[string]*Provider)
}
if _, exists := m.exact[name]; !exists {
m.exact[name] = p
}
}
func (m *sniMatcher) insertWildcardSuffix(suffix string, p *Provider) {
if suffix == "" || p == nil {
return
}
n := &m.root
labels := strings.Split(suffix, ".")
for i := len(labels) - 1; i >= 0; i-- {
if n.children == nil {
n.children = make(map[string]*sniTreeNode)
}
next := n.children[labels[i]]
if next == nil {
next = &sniTreeNode{}
n.children[labels[i]] = next
}
n = next
}
if n.wildcard == nil {
n.wildcard = p
}
}