This commit is contained in:
Linrador 2026-06-26 16:31:08 +02:00
parent af60ee5319
commit ab9f1f79b5
15 changed files with 447 additions and 192 deletions

View File

@ -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)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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-----

View File

@ -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-----

View File

@ -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()

237
backend/tls_dynamic.go Normal file
View File

@ -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
}

Binary file not shown.

View File

@ -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-----

View File

@ -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-----