优化
This commit is contained in:
truewhile
2026-08-25 14:12:59 +08:00
parent eb09251424
commit da1fb02c9d
18 changed files with 1011 additions and 86 deletions
+160
View File
@@ -0,0 +1,160 @@
package service
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// ResolveSSLMaterial 解析一份 SSL 材料(证书或私钥)的 PEM 内容:
// 优先读取 path 指向的文件,其次使用内容;两者都为空时返回错误。
// what 用于错误提示("证书" / "私钥")。
func ResolveSSLMaterial(content, path, what string) (string, error) {
p := strings.TrimSpace(path)
if p != "" {
if info, err := os.Stat(p); err != nil {
return "", fmt.Errorf("SSL %s文件不可访问:%s(%v)", what, p, err)
} else if info.IsDir() {
return "", fmt.Errorf("SSL %s路径指向的是目录,请填写文件路径:%s", what, p)
}
b, err := os.ReadFile(p)
if err != nil {
return "", fmt.Errorf("读取 SSL %s文件失败:%s(%v)", what, p, err)
}
return strings.TrimSpace(string(b)), nil
}
c := strings.TrimSpace(content)
if c == "" {
return "", fmt.Errorf("SSL %s未配置:请填写内容或文件路径", what)
}
return c, nil
}
// ResolveSSLKeyPair 解析证书与私钥(各自支持 内容或路径),校验格式与匹配后
// 返回可用的 tls.Certificate。任何一步失败都会给出明确错误。
func ResolveSSLKeyPair(certContent, certPath, keyContent, keyPath string) (*tls.Certificate, error) {
certPEM, err := ResolveSSLMaterial(certContent, certPath, "证书")
if err != nil {
return nil, err
}
keyPEM, err := ResolveSSLMaterial(keyContent, keyPath, "私钥")
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
if err != nil {
return nil, fmt.Errorf("SSL 证书/私钥无效:%v", err)
}
if !sslKeyMatchesCert(cert) {
return nil, errors.New("SSL 证书与私钥不匹配")
}
return &cert, nil
}
// PathFingerprint 返回文件路径的内容指纹(路径 + 大小 + 修改时间),用于检测
// 文件是否被替换过;文件不存在时返回 ("", false)。
func PathFingerprint(p string) (string, bool) {
p = strings.TrimSpace(p)
if p == "" {
return "", false
}
info, err := os.Stat(p)
if err != nil {
return "", false
}
return fmt.Sprintf("path:%s|size:%d|mtime:%d", filepath.Clean(p), info.Size(), info.ModTime().UnixNano()), true
}
// ValidateSSLCert 校验 s 是一个可解析的 PEM 编码 X.509 证书。
func ValidateSSLCert(s string) error {
block, _ := pem.Decode([]byte(strings.TrimSpace(s)))
if block == nil {
return errors.New("SSL 证书格式无效:未找到 PEM 数据")
}
if block.Type != "CERTIFICATE" {
return fmt.Errorf("SSL 证书格式无效:期望 CERTIFICATE,实际为 %s", block.Type)
}
if _, err := x509.ParseCertificate(block.Bytes); err != nil {
return fmt.Errorf("SSL 证书解析失败:%v", err)
}
return nil
}
// ValidateSSLKey 校验 s 是一个可解析的 PEM 编码私钥。
func ValidateSSLKey(s string) error {
block, _ := pem.Decode([]byte(strings.TrimSpace(s)))
if block == nil {
return errors.New("SSL 私钥格式无效:未找到 PEM 数据")
}
if _, err := parsePrivateKeyBlock(block); err != nil {
return fmt.Errorf("SSL 私钥解析失败:%v", err)
}
return nil
}
// ValidateSSLKeyPair 校验证书与私钥都存在、可解析且相互匹配。
func ValidateSSLKeyPair(certPEM, keyPEM string) error {
cert, err := tls.X509KeyPair([]byte(strings.TrimSpace(certPEM)), []byte(strings.TrimSpace(keyPEM)))
if err != nil {
return fmt.Errorf("SSL 证书/私钥无效:%v", err)
}
if !sslKeyMatchesCert(cert) {
return errors.New("SSL 证书与私钥不匹配")
}
return nil
}
// sslKeyMatchesCert 通过公钥是否一致来判断私钥确实对应证书。
func sslKeyMatchesCert(cert tls.Certificate) bool {
if len(cert.Certificate) == 0 || cert.PrivateKey == nil {
return false
}
leaf, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return false
}
privPub := publicKeyOf(cert.PrivateKey)
if privPub == nil {
return false
}
eq, ok := leaf.PublicKey.(interface {
Equal(x crypto.PublicKey) bool
})
return ok && eq.Equal(privPub)
}
// publicKeyOf 从各类私钥中提取对应的公钥。
func publicKeyOf(priv crypto.PrivateKey) crypto.PublicKey {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case ed25519.PrivateKey:
return k.Public()
}
return nil
}
// parsePrivateKeyBlock 支持 PKCS#8 / PKCS#1 RSA / EC 三种常见私钥格式。
func parsePrivateKeyBlock(block *pem.Block) (crypto.PrivateKey, error) {
if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
return key, nil
}
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return key, nil
}
if key, err := x509.ParseECPrivateKey(block.Bytes); err == nil {
return key, nil
}
return nil, errors.New("无法解析私钥(支持 PKCS#8 / PKCS#1 RSA / EC)")
}
+121
View File
@@ -0,0 +1,121 @@
package service
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func makeTestKeyPair(t *testing.T) (certPEM, keyPEM string) {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
tpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "localhost"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
DNSNames: []string{"localhost"},
}
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &priv.PublicKey, priv)
if err != nil {
t.Fatal(err)
}
keyDER, err := x509.MarshalECPrivateKey(priv)
if err != nil {
t.Fatal(err)
}
certPEM = strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})))
keyPEM = strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})))
return certPEM, keyPEM
}
func TestResolveSSLMaterial(t *testing.T) {
dir := t.TempDir()
certPEM, _ := makeTestKeyPair(t)
path := filepath.Join(dir, "cert.pem")
if err := os.WriteFile(path, []byte(certPEM), 0o600); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
content string
path string
want string
err bool
}{
{name: "content only", content: certPEM, want: certPEM},
{name: "path only", path: path, want: certPEM},
{name: "path wins over content", content: "bogus", path: path, want: certPEM},
{name: "both empty", err: true},
{name: "missing file", path: filepath.Join(dir, "missing.pem"), err: true},
{name: "path is dir", path: dir, err: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := ResolveSSLMaterial(tc.content, tc.path, "证书")
if tc.err {
if err == nil {
t.Fatalf("expected error, got %q", got)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.want {
t.Fatalf("got %q want %q", got, tc.want)
}
})
}
}
func TestResolveSSLKeyPair(t *testing.T) {
dir := t.TempDir()
certPEM, keyPEM := makeTestKeyPair(t)
certPath := filepath.Join(dir, "cert.pem")
keyPath := filepath.Join(dir, "key.pem")
if err := os.WriteFile(certPath, []byte(certPEM), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(keyPath, []byte(keyPEM), 0o600); err != nil {
t.Fatal(err)
}
if _, err := ResolveSSLKeyPair(certPEM, "", keyPEM, ""); err != nil {
t.Fatalf("content pair: %v", err)
}
if _, err := ResolveSSLKeyPair("", certPath, "", keyPath); err != nil {
t.Fatalf("path pair: %v", err)
}
if _, err := ResolveSSLKeyPair(certPEM, "", "", keyPath); err != nil {
t.Fatalf("mixed pair: %v", err)
}
otherCert, _ := makeTestKeyPair(t)
if _, err := ResolveSSLKeyPair(otherCert, "", keyPEM, ""); err == nil {
t.Fatal("expected mismatch error")
}
if got, ok := PathFingerprint(certPath); !ok || got == "" {
t.Fatalf("PathFingerprint failed: got=%q ok=%v", got, ok)
}
if _, ok := PathFingerprint(filepath.Join(dir, "missing.pem")); ok {
t.Fatal("PathFingerprint should report missing file")
}
if _, ok := PathFingerprint(" "); ok {
t.Fatal("empty PathFingerprint should not be ok")
}
}
+10
View File
@@ -100,6 +100,16 @@ func ApplyRuntimeSetting(cfg *config.Config, key, value string) {
}
case "transcode.video_bitrate", "transcoder.video_bitrate":
cfg.Transcoder.VideoBitrate = value
case "https.enabled":
cfg.App.HTTPSEnabled = parseBoolSetting(value, false)
case "https.cert":
cfg.App.SSLCert = value
case "https.key":
cfg.App.SSLKey = value
case "https.cert_path":
cfg.App.SSLCertPath = strings.TrimSpace(value)
case "https.key_path":
cfg.App.SSLKeyPath = strings.TrimSpace(value)
}
}
+4
View File
@@ -64,6 +64,10 @@ type Container struct {
stopCtx context.Context
stopCancel context.CancelFunc
// ReloadHTTPServer 由 cmd/server 注入。HTTPS 相关设置保存后,handler
// 会调用它把 HTTP/HTTPS 监听热切换到最新配置;nil 表示未注入(测试环境)。
ReloadHTTPServer func() error
}
// New 构建服务容器。