Smail/backend/config/ssl_apex_test.go
smail 2e520eb5a8
Some checks are pending
Security Checks / security (push) Waiting to run
统一根域名与泛域名证书校验并补充云端反例验收
2026-09-21 04:38:46 +08:00

180 lines
6.7 KiB
Go

package config
import (
"bytes"
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"time"
)
func TestProjectDNSRuntimeRequiresRootAndWildcard(t *testing.T) {
for _, test := range []struct {
name string
domains []string
valid bool
}{
{"root_and_wildcard", []string{"example.com", "*.example.com"}, true},
{"wildcard_only", []string{"*.example.com"}, false},
{"root_only", []string{"example.com"}, false},
} {
t.Run(test.name, func(t *testing.T) {
previousRoot := ROOT_PATH
ROOT_PATH = t.TempDir()
t.Cleanup(func() { ROOT_PATH = previousRoot })
key, certificate := testManagedCertificate(t, test.domains[0], time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048, test.domains[1:]...)
// 故意不在准备阶段检查域名,模拟已有证书文件的加载与配置复验。
reference, err := stageFlatSSLAtRoot(ROOT_PATH, ManagedSSLModeDefault, key, certificate, nil, nil)
if err != nil {
t.Fatal(err)
}
defer ReleaseStagedSSL(reference)
candidate := RuntimeConfig{Root: RootConfig{Domains: DomainConfig{
Primary: "example.com", Web: "web.example.com", Accepted: []string{"example.com"},
}}}
candidate.SSL.TLS = SSLConfig{
Management: SSLManagementProject, Challenge: SSLChallengeDNS,
Mode: reference.Mode, CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.PrivateKeyRef,
}
check := func(name string, err error) {
t.Helper()
if test.valid && err != nil {
t.Errorf("%s 拒绝完整域名覆盖:%v", name, err)
}
if !test.valid && !errors.Is(err, ErrSSLCertificateDomain) {
t.Errorf("%s 未拒绝不完整域名覆盖:%v", name, err)
}
}
check("配置复验", validateCustomSSLReferences(ROOT_PATH, candidate))
cfg := &Config{Domain: "example.com", WebDomain: "web.example.com", Domains: []string{"example.com"}, SSL: candidate.SSL.TLS}
_, err = loadValidatedRuntimeCertificate(cfg)
check("运行时证书", err)
cfg.SSL.Management, cfg.SSL.Challenge, cfg.SSLType = "", "", SSLTypeAutoDNS
_, err = loadValidatedRuntimeCertificate(cfg)
check("旧自动 DNS 配置", err)
})
}
}
func TestProjectDNSExportRejectsMissingCoverageAndKeepsBundle(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("证书导出需要 Linux 权限检查")
}
for _, domain := range []string{"*.example.com", "example.com"} {
t.Run(domain, func(t *testing.T) {
root, output := t.TempDir(), t.TempDir()
if err := os.Chmod(output, 0700); err != nil {
t.Fatal(err)
}
candidate := validRuntimeConfig()
candidate.Root.Domains = DomainConfig{Primary: "example.com", Web: "web.example.com", Accepted: []string{"example.com"}}
install := func(domains ...string) {
t.Helper()
key, certificate := testManagedCertificate(t, domains[0], time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048, domains[1:]...)
reference, err := stageFlatSSLAtRoot(root, ManagedSSLModeDefault, key, certificate, nil, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { releaseManagedSSL(root, SSLConfig{CertificateRef: reference.CertificateRef}) })
candidate.SSL.TLS = SSLConfig{
Management: SSLManagementProject, Challenge: SSLChallengeDNS, Mode: ManagedSSLModeDefault,
DefaultDirectory: "ssl", CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.PrivateKeyRef,
}
writeRuntimeFixture(t, root, candidate)
}
install("example.com", "*.example.com")
destination := filepath.Join(output, "tls.pem")
if err := ExportProjectTLS(root, destination); err != nil {
t.Fatal(err)
}
before, err := os.ReadFile(destination)
if err != nil {
t.Fatal(err)
}
info, err := os.Stat(destination)
if err != nil {
t.Fatal(err)
}
install(domain)
if err := ExportProjectTLS(root, destination); err == nil {
t.Error("导出接受了不完整的根域名与泛域名覆盖")
}
after, err := os.ReadFile(destination)
if err != nil || !bytes.Equal(before, after) {
t.Error("失败导出替换了有效证书内容")
}
current, err := os.Stat(destination)
if err != nil || !os.SameFile(info, current) || current.Mode().Perm() != 0600 {
t.Error("失败导出改变了有效证书文件或权限")
}
})
}
}
func TestProjectDNSReloadKeepsValidCertificateOnMissingRoot(t *testing.T) {
previousRoot := ROOT_PATH
previousConfig, previousPublished := publishedConfig.Load(), hasPublishedConfig.Load()
runtimeManager.Lock()
previousManager := runtimeManager.value
runtimeManager.value = nil
runtimeManager.Unlock()
liveTLS.Lock()
previousLiveRoot, previousCertificate := liveTLS.root, liveTLS.certificate
previousChecked, previousWarned := liveTLS.checked, liveTLS.warned
liveTLS.Unlock()
ROOT_PATH = t.TempDir()
t.Cleanup(func() {
ROOT_PATH = previousRoot
publishedConfig.Store(previousConfig)
hasPublishedConfig.Store(previousPublished)
runtimeManager.Lock()
runtimeManager.value = previousManager
runtimeManager.Unlock()
liveTLS.Lock()
liveTLS.root, liveTLS.certificate = previousLiveRoot, previousCertificate
liveTLS.checked, liveTLS.warned = previousChecked, previousWarned
liveTLS.Unlock()
})
install := func(domains ...string) {
t.Helper()
key, certificate := testManagedCertificate(t, domains[0], time.Now().Add(-time.Hour), time.Now().Add(time.Hour), 2048, domains[1:]...)
reference, err := stageFlatSSLAtRoot(ROOT_PATH, ManagedSSLModeDefault, key, certificate, nil, nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ReleaseStagedSSL(reference) })
PublishConfigSnapshot(&Config{
Domain: "example.com", WebDomain: "web.example.com", Domains: []string{"example.com"},
SSL: SSLConfig{Management: SSLManagementProject, Challenge: SSLChallengeDNS, Mode: reference.Mode,
CertificateRef: reference.CertificateRef, PrivateKeyRef: reference.PrivateKeyRef},
})
}
install("example.com", "*.example.com")
configuration, err := NewRuntimeTLSConfig()
if err != nil {
t.Fatal(err)
}
before, err := configuration.GetCertificate(nil)
if err != nil {
t.Fatal(err)
}
install("*.example.com")
if err := ReloadRuntimeTLSCertificate(); !errors.Is(err, ErrSSLCertificateDomain) {
t.Fatalf("热加载未拒绝缺失根域名的新证书:%v", err)
}
after, err := configuration.GetCertificate(nil)
if err != nil || after == nil || !bytes.Equal(before.Certificate[0], after.Certificate[0]) {
t.Fatal("无效新证书替换了仍有效的内存证书")
}
install("example.com", "*.example.com")
if err := ReloadRuntimeTLSCertificate(); err != nil {
t.Fatal(err)
}
replaced, err := configuration.GetCertificate(nil)
if err != nil || replaced == nil || bytes.Equal(before.Certificate[0], replaced.Certificate[0]) {
t.Fatal("后续有效证书未能热加载")
}
}