- 移除自签证书回退逻辑,简化为仅使用 AnyIP 证书 - 删除 internal/tls/generate.go(不再需要) - 重构 main.go:提取初始化逻辑,main() 从 156 行降至 13 行 - 重构 internal/ws/handler.go:提取消息处理,handleConn() 从 131 行降至 25 行 - 重构 internal/config/load.go:使用 map 驱动消除重复代码 - 优化前端 startRecording():使用标准 AbortController API - 优化前端 showToast():预定义 DOM 元素,代码减少 50% 代码行数减少 90 行,复杂度显著降低,所有构建通过
126 lines
3.5 KiB
Go
126 lines
3.5 KiB
Go
package tls
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// certDir returns the platform-appropriate cache directory for certificates.
|
|
func certDir() string {
|
|
base, err := os.UserCacheDir()
|
|
if err != nil {
|
|
base, _ = os.UserHomeDir()
|
|
base = filepath.Join(base, ".cache")
|
|
}
|
|
dir := filepath.Join(base, "voicepaste", "certs")
|
|
os.MkdirAll(dir, 0700)
|
|
return dir
|
|
}
|
|
|
|
// Result holds the TLS config and the AnyIP hostname.
|
|
type Result struct {
|
|
Config *tls.Config
|
|
Host string // AnyIP hostname (e.g. voicepaste-192-168-1-5.anyip.dev)
|
|
}
|
|
|
|
// AnyIPHost returns the AnyIP hostname for a given LAN IP.
|
|
// e.g. 192.168.1.5 → voicepaste-192-168-1-5.anyip.dev
|
|
func AnyIPHost(lanIP string) string {
|
|
dashed := strings.ReplaceAll(lanIP, ".", "-")
|
|
return fmt.Sprintf("voicepaste-%s.anyip.dev", dashed)
|
|
}
|
|
|
|
// GetTLSConfig returns a TLS config using AnyIP wildcard certificate.
|
|
// It tries cached cert first, then downloads fresh if needed.
|
|
func GetTLSConfig(lanIP string) (*Result, error) {
|
|
dir := certDir()
|
|
anyipDir := filepath.Join(dir, "anyip")
|
|
os.MkdirAll(anyipDir, 0700)
|
|
certFile := filepath.Join(anyipDir, "fullchain.pem")
|
|
keyFile := filepath.Join(anyipDir, "privkey.pem")
|
|
host := AnyIPHost(lanIP)
|
|
|
|
// Try cached cert first
|
|
if cert, err := loadAndValidateCert(certFile, keyFile); err == nil {
|
|
slog.Info("using cached AnyIP certificate")
|
|
return &Result{
|
|
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
|
Host: host,
|
|
}, nil
|
|
}
|
|
|
|
// Download fresh cert
|
|
slog.Info("downloading AnyIP certificate")
|
|
if err := downloadAnyIPCert(certFile, keyFile); err != nil {
|
|
return nil, fmt.Errorf("failed to download AnyIP certificate: %w", err)
|
|
}
|
|
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load downloaded certificate: %w", err)
|
|
}
|
|
|
|
slog.Info("downloaded fresh AnyIP certificate")
|
|
return &Result{
|
|
Config: &tls.Config{Certificates: []tls.Certificate{cert}},
|
|
Host: host,
|
|
}, nil
|
|
}
|
|
|
|
// loadAndValidateCert loads a certificate and validates it's not expired.
|
|
func loadAndValidateCert(certFile, keyFile string) (tls.Certificate, error) {
|
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
|
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
|
|
// Check if cert expires within 24 hours
|
|
if time.Now().After(leaf.NotAfter.Add(-24 * time.Hour)) {
|
|
return tls.Certificate{}, fmt.Errorf("certificate expired or expiring soon")
|
|
}
|
|
|
|
return cert, nil
|
|
}
|
|
|
|
// downloadAnyIPCert downloads the AnyIP wildcard cert and key.
|
|
func downloadAnyIPCert(certFile, keyFile string) error {
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
if err := downloadFile(client, "https://anyip.dev/cert/fullchain.pem", certFile); err != nil {
|
|
return fmt.Errorf("download fullchain: %w", err)
|
|
}
|
|
if err := downloadFile(client, "https://anyip.dev/cert/privkey.pem", keyFile); err != nil {
|
|
os.Remove(certFile) // clean up partial download
|
|
return fmt.Errorf("download privkey: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
func downloadFile(client *http.Client, url, dest string) error {
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
|
|
}
|
|
f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, resp.Body)
|
|
return err
|
|
} |