103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
|
)
|
|
|
|
type ManagementCertificateState struct {
|
|
Hostname string
|
|
CertificatePath string
|
|
PrivateKeyPath string
|
|
NotAfter time.Time
|
|
}
|
|
|
|
// ManagementCertificate 返回实际配置的管理证书,不依赖业务域名仓储。
|
|
func (s *Server) ManagementCertificate() (ManagementCertificateState, error) {
|
|
s.configMu.RLock()
|
|
defer s.configMu.RUnlock()
|
|
state := ManagementCertificateState{
|
|
Hostname: strings.TrimSpace(s.config.Web.Hostname),
|
|
CertificatePath: s.config.Server.TLSCert,
|
|
PrivateKeyPath: s.config.Server.TLSKey,
|
|
}
|
|
pair, err := tls.LoadX509KeyPair(state.CertificatePath, state.PrivateKeyPath)
|
|
if err != nil {
|
|
return state, fmt.Errorf("读取管理证书失败:%w", err)
|
|
}
|
|
leaf, err := x509.ParseCertificate(pair.Certificate[0])
|
|
if err != nil {
|
|
return state, fmt.Errorf("解析管理证书失败:%w", err)
|
|
}
|
|
state.NotAfter = leaf.NotAfter
|
|
return state, nil
|
|
}
|
|
|
|
// ActivateTLSCertificate 先持久化完整证书路径,再切换不可变快照;失败不影响旧连接。
|
|
// 此方法仅供进程内部续期流程调用,不提供上传或任意路径的 HTTP 接口。
|
|
func (s *Server) ActivateTLSCertificate(certificatePath, privateKeyPath string) error {
|
|
s.tlsMu.Lock()
|
|
defer s.tlsMu.Unlock()
|
|
s.configMu.Lock()
|
|
defer s.configMu.Unlock()
|
|
if !s.config.SetupReady || strings.TrimSpace(s.configPath) == "" {
|
|
return errors.New("管理证书热更新需要已初始化的持久化配置")
|
|
}
|
|
certificatePath = strings.TrimSpace(certificatePath)
|
|
privateKeyPath = strings.TrimSpace(privateKeyPath)
|
|
pair, err := tls.LoadX509KeyPair(certificatePath, privateKeyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("加载新的管理证书失败:%w", err)
|
|
}
|
|
leaf, err := x509.ParseCertificate(pair.Certificate[0])
|
|
if err != nil {
|
|
return fmt.Errorf("解析新的管理证书失败:%w", err)
|
|
}
|
|
now := time.Now()
|
|
if now.Before(leaf.NotBefore) || !now.Before(leaf.NotAfter) {
|
|
return errors.New("新的管理证书不在有效期内")
|
|
}
|
|
host := strings.TrimSpace(s.config.Web.Hostname)
|
|
if host == "" {
|
|
host = strings.TrimSpace(s.config.Web.PublicIP)
|
|
}
|
|
if host == "" {
|
|
return errors.New("管理域名或公网 IP 未配置")
|
|
}
|
|
// 校验与切换使用同一份内存数据,避免重复读文件造成校验对象变化。
|
|
var chain []byte
|
|
for _, der := range pair.Certificate {
|
|
chain = append(chain, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...)
|
|
}
|
|
validator := certificate.LocalValidator{VerifyChain: true, Roots: s.tlsRoots}
|
|
if _, err := validator.ValidatePEM(context.Background(), chain, []string{host}); err != nil {
|
|
return fmt.Errorf("新的管理证书校验失败:%w", err)
|
|
}
|
|
pair.Leaf = leaf
|
|
candidate := s.config
|
|
candidate.Server.TLSCert = certificatePath
|
|
candidate.Server.TLSKey = privateKeyPath
|
|
if err := candidate.Save(s.configPath); err != nil {
|
|
return fmt.Errorf("保存管理证书配置失败:%w", err)
|
|
}
|
|
s.config = candidate
|
|
s.tlsCertificate.Store(&pair)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getTLSCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
pair := s.tlsCertificate.Load()
|
|
if pair == nil {
|
|
return nil, errors.New("管理 HTTPS 证书尚未加载")
|
|
}
|
|
return pair, nil
|
|
}
|