nsfwapp/backend/tls_dynamic.go
2026-06-26 16:31:08 +02:00

238 lines
4.9 KiB
Go

package main
import (
"crypto/ecdsa"
"crypto/elliptic"
cryptoRand "crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
var (
localTLSOnce sync.Once
localTLSCertPath string
localTLSKeyPath string
localTLSErr error
)
func localTLSPathsCached() (string, string, error) {
localTLSOnce.Do(func() {
localTLSCertPath, localTLSKeyPath, localTLSErr = ensureLocalTLSCertificate()
})
return localTLSCertPath, localTLSKeyPath, localTLSErr
}
func localTLSDir() (string, error) {
if dir, err := os.UserConfigDir(); err == nil && strings.TrimSpace(dir) != "" {
return filepath.Join(dir, "nsfwapp", "tls"), nil
}
home, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(home) == "" {
return "", fmt.Errorf("user config dir konnte nicht ermittelt werden")
}
return filepath.Join(home, ".config", "nsfwapp", "tls"), nil
}
func localTLSHosts() []string {
seen := map[string]bool{}
out := []string{}
add := func(host string) {
host = normalizeOriginHost(host)
if host == "" || seen[host] {
return
}
seen[host] = true
out = append(out, host)
}
for _, origin := range configuredPublicAppOrigins() {
u, err := url.Parse(strings.TrimSpace(origin))
if err == nil {
add(u.Hostname())
}
}
for _, host := range localAppHostCandidates() {
add(host)
}
add("localhost")
add("127.0.0.1")
add("::1")
sort.Strings(out)
return out
}
func tlsCertificateCoversHosts(certPath string, hosts []string) bool {
cert, err := readLeafCertificate(certPath)
if err != nil {
return false
}
now := time.Now()
if now.Before(cert.NotBefore) || now.After(cert.NotAfter.Add(-30*24*time.Hour)) {
return false
}
for _, host := range hosts {
host = normalizeOriginHost(host)
if host == "" {
continue
}
if err := cert.VerifyHostname(host); err != nil {
return false
}
}
return true
}
func readLeafCertificate(certPath string) (*x509.Certificate, error) {
b, err := os.ReadFile(certPath)
if err != nil {
return nil, err
}
block, _ := pem.Decode(b)
if block == nil || block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("keine PEM-Zertifikatsdaten")
}
return x509.ParseCertificate(block.Bytes)
}
func ensureLocalTLSCertificate() (string, string, error) {
dir, err := localTLSDir()
if err != nil {
return "", "", err
}
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", "", err
}
certPath := filepath.Join(dir, "recorder-cert.pem")
keyPath := filepath.Join(dir, "recorder-key.pem")
hosts := localTLSHosts()
if fileExistsNonEmpty(certPath) &&
fileExistsNonEmpty(keyPath) &&
tlsCertificateCoversHosts(certPath, hosts) {
appLogln("🔐 TLS-Zertifikat aktiv:", certPath)
return certPath, keyPath, nil
}
if err := generateLocalTLSCertificate(certPath, keyPath, hosts); err != nil {
return "", "", err
}
appLogln("🔐 TLS-Zertifikat neu erstellt:", certPath)
appLogln("🔐 TLS-Namen:", strings.Join(hosts, ", "))
return certPath, keyPath, nil
}
func generateLocalTLSCertificate(certPath string, keyPath string, hosts []string) error {
key, err := ecdsa.GenerateKey(elliptic.P256(), cryptoRand.Reader)
if err != nil {
return err
}
serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serial, err := cryptoRand.Int(cryptoRand.Reader, serialLimit)
if err != nil {
return err
}
notBefore := time.Now().Add(-1 * time.Hour)
notAfter := notBefore.Add(397 * 24 * time.Hour)
tpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "nsfwapp local",
Organization: []string{"nsfwapp"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
seenDNS := map[string]bool{}
seenIP := map[string]bool{}
for _, host := range hosts {
host = normalizeOriginHost(host)
if host == "" {
continue
}
if ip := net.ParseIP(host); ip != nil {
key := ip.String()
if !seenIP[key] {
seenIP[key] = true
tpl.IPAddresses = append(tpl.IPAddresses, ip)
}
continue
}
if !seenDNS[host] {
seenDNS[host] = true
tpl.DNSNames = append(tpl.DNSNames, host)
}
}
if len(tpl.DNSNames) == 0 && len(tpl.IPAddresses) == 0 {
tpl.DNSNames = []string{"localhost"}
}
certDER, err := x509.CreateCertificate(
cryptoRand.Reader,
tpl,
tpl,
&key.PublicKey,
key,
)
if err != nil {
return err
}
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyDER,
})
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
return err
}
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
return err
}
return nil
}