Files
voicepaste/internal/tls/tls.go
imbytecat b87fead2fd refactor: 优化代码质量,遵循 KISS 原则
- 移除自签证书回退逻辑,简化为仅使用 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 行,复杂度显著降低,所有构建通过
2026-03-02 00:25:14 +08:00

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
}