Files
opsxdb/apps/server/internal/db/tls.go
T
logan f8740d27f7 feat: OpsxDB 桌面数据库客户端首版入库
含工作台/连库多引擎字段与 SSH·SSL、结果编辑、Redis 键浏览、
Tauri 桌面与 docs/落地页;排除本地构建产物与 sidecar 二进制。
2026-09-19 22:43:45 +08:00

116 lines
3.0 KiB
Go

package db
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strings"
)
// EffectiveSSLMode resolves ssl / sslMode for drivers.
// Checkbox off or mode disable → disable. Legacy ssl:true with empty mode → require.
func EffectiveSSLMode(c ConnReq) string {
mode := strings.ToLower(strings.TrimSpace(c.SSLMode))
switch mode {
case "disable", "require", "skip-verify", "verify-ca":
if mode == "disable" {
return "disable"
}
return mode
}
if c.SSL {
return "require"
}
return "disable"
}
// SSLEnabled is true when TLS should be negotiated.
func SSLEnabled(c ConnReq) bool {
return EffectiveSSLMode(c) != "disable"
}
// tlsConfigFrom builds a *tls.Config from ConnReq paths/mode.
// Returns (nil, nil) when SSL is disabled.
func tlsConfigFrom(c ConnReq) (*tls.Config, error) {
mode := EffectiveSSLMode(c)
if mode == "disable" {
return nil, nil
}
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if host := strings.TrimSpace(c.Host); host != "" {
cfg.ServerName = host
}
switch mode {
case "skip-verify":
cfg.InsecureSkipVerify = true
case "require":
// Encrypt; verify with system / provided CAs when present.
case "verify-ca":
// Prefer explicit CA; still verify chain (hostname via ServerName when set).
default:
return nil, fmt.Errorf("未知 SSL 模式: %s", mode)
}
if path := strings.TrimSpace(c.SSLCA); path != "" {
pem, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取 CA 证书失败 (%s): %w", path, err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pem) {
return nil, fmt.Errorf("CA 证书不是有效 PEM: %s", path)
}
cfg.RootCAs = pool
if mode == "verify-ca" {
// verify-ca: trust given CA but allow hostname mismatch (common for IP hosts)
cfg.InsecureSkipVerify = true
roots := pool
cfg.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("服务端未提供证书")
}
certs := make([]*x509.Certificate, 0, len(rawCerts))
for i, raw := range rawCerts {
cert, err := x509.ParseCertificate(raw)
if err != nil {
return fmt.Errorf("解析服务端证书失败: %w", err)
}
if i == 0 {
// keep leaf for Intermediates below
}
certs = append(certs, cert)
}
leaf := certs[0]
inter := x509.NewCertPool()
for _, cert := range certs[1:] {
inter.AddCert(cert)
}
_, err := leaf.Verify(x509.VerifyOptions{Roots: roots, Intermediates: inter})
return err
}
}
} else if mode == "verify-ca" {
return nil, fmt.Errorf("verify-ca 模式需要填写 CA 证书路径")
}
certPath := strings.TrimSpace(c.SSLCert)
keyPath := strings.TrimSpace(c.SSLKey)
if certPath != "" || keyPath != "" {
if certPath == "" || keyPath == "" {
return nil, fmt.Errorf("客户端证书与密钥需同时填写")
}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("加载客户端证书失败: %w", err)
}
cfg.Certificates = []tls.Certificate{cert}
}
return cfg, nil
}