diff --git a/backend/auth_passkey.go b/backend/auth_passkey.go index b182e53..0b8f44d 100644 --- a/backend/auth_passkey.go +++ b/backend/auth_passkey.go @@ -3,6 +3,7 @@ package main import ( + "context" "crypto/rand" "encoding/base64" "encoding/json" @@ -246,6 +247,7 @@ func localAppHostCandidates() []string { host = normalizeOriginHost(host) if host != "" && !strings.Contains(host, ".") { add(host + ".local") + addHostWithDNSSuffixes(add, host) } } @@ -278,6 +280,44 @@ func addInterfaceIPv4Hosts(add func(string)) { } add(ip4.String()) + addReverseLookupHosts(add, ip4) + } +} + +func addHostWithDNSSuffixes(add func(string), host string) { + host = normalizeOriginHost(host) + if host == "" || strings.Contains(host, ".") { + return + } + + for _, key := range []string{ + "USERDNSDOMAIN", + "DNSDOMAIN", + "LOCALDOMAIN", + } { + suffix := normalizeOriginHost(os.Getenv(key)) + if suffix == "" || !strings.Contains(suffix, ".") { + continue + } + add(host + "." + suffix) + } +} + +func addReverseLookupHosts(add func(string), ip net.IP) { + if ip == nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + names, err := net.DefaultResolver.LookupAddr(ctx, ip.String()) + if err != nil { + return + } + + for _, name := range names { + add(name) } } diff --git a/backend/dist/nsfwapp-linux-amd64 b/backend/dist/nsfwapp-linux-amd64 index bce836d..dd91f16 100644 Binary files a/backend/dist/nsfwapp-linux-amd64 and b/backend/dist/nsfwapp-linux-amd64 differ diff --git a/backend/dist/nsfwapp.exe b/backend/dist/nsfwapp.exe index fd73935..523625d 100644 Binary files a/backend/dist/nsfwapp.exe and b/backend/dist/nsfwapp.exe differ diff --git a/backend/dist/nsfwapp_amd64.deb b/backend/dist/nsfwapp_amd64.deb index ec46339..ef1c1ad 100644 Binary files a/backend/dist/nsfwapp_amd64.deb and b/backend/dist/nsfwapp_amd64.deb differ diff --git a/backend/embed.go b/backend/embed.go index 0a4b015..4a2ec8b 100644 --- a/backend/embed.go +++ b/backend/embed.go @@ -8,7 +8,7 @@ import ( "path/filepath" ) -//go:embed ml/*.py ml/detection_labels.json ai_server.py recorder-cert.pem recorder-key.pem yolo26n-pose.pt +//go:embed ml/*.py ml/detection_labels.json ai_server.py yolo26n-pose.pt var embeddedMLFiles embed.FS func embeddedWriteFileIfNeeded(dstPath string, b []byte, perm os.FileMode) error { @@ -126,42 +126,3 @@ func embeddedPoseModelPath() (string, error) { return dstPath, nil } - -func embeddedTLSCertBytes() ([]byte, error) { - return embeddedMLFiles.ReadFile("recorder-cert.pem") -} - -func embeddedTLSKeyBytes() ([]byte, error) { - return embeddedMLFiles.ReadFile("recorder-key.pem") -} - -func embeddedTLSFilesDir() (string, string, error) { - dir := filepath.Join(os.TempDir(), "nsfwapp-tls") - - if err := os.MkdirAll(dir, 0700); err != nil { - return "", "", err - } - - certBytes, err := embeddedTLSCertBytes() - if err != nil { - return "", "", err - } - - keyBytes, err := embeddedTLSKeyBytes() - if err != nil { - return "", "", err - } - - certPath := filepath.Join(dir, "recorder-cert.pem") - keyPath := filepath.Join(dir, "recorder-key.pem") - - if err := os.WriteFile(certPath, certBytes, 0600); err != nil { - return "", "", err - } - - if err := os.WriteFile(keyPath, keyBytes, 0600); err != nil { - return "", "", err - } - - return certPath, keyPath, nil -} diff --git a/backend/log.go b/backend/log.go index 1a7b63e..241243d 100644 --- a/backend/log.go +++ b/backend/log.go @@ -5,17 +5,22 @@ package main import ( "fmt" "io" + "log" "net/http" "os" "path/filepath" + "runtime/debug" "strings" "sync" "time" ) var ( - appLogMu sync.Mutex - appLogFile *os.File + appLogMu sync.Mutex + appLogFile *os.File + appConsoleOut *os.File + appStdStreamsRedirected bool + appDiagnosticsConfigured bool ) func appLogPath() string { @@ -59,6 +64,7 @@ func initAppLog() { } appLogFile = f + configureAppDiagnosticsLocked() _, _ = fmt.Fprintf( appLogFile, @@ -69,6 +75,26 @@ func initAppLog() { _ = appLogFile.Sync() } +func configureAppDiagnosticsLocked() { + if appLogFile == nil || appDiagnosticsConfigured { + return + } + + log.SetOutput(appLogFile) + log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile) + debug.SetTraceback("all") + + appConsoleOut = os.Stdout + + if err := redirectProcessStdStreamsToFile(appLogFile); err != nil { + _, _ = fmt.Fprintf(appLogFile, "⚠️ stdout/stderr konnte nicht auf recorder.log umgeleitet werden: %v\n", err) + } else { + appStdStreamsRedirected = true + } + + appDiagnosticsConfigured = true +} + func closeAppLog() { appLogMu.Lock() defer appLogMu.Unlock() @@ -113,7 +139,13 @@ func appLogWriteLine(line string) { appLogMu.Lock() defer appLogMu.Unlock() - fmt.Print(out) + if appStdStreamsRedirected { + if appConsoleOut != nil && appConsoleOut != appLogFile { + _, _ = appConsoleOut.WriteString(out) + } + } else { + fmt.Print(out) + } if appLogFile != nil { _, _ = appLogFile.WriteString(out) @@ -129,6 +161,44 @@ func appLogf(format string, args ...any) { appLogWriteLine(fmt.Sprintf(format, args...)) } +func appLogMultiline(prefix string, text string) { + text = strings.TrimRight(text, "\r\n") + if text == "" { + return + } + + for _, line := range strings.Split(text, "\n") { + appLogWriteLine(prefix + strings.TrimRight(line, "\r")) + } +} + +func appLogPanic(scope string, recovered any) { + scope = strings.TrimSpace(scope) + if scope == "" { + scope = "unbekannt" + } + + appLogln("❌ PANIC in "+scope+":", recovered) + appLogln("Stacktrace:") + appLogMultiline(" ", string(debug.Stack())) +} + +func appRecoverPanic(scope string) bool { + if recovered := recover(); recovered != nil { + appLogPanic(scope, recovered) + return true + } + + return false +} + +func appGo(scope string, fn func()) { + go func() { + defer appRecoverPanic(scope) + fn() + }() +} + // appErrorLogSuppressed unterdrückt das automatische ❌-Logging für bekannte, // erwartbare und sehr häufige Fehler (z.B. abgelaufene HLS-Edge-URLs während // eines Stream-Reconnects). Der Fehler wird weiterhin zurückgegeben, damit die diff --git a/backend/log_redirect_unix.go b/backend/log_redirect_unix.go new file mode 100644 index 0000000..d523d95 --- /dev/null +++ b/backend/log_redirect_unix.go @@ -0,0 +1,26 @@ +//go:build !windows + +package main + +import ( + "os" + "syscall" +) + +func redirectProcessStdStreamsToFile(f *os.File) error { + if f == nil { + return nil + } + + fd := int(f.Fd()) + if err := syscall.Dup2(fd, int(os.Stdout.Fd())); err != nil { + return err + } + if err := syscall.Dup2(fd, int(os.Stderr.Fd())); err != nil { + return err + } + + os.Stdout = f + os.Stderr = f + return nil +} diff --git a/backend/log_redirect_windows.go b/backend/log_redirect_windows.go new file mode 100644 index 0000000..aadde25 --- /dev/null +++ b/backend/log_redirect_windows.go @@ -0,0 +1,27 @@ +//go:build windows + +package main + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func redirectProcessStdStreamsToFile(f *os.File) error { + if f == nil { + return nil + } + + handle := windows.Handle(f.Fd()) + if err := windows.SetStdHandle(windows.STD_OUTPUT_HANDLE, handle); err != nil { + return err + } + if err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, handle); err != nil { + return err + } + + os.Stdout = f + os.Stderr = f + return nil +} diff --git a/backend/recorder-cert.pem b/backend/recorder-cert.pem deleted file mode 100644 index 33c3442..0000000 --- a/backend/recorder-cert.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEbjCCAtagAwIBAgIRAKGvq9b44yCGC32q8PwkueowDQYJKoZIhvcNAQELBQAw -gYsxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEwMC4GA1UECwwnVEVH -RFNTRFxSb3RoZXJATDE0UEJCSzk1MTAwMDA2IChSb3RoZXIpMTcwNQYDVQQDDC5t -a2NlcnQgVEVHRFNTRFxSb3RoZXJATDE0UEJCSzk1MTAwMDA2IChSb3RoZXIpMB4X -DTI2MDUwODEwMDQzN1oXDTI4MDgwODEwMDQzN1owWzEnMCUGA1UEChMebWtjZXJ0 -IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTAwLgYDVQQLDCdURUdEU1NEXFJvdGhl -ckBMMTRQQkJLOTUxMDAwMDYgKFJvdGhlcikwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDDghqU3igC3sNP2zAJTZbvZcT1xxjUgk3oA2j0WwNPO+xhCH4K -mJ6enU5zrGWI4+T0IHfbo737rvgk6XfTDSvzB9Uags8egBUGxbr9eMBya0hmnYGs -zIQGJeI4TIeQ7sa7krCpMB/e1YxR1qYFf1C8oSt43dJCdJP4Ev//BaR7rrFt3D3N -CC2OgxVYSh/hR9XNIIEPMoioQIYhle41mQWjyPFt16Fjg9sWrrnHSz7M4cgVVSot -+aBlRjbxgOarLyST3XdtjAfC6CqxnzprX1LfznuULjatoS+pFDBDoy2B13xwa9Br -f+Xy3CswmSO+AzNomWfbeBkJnl6xb3Tb1urrAgMBAAGjfDB6MA4GA1UdDwEB/wQE -AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBR0Zph7hFKq4GpE -XO0y3U+T0iFoQDAyBgNVHREEKzApgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAA -AAAAAAAAAAGHBAoAARkwDQYJKoZIhvcNAQELBQADggGBAJShhgQS+9cwuVOJUIW+ -uy9t1ZAjR9qTd31kHdK1vu294GiavrG8RD+92BRujG4Fq1ambvqcu9sUaC8+0HAP -cQvdmnbTxG0ZHkGP7VVj3Poee0jpsVAVtV3aVwKxyO/E7Mv/nEKTqNfnECbEuiqC -tOJ8mI3E9tf2/ljzq2PqvfIPFBBtUT2lihNAFXM/06Nzoa1NPdfYd58N+VBFSOSq -zEM4fmpcqMThZGKGBoWmHwTE5Mw6mZFLyxHJpOFu5wNzyPJGes5o/F0ot5n+ZGah -GbYaGmhEedOxC3/WXRtexJmZ0jrB8Hkx+c/4h6+0HKU/PelrFJS+c2d6h8YGXyhv -pUsfyyoQzx7rSgyLfJqXK+52ZtnWNc0yVVMyKJwjvhv4mcP9l/q3UGR/i7xkD5Pc -2bLgM+wc8nKI7Yd/xejLpQDC0b/P1XibT1T3GRjiC/eu64G/uFOF+m3P5XvmF8JO -0cj8fXwUCq+TVD+22rFmtT+2VYkf8RUsC48Ou+PHccmDyw== ------END CERTIFICATE----- diff --git a/backend/recorder-key.pem b/backend/recorder-key.pem deleted file mode 100644 index 67692b1..0000000 --- a/backend/recorder-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDDghqU3igC3sNP -2zAJTZbvZcT1xxjUgk3oA2j0WwNPO+xhCH4KmJ6enU5zrGWI4+T0IHfbo737rvgk -6XfTDSvzB9Uags8egBUGxbr9eMBya0hmnYGszIQGJeI4TIeQ7sa7krCpMB/e1YxR -1qYFf1C8oSt43dJCdJP4Ev//BaR7rrFt3D3NCC2OgxVYSh/hR9XNIIEPMoioQIYh -le41mQWjyPFt16Fjg9sWrrnHSz7M4cgVVSot+aBlRjbxgOarLyST3XdtjAfC6Cqx -nzprX1LfznuULjatoS+pFDBDoy2B13xwa9Brf+Xy3CswmSO+AzNomWfbeBkJnl6x -b3Tb1urrAgMBAAECggEBAI31EBv72w2KdkKroot+vROCz6quMAdNvgezQif7VcHY -fuBN7EcBXltJWUeAbBEjeIESejUPBcmT2DXlF841CC5lB4VCaeV5lsreE9IsNYBf -CakIwLmZnltgcovydZT063QTJRcUDHAems5pjw76zMLKO+h9GEiMoUxFb3/atv3e -K5HE+uUd6JcPU0q91S6TxqvS9gH30OOkwH6Kl4tzWauQAKg/sVzYafbyTAuEpiMO -jLLLAn/sOmiL4knnRVu1FK4aZSH+t9BN9YuwfucXFn9jFgPtIJ0bF1GuqnwIeDI5 -BTNanHfaZRHD0+FbS0KbhARIFgDveoGrdoEtou0eDOECgYEA5dJqUvk96f7zCrxD -eSuUl1MvOjydY3GacfjevZoTxJDqh4Wt2wgLYJc4XB7tI02ci7NncecG8qPfqWq7 -aFjQAGAYdR+XvE7R2m3OFK9WbRX4cH6b+l7mSq0eOvcAAajn4RuNr/il+7Opw7zX -eKUB/pnW36B+ethdr5l+kDUQb8MCgYEA2ccanIT2QAxJb5j0F287NGhaTPuWfxcX -2T53I2fIwMiell0YXjwsKwKpckubRB2f5X4b9QJifUTmtxHky/Tx4H9OVhCaxumw -edol5YNzZaP/Tx8m2mqpyN2WPbezIFeA+GNcmDAxEo/NT2B64OVCHWKosndvb+Yk -GFmVb+g9zbkCgYEA1YFJLZRHJJ+pgour02HdRUgOU/gD72KWrNMbeuEtBCvs9cIG -5bjveOiDf3FrtKRhjpc4vuR12+zJ2EZDnIkFk5OypPyYpmRDKL1h+m15yRXkG/5D -QbHwF+gEcZsN8nzMDqDeXGCPMuqSCDnjoz0IQVMB//bGCbIANyZOIgJqJqkCgYEA -vHc+ZG383fjEJLvtoco1JmmYnD6uQ1Ys4WjZmd5bMdtswxvV1tekMaSgF7WurQgm -NGkqsKJbsaVLNOtbYdac7He/x2OfTr02aH2Nhk54M2H1tPd0nFjqjlaVitvLPRX9 -GviCTYKHNVUVjLgmHzLIQL382FXcLq6wVhJQ7QPDWKECgYA6ZmdB8waN2SFOJRnj -5zWTgdImicl4yCu8PaEyloO9whYSdYH8zU3K+FKaowXGNInp3BGCmP9TUwoG8Iwx -Ug5d55P2FgwnhCi59Y6dMXih1p94BuQ0vcWPBqztofl7lZbBEnjwzKVycFvcyNFX -QFb/UA4/CfQli9ciDtlqzZt8SA== ------END PRIVATE KEY----- diff --git a/backend/server.go b/backend/server.go index 2a35e99..344d17b 100644 --- a/backend/server.go +++ b/backend/server.go @@ -22,19 +22,6 @@ import ( "github.com/joho/godotenv" ) -var embeddedTLSOnce sync.Once -var embeddedTLSCertPath string -var embeddedTLSKeyPath string -var embeddedTLSErr error - -func embeddedTLSPathsCached() (string, string, error) { - embeddedTLSOnce.Do(func() { - embeddedTLSCertPath, embeddedTLSKeyPath, embeddedTLSErr = embeddedTLSFilesDir() - }) - - return embeddedTLSCertPath, embeddedTLSKeyPath, embeddedTLSErr -} - func escapePowerShell(s string) string { return strings.ReplaceAll(s, "'", "''") } @@ -654,7 +641,7 @@ func startAIServer(ctx context.Context) (*aiServerProcess, error) { done: make(chan struct{}), } - go func() { + appGo("ai-server-wait", func() { err := cmd.Wait() if err != nil && ctx.Err() == nil && !proc.isStopping() { @@ -664,7 +651,7 @@ func startAIServer(ctx context.Context) (*aiServerProcess, error) { } close(proc.done) - }() + }) if err := waitForAIServer(ctx, aiServerURL(), 120*time.Second); err != nil { appLogln("⚠️ AI Server noch nicht bereit:", err) @@ -820,7 +807,7 @@ func startTrainingBestModelWatcher(ctx context.Context, trainingRoot string) { lastRestart = time.Now() appLogln("🔄 Training-Modell geändert:", strings.Join(changedPaths, " | ")) - go restartAIServer() + appGo("ai-server-restart", restartAIServer) } } } @@ -961,12 +948,15 @@ func tlsCertFile() string { return resolveMaybeRelativeToExe(raw) } - certPath, _, err := embeddedTLSPathsCached() + certPath, _, err := localTLSPathsCached() if err == nil && certPath != "" { return certPath } + if err != nil { + appLogln("⚠️ dynamisches TLS-Zertifikat konnte nicht erstellt werden:", err) + } - return resolveMaybeRelativeToExe("recorder-cert.pem") + return "" } func tlsKeyFile() string { @@ -975,12 +965,16 @@ func tlsKeyFile() string { return resolveMaybeRelativeToExe(raw) } - _, keyPath, err := embeddedTLSPathsCached() + _, keyPath, err := localTLSPathsCached() if err == nil && keyPath != "" { return keyPath } - return resolveMaybeRelativeToExe("recorder-key.pem") + if err != nil { + appLogln("⚠️ dynamischer TLS-Key konnte nicht erstellt werden:", err) + } + + return "" } func publicAppURL() string { @@ -1030,7 +1024,13 @@ func publicAppURLs() []string { func main() { clearAppLog() initAppLog() - defer closeAppLog() + defer func() { + panicked := appRecoverPanic("main") + closeAppLog() + if panicked { + os.Exit(2) + } + }() loadAppEnv() @@ -1055,7 +1055,7 @@ func main() { "Die Anwendung wird bereits ausgeführt.", ) appLogln("⚠️ Die App läuft bereits oder Port " + appPort() + " ist bereits belegt.") - os.Exit(0) + return } defer appLn.Close() @@ -1071,7 +1071,7 @@ func main() { startPostworkLeftoverScanOnStartup() - go startGeneratedGarbageCollector() + appGo("generated-garbage-collector", startGeneratedGarbageCollector) // Altes NSFW-ONNX nicht mehr hart initialisieren. // Das neue Training-/YOLO-Modell wird über trainingPredictFrame genutzt. @@ -1089,7 +1089,7 @@ func main() { if err != nil { appLogln("❌ auth init:", err) closeNSFW() - os.Exit(1) + return } store := registerRoutes(mux, auth) @@ -1100,14 +1100,14 @@ func main() { } // Hintergrund-Worker - go startDiskSpaceGuard() - go startChaturbateOnlinePoller(store) - go startChaturbateAutoStartWorker(store) - go startMyFreeCamsAutoStartWorker(store) + appGo("disk-space-guard", startDiskSpaceGuard) + appGo("chaturbate-online-poller", func() { startChaturbateOnlinePoller(store) }) + appGo("chaturbate-autostart-worker", func() { startChaturbateAutoStartWorker(store) }) + appGo("myfreecams-autostart-worker", func() { startMyFreeCamsAutoStartWorker(store) }) // optional: Bootstrap nur im Hintergrund if getSettings().UseChaturbateAPI { - go func() { + appGo("chaturbate-api-bootstrap", func() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() @@ -1116,7 +1116,7 @@ func main() { } else { appLogln("✅ Chaturbate API geladen") } - }() + }) } if _, err := ensureCoversDir(); err != nil { @@ -1164,14 +1164,14 @@ func main() { stopSig := make(chan os.Signal, 1) signal.Notify(stopSig, os.Interrupt, syscall.SIGTERM) - go func() { + appGo("shutdown-signal-listener", func() { <-stopSig shutdown() - }() + }) serverErrCh := make(chan error, 1) - go func() { + appGo("http-server", func() { var err error if httpsEnabled() { @@ -1186,9 +1186,9 @@ func main() { } serverErrCh <- nil - }() + }) - go func() { + appGo("ai-server-start", func() { aiProc, err := startAIServer(appCtx) if err != nil { appLogln("⚠️ AI Server konnte nicht gestartet werden:", err) @@ -1205,23 +1205,25 @@ func main() { watchedTrainingRoot := aiServerRoot aiServerMu.Unlock() - go startTrainingBestModelWatcher(appCtx, watchedTrainingRoot) - }() + appGo("training-best-model-watcher", func() { + startTrainingBestModelWatcher(appCtx, watchedTrainingRoot) + }) + }) - go func() { + appGo("startup-notification", func() { _ = showAppNotification( "Recorder gestartet", buildStartupNotificationMessage(), ) - }() + }) - go func() { + appGo("http-server-error-listener", func() { err := <-serverErrCh if err != nil { appLogln("❌ HTTP-Server Fehler:", err) shutdown() } - }() + }) runTray(appCtx, shutdown, buildTrayStats) shutdown() diff --git a/backend/tls_dynamic.go b/backend/tls_dynamic.go new file mode 100644 index 0000000..2ece580 --- /dev/null +++ b/backend/tls_dynamic.go @@ -0,0 +1,237 @@ +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 +} diff --git a/cert/mkcert-v1.4.4-windows-amd64.exe b/cert/mkcert-v1.4.4-windows-amd64.exe deleted file mode 100644 index c9cde49..0000000 Binary files a/cert/mkcert-v1.4.4-windows-amd64.exe and /dev/null differ diff --git a/cert/recorder-cert.pem b/cert/recorder-cert.pem deleted file mode 100644 index 33c3442..0000000 --- a/cert/recorder-cert.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEbjCCAtagAwIBAgIRAKGvq9b44yCGC32q8PwkueowDQYJKoZIhvcNAQELBQAw -gYsxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEwMC4GA1UECwwnVEVH -RFNTRFxSb3RoZXJATDE0UEJCSzk1MTAwMDA2IChSb3RoZXIpMTcwNQYDVQQDDC5t -a2NlcnQgVEVHRFNTRFxSb3RoZXJATDE0UEJCSzk1MTAwMDA2IChSb3RoZXIpMB4X -DTI2MDUwODEwMDQzN1oXDTI4MDgwODEwMDQzN1owWzEnMCUGA1UEChMebWtjZXJ0 -IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTAwLgYDVQQLDCdURUdEU1NEXFJvdGhl -ckBMMTRQQkJLOTUxMDAwMDYgKFJvdGhlcikwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDDghqU3igC3sNP2zAJTZbvZcT1xxjUgk3oA2j0WwNPO+xhCH4K -mJ6enU5zrGWI4+T0IHfbo737rvgk6XfTDSvzB9Uags8egBUGxbr9eMBya0hmnYGs -zIQGJeI4TIeQ7sa7krCpMB/e1YxR1qYFf1C8oSt43dJCdJP4Ev//BaR7rrFt3D3N -CC2OgxVYSh/hR9XNIIEPMoioQIYhle41mQWjyPFt16Fjg9sWrrnHSz7M4cgVVSot -+aBlRjbxgOarLyST3XdtjAfC6CqxnzprX1LfznuULjatoS+pFDBDoy2B13xwa9Br -f+Xy3CswmSO+AzNomWfbeBkJnl6xb3Tb1urrAgMBAAGjfDB6MA4GA1UdDwEB/wQE -AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBR0Zph7hFKq4GpE -XO0y3U+T0iFoQDAyBgNVHREEKzApgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAA -AAAAAAAAAAGHBAoAARkwDQYJKoZIhvcNAQELBQADggGBAJShhgQS+9cwuVOJUIW+ -uy9t1ZAjR9qTd31kHdK1vu294GiavrG8RD+92BRujG4Fq1ambvqcu9sUaC8+0HAP -cQvdmnbTxG0ZHkGP7VVj3Poee0jpsVAVtV3aVwKxyO/E7Mv/nEKTqNfnECbEuiqC -tOJ8mI3E9tf2/ljzq2PqvfIPFBBtUT2lihNAFXM/06Nzoa1NPdfYd58N+VBFSOSq -zEM4fmpcqMThZGKGBoWmHwTE5Mw6mZFLyxHJpOFu5wNzyPJGes5o/F0ot5n+ZGah -GbYaGmhEedOxC3/WXRtexJmZ0jrB8Hkx+c/4h6+0HKU/PelrFJS+c2d6h8YGXyhv -pUsfyyoQzx7rSgyLfJqXK+52ZtnWNc0yVVMyKJwjvhv4mcP9l/q3UGR/i7xkD5Pc -2bLgM+wc8nKI7Yd/xejLpQDC0b/P1XibT1T3GRjiC/eu64G/uFOF+m3P5XvmF8JO -0cj8fXwUCq+TVD+22rFmtT+2VYkf8RUsC48Ou+PHccmDyw== ------END CERTIFICATE----- diff --git a/cert/recorder-key.pem b/cert/recorder-key.pem deleted file mode 100644 index 67692b1..0000000 --- a/cert/recorder-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDDghqU3igC3sNP -2zAJTZbvZcT1xxjUgk3oA2j0WwNPO+xhCH4KmJ6enU5zrGWI4+T0IHfbo737rvgk -6XfTDSvzB9Uags8egBUGxbr9eMBya0hmnYGszIQGJeI4TIeQ7sa7krCpMB/e1YxR -1qYFf1C8oSt43dJCdJP4Ev//BaR7rrFt3D3NCC2OgxVYSh/hR9XNIIEPMoioQIYh -le41mQWjyPFt16Fjg9sWrrnHSz7M4cgVVSot+aBlRjbxgOarLyST3XdtjAfC6Cqx -nzprX1LfznuULjatoS+pFDBDoy2B13xwa9Brf+Xy3CswmSO+AzNomWfbeBkJnl6x -b3Tb1urrAgMBAAECggEBAI31EBv72w2KdkKroot+vROCz6quMAdNvgezQif7VcHY -fuBN7EcBXltJWUeAbBEjeIESejUPBcmT2DXlF841CC5lB4VCaeV5lsreE9IsNYBf -CakIwLmZnltgcovydZT063QTJRcUDHAems5pjw76zMLKO+h9GEiMoUxFb3/atv3e -K5HE+uUd6JcPU0q91S6TxqvS9gH30OOkwH6Kl4tzWauQAKg/sVzYafbyTAuEpiMO -jLLLAn/sOmiL4knnRVu1FK4aZSH+t9BN9YuwfucXFn9jFgPtIJ0bF1GuqnwIeDI5 -BTNanHfaZRHD0+FbS0KbhARIFgDveoGrdoEtou0eDOECgYEA5dJqUvk96f7zCrxD -eSuUl1MvOjydY3GacfjevZoTxJDqh4Wt2wgLYJc4XB7tI02ci7NncecG8qPfqWq7 -aFjQAGAYdR+XvE7R2m3OFK9WbRX4cH6b+l7mSq0eOvcAAajn4RuNr/il+7Opw7zX -eKUB/pnW36B+ethdr5l+kDUQb8MCgYEA2ccanIT2QAxJb5j0F287NGhaTPuWfxcX -2T53I2fIwMiell0YXjwsKwKpckubRB2f5X4b9QJifUTmtxHky/Tx4H9OVhCaxumw -edol5YNzZaP/Tx8m2mqpyN2WPbezIFeA+GNcmDAxEo/NT2B64OVCHWKosndvb+Yk -GFmVb+g9zbkCgYEA1YFJLZRHJJ+pgour02HdRUgOU/gD72KWrNMbeuEtBCvs9cIG -5bjveOiDf3FrtKRhjpc4vuR12+zJ2EZDnIkFk5OypPyYpmRDKL1h+m15yRXkG/5D -QbHwF+gEcZsN8nzMDqDeXGCPMuqSCDnjoz0IQVMB//bGCbIANyZOIgJqJqkCgYEA -vHc+ZG383fjEJLvtoco1JmmYnD6uQ1Ys4WjZmd5bMdtswxvV1tekMaSgF7WurQgm -NGkqsKJbsaVLNOtbYdac7He/x2OfTr02aH2Nhk54M2H1tPd0nFjqjlaVitvLPRX9 -GviCTYKHNVUVjLgmHzLIQL382FXcLq6wVhJQ7QPDWKECgYA6ZmdB8waN2SFOJRnj -5zWTgdImicl4yCu8PaEyloO9whYSdYH8zU3K+FKaowXGNInp3BGCmP9TUwoG8Iwx -Ug5d55P2FgwnhCi59Y6dMXih1p94BuQ0vcWPBqztofl7lZbBEnjwzKVycFvcyNFX -QFb/UA4/CfQli9ciDtlqzZt8SA== ------END PRIVATE KEY-----