Files

165 lines
4.0 KiB
Go

package nginx
import (
"fmt"
"os"
"strings"
"github.com/havline/havline/internal/proxy"
)
func osReadDir(path string) ([]os.DirEntry, error) {
return os.ReadDir(path)
}
type CertSource struct {
Domains []string
CertPath string
KeyPath string
}
func FindCertificateForHosts(certsDir string, hostnames []string, certs []CertSource) *certFiles {
return findCertificateForHosts(certsDir, hostnames, certs)
}
func HasCertificateForHosts(certsDir string, hostnames []string, certs []CertSource) bool {
return certUsable(findCertificateForHosts(certsDir, hostnames, certs))
}
func findCertificateForHosts(certsDir string, hostnames []string, certs []CertSource) *certFiles {
for _, hostname := range hostnames {
for _, source := range certs {
if !hostCoveredByCert(hostname, source.Domains) {
continue
}
if cert := certFromSource(source); cert != nil {
return cert
}
for _, domain := range source.Domains {
if cert := certAt(certsDir, storageDirName(domain)); cert != nil && certUsable(cert) {
return cert
}
}
}
if cert := findCertificate(certsDir, hostname); cert != nil && certUsable(cert) {
return cert
}
}
return scanCertDirectories(certsDir, hostnames)
}
func storageDirName(domain string) string {
domain = strings.TrimSpace(domain)
if strings.HasPrefix(domain, "*.") {
return "wildcard." + strings.TrimPrefix(domain, "*.")
}
return domain
}
func certFromSource(source CertSource) *certFiles {
cert := &certFiles{
CertPath: absNginxPath(source.CertPath),
KeyPath: absNginxPath(source.KeyPath),
}
if !certUsable(cert) {
return nil
}
return cert
}
func hostCoveredByCert(hostname string, domains []string) bool {
hostname = strings.ToLower(strings.TrimSpace(hostname))
for _, domain := range domains {
domain = strings.ToLower(strings.TrimSpace(domain))
if domain == "" {
continue
}
if domain == hostname {
return true
}
if strings.HasPrefix(domain, "*.") {
suffix := strings.TrimPrefix(domain, "*.")
if hostname == suffix || strings.HasSuffix(hostname, "."+suffix) {
return true
}
}
}
return false
}
func scanCertDirectories(certsDir string, hostnames []string) *certFiles {
entries, err := osReadDir(certsDir)
if err != nil {
return nil
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
pattern := dirNameToPattern(entry.Name())
for _, hostname := range hostnames {
if !hostCoveredByCert(hostname, []string{pattern, entry.Name()}) {
continue
}
if cert := certAt(certsDir, pattern); cert != nil && certUsable(cert) {
return cert
}
if cert := certAt(certsDir, entry.Name()); cert != nil && certUsable(cert) {
return cert
}
}
}
return nil
}
func dirNameToPattern(name string) string {
if strings.HasPrefix(name, "wildcard.") {
return "*." + strings.TrimPrefix(name, "wildcard.")
}
return name
}
func certSummary(certs []CertSource) string {
seen := map[string]struct{}{}
var domains []string
for _, cert := range certs {
for _, domain := range cert.Domains {
if domain == "" {
continue
}
if _, ok := seen[domain]; ok {
continue
}
seen[domain] = struct{}{}
domains = append(domains, domain)
}
}
if len(domains) == 0 {
return "无"
}
return strings.Join(domains, ", ")
}
func HTTPSCoverageError(hostname string, certs []CertSource) error {
return fmt.Errorf("域名 %s 未匹配到可用证书(当前证书覆盖:%s),请检查拼写或先在证书页导入/申请", hostname, certSummary(certs))
}
func ValidateHTTPSRules(rules []proxy.Rule, certs []CertSource, certsDir string) error {
for _, rule := range rules {
if !rule.Enabled || !rule.HTTPSEnabled {
continue
}
for _, group := range rule.PortGroups() {
cert := findCertificateForHosts(certsDir, group.Hostnames, certs)
if certUsable(cert) {
continue
}
if len(group.Hostnames) == 0 {
return fmt.Errorf("启用 HTTPS 的规则缺少前端域名")
}
return HTTPSCoverageError(group.Hostnames[0], certs)
}
}
return nil
}