package main import ( "context" "crypto/tls" "encoding/json" "html/template" "log" "net" "net/http" "os" "os/signal" "path/filepath" "sort" "strings" "syscall" "time" ) type App struct { Title string `json:"title"` URL string `json:"url"` Icon string `json:"icon"` Category string `json:"category"` Color string `json:"color"` } type PageData struct { Title string Apps []App Categories []string Now time.Time } func getenv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func must(err error) { if err != nil { log.Fatal(err) } } func loadApps(path string) ([]App, []string, error) { b, err := os.ReadFile(path) if err != nil { return nil, nil, err } var apps []App if err := json.Unmarshal(b, &apps); err != nil { return nil, nil, err } sort.Slice(apps, func(i, j int) bool { return strings.ToLower(apps[i].Title) < strings.ToLower(apps[j].Title) }) seen := map[string]bool{} var cats []string for _, a := range apps { if a.Category == "" { continue } if !seen[a.Category] { seen[a.Category] = true cats = append(cats, a.Category) } } sort.Strings(cats) return apps, cats, nil } func withSecurityHeaders(next http.Handler, hsts bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") w.Header().Set("X-XSS-Protection", "0") w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data: https:; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; script-src 'self';") if hsts && r.TLS != nil { w.Header().Set("Strict-Transport-Security", "max-age=15552000; includeSubDomains") } next.ServeHTTP(w, r) }) } func redirectToHTTPS(httpsAddr string) http.Handler { hostPort := func(host string) string { h, _, err := net.SplitHostPort(host) if err != nil { return host } return h } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host := r.Host httpsPort := ":8443" if httpsAddr != "" && httpsAddr != ":8443" { httpsPort = httpsAddr } targetHost := hostPort(host) if _, _, err := net.SplitHostPort(host); err == nil { targetHost = hostPort(host) + httpsPort } else { if httpsPort != ":8443" { targetHost = host + httpsPort } } target := "https://" + targetHost + r.URL.RequestURI() http.Redirect(w, r, target, http.StatusMovedPermanently) }) } func main() { // App-Config appTitle := getenv("APP_TITLE", "Mein App-Store") jsonPath := getenv("APPS_JSON", "/data/apps.json") staticDir := getenv("STATIC_DIR", "static") tmplDir := getenv("TEMPLATE_DIR", "templates") // Server-Modus serverMode := strings.ToLower(getenv("SERVER_MODE", "http")) // "http" | "https" addr := getenv("ADDR", func() string { if serverMode == "https" { return ":8443" } return ":8080" }()) // HTTPS-spezifisch certFile := getenv("TLS_CERT_FILE", "") keyFile := getenv("TLS_KEY_FILE", "") hstsEnabled := getenv("HSTS", "true") == "true" // Optionaler Redirect (nur wenn SERVER_MODE=https) redirectAddr := getenv("HTTP_REDIRECT_ADDR", ":8080") redirectEnabled := getenv("HTTP_REDIRECT_ENABLED", "true") == "true" // Templates tmpl := template.Must(template.New("index.html").Funcs(template.FuncMap{ "safeURL": func(u string) template.URL { return template.URL(u) }, "hasPrefix": strings.HasPrefix, }).ParseFiles(filepath.Join(tmplDir, "index.html"))) readData := func() (PageData, error) { apps, cats, err := loadApps(jsonPath) if err != nil { return PageData{}, err } return PageData{ Title: appTitle, Apps: apps, Categories: cats, Now: time.Now(), }, nil } // Handler mux := http.NewServeMux() mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { data, err := readData() if err != nil { http.Error(w, "Fehler beim Laden der Apps: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil { log.Println("template error:", err) } }) // Gemeinsame Server-Instanzen var mainSrv *http.Server var redirSrv *http.Server // Graceful Shutdown Verkabelung stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) switch serverMode { case "https": // HTTPS mit Security-Headern (inkl. HSTS) secureHandler := withSecurityHeaders(mux, hstsEnabled) tlsCfg := &tls.Config{ MinVersion: tls.VersionTLS12, PreferServerCipherSuites: true, CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384}, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, }, } if certFile == "" || keyFile == "" { log.Fatal("SERVER_MODE=https, aber TLS_CERT_FILE oder TLS_KEY_FILE fehlt.") } mainSrv = &http.Server{ Addr: addr, Handler: secureHandler, TLSConfig: tlsCfg, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } // Optionaler HTTP→HTTPS Redirect if redirectEnabled && redirectAddr != "" { redirSrv = &http.Server{ Addr: redirectAddr, Handler: redirectToHTTPS(addr), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 10 * time.Second, } go func() { log.Printf("HTTP Redirect auf %s → HTTPS %s …", redirectAddr, addr) if err := redirSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("Redirect-Server Fehler: %v", err) } }() } go func() { log.Printf("HTTPS läuft auf %s …", addr) if err := mainSrv.ListenAndServeTLS(certFile, keyFile); err != nil && err != http.ErrServerClosed { log.Fatalf("HTTPS-Server Fehler: %v", err) } }() case "http": // HTTP ohne HSTS (Security-Header bleiben) handler := withSecurityHeaders(mux, false) mainSrv = &http.Server{ Addr: addr, Handler: handler, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Printf("HTTP läuft auf %s …", addr) if err := mainSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("HTTP-Server Fehler: %v", err) } }() default: log.Fatalf("Ungültiger SERVER_MODE: %q (erwartet: http oder https)", serverMode) } // Warten auf Stop-Signal <-stop log.Println("Beende …") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if redirSrv != nil { _ = redirSrv.Shutdown(ctx) } if mainSrv != nil { _ = mainSrv.Shutdown(ctx) } log.Println("Sauber beendet.") }