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" } // tlsServerNameFor 返回 TLS 校验用的 ServerName:显式 tlsServerName 优先,否则用 Host。 // SSH 隧道把 Host 改写成 127.0.0.1 之前,ApplySSHTunnel 会把原 Host 存入 TLSServerName。 func tlsServerNameFor(c ConnReq) string { if v := strings.TrimSpace(c.TLSServerName); v != "" { return v } return strings.TrimSpace(c.Host) } // 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 := tlsServerNameFor(c); 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) } caContent := strings.TrimSpace(c.SSLCAContent) caPath := strings.TrimSpace(c.SSLCA) if caContent != "" || caPath != "" { // 内容优先于路径(免在服务端落盘/共享文件) var pem []byte if caContent != "" { pem = []byte(caContent) } else { var err error pem, err = os.ReadFile(caPath) if err != nil { return nil, fmt.Errorf("读取 CA 证书失败 (%s): %w", caPath, err) } } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pem) { if caContent != "" { return nil, fmt.Errorf("CA 证书内容不是有效 PEM") } return nil, fmt.Errorf("CA 证书不是有效 PEM: %s", caPath) } 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 证书路径") } certContent := strings.TrimSpace(c.SSLCertContent) keyContent := strings.TrimSpace(c.SSLKeyContent) certPath := strings.TrimSpace(c.SSLCert) keyPath := strings.TrimSpace(c.SSLKey) if certContent != "" || keyContent != "" { // 内容与路径二选一,各自成对:证书与密钥必须同时给 if certContent == "" || keyContent == "" { return nil, fmt.Errorf("客户端证书与密钥需同时填写") } cert, err := tls.X509KeyPair([]byte(certContent), []byte(keyContent)) if err != nil { return nil, fmt.Errorf("加载客户端证书内容失败: %w", err) } cfg.Certificates = []tls.Certificate{cert} } else 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 }