diff --git a/Dockerfile b/Dockerfile index f46387b..72b0dbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,22 @@ -# ---------- Build Stage ---------- +# --- Build --- FROM golang:1.24-alpine AS build WORKDIR /src -# System‑Deps nur für Build RUN apk add --no-cache git ca-certificates tzdata && update-ca-certificates - -# Module separat cachen COPY go.mod go.sum ./ RUN go mod download - -# Quellcode COPY . . - -# statisch bauen (kein CGO), mit kleinen Binaries ENV CGO_ENABLED=0 RUN --mount=type=cache,target=/root/.cache/go-build \ go build -trimpath -ldflags="-s -w" -o /out/dashboard ./cmd/dashboard -# ---------- Runtime Stage ---------- -# Distroless ist sehr klein/sicher; enthält CA‑Zertifikate für HTTPS‑Calls +# --- Runtime --- FROM gcr.io/distroless/base-debian12:nonroot WORKDIR /app - -# Expose Port EXPOSE 8080 - -# Copy Binary + benötigte Zeitzonen/Certs sind in Distroless bereits enthalten +# Binärdatei COPY --from=build /out/dashboard /app/dashboard - -# Security: läuft als nonroot User (Distroless nonroot UID 65532) +# Statische Dateien: +COPY web /app/web +ENV WEB_ROOT=/app/web USER nonroot:nonroot - -# Healthcheck via Startkommando ist nicht möglich in Distroless – per Compose lösen -ENTRYPOINT ["/app/dashboard"] \ No newline at end of file +ENTRYPOINT ["/app/dashboard"] diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 690d7eb..0ada20f 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -10,6 +10,7 @@ import ( "net/http/httputil" "net/url" "os" + "path/filepath" "strings" "time" @@ -18,7 +19,6 @@ import ( "github.com/goccy/go-json" "git.send.nrw/sendnrw/nginx-stream-server/internal/mtx" - "git.send.nrw/sendnrw/nginx-stream-server/internal/ui" ) var ( @@ -28,6 +28,11 @@ var ( streamsCSV = os.Getenv("STREAMS") basicUser = os.Getenv("BASIC_AUTH_USER") basicPass = os.Getenv("BASIC_AUTH_PASS") + + // Root für Webassets (per ENV überschreibbar) + webRoot = env("WEB_ROOT", "web") + // Templates werden beim Start geladen + tpls *template.Template ) func init() { @@ -45,10 +50,21 @@ func env(k, def string) string { return def } +func mustLoadTemplates() *template.Template { + pattern := filepath.Join(webRoot, "templates", "*.html") + t, err := template.ParseGlob(pattern) + if err != nil { + log.Fatalf("templates laden fehlgeschlagen (%s): %v", pattern, err) + } + return t +} + func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) + tpls = mustLoadTemplates() r := chi.NewRouter() + // Strenge CSP (alles lokal) r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Security-Policy", @@ -59,7 +75,6 @@ func main() { "font-src 'self'", "script-src 'self'", "connect-src 'self'", - // optional: "media-src 'self'", }, "; "), ) w.Header().Set("X-Content-Type-Options", "nosniff") @@ -69,11 +84,13 @@ func main() { }) }) + // API r.Group(func(r chi.Router) { r.Use(httprate.LimitByIP(30, time.Minute)) r.Get("/api/streams", apiStreams) }) + // Optional Basic Auth für Seiten if basicUser != "" { creds := basicUser + ":" + basicPass r.Group(func(p chi.Router) { @@ -96,18 +113,20 @@ func main() { r.Get("/{name}", pageStream) } + // /static → echtes Dateisystem + staticDir := http.Dir(filepath.Join(webRoot, "static")) + r.Handle("/static/*", + http.StripPrefix("/static", + http.FileServer(staticDir), + ), + ) + + // HLS-Reverse-Proxy up, _ := url.Parse(mtxHLS) proxy := httputil.NewSingleHostReverseProxy(up) proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e error) { http.Error(w, "upstream error", http.StatusBadGateway) } - - r.Handle("/static/*", - http.StripPrefix("/static", - http.FileServer(http.FS(ui.StaticFS)), - ), - ) - r.Handle("/hls/*", http.StripPrefix("/hls", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "..") { http.NotFound(w, r) @@ -116,9 +135,10 @@ func main() { proxy.ServeHTTP(w, r) }))) + // Health r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) - log.Printf("Dashboard listening on %s (API=%s HLS=%s)\n", listen, mtxAPI, mtxHLS) + log.Printf("Dashboard listening on %s (API=%s HLS=%s, WEB_ROOT=%s)\n", listen, mtxAPI, mtxHLS, webRoot) if err := http.ListenAndServe(listen, r); err != nil { log.Fatal(err) } @@ -158,22 +178,24 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { } func pageIndex(w http.ResponseWriter, r *http.Request) { - b, _ := ui.FS.ReadFile("index.html") w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write(b) + if err := tpls.ExecuteTemplate(w, "index.html", nil); err != nil { + http.Error(w, err.Error(), 500) + } } func pageStream(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") - tpl, err := template.ParseFS(ui.FS, "stream.html") - if err != nil { - http.Error(w, err.Error(), 500) - return - } w.Header().Set("Content-Type", "text/html; charset=utf-8") - tpl.Execute(w, map[string]any{"Name": name, "JSONName": fmt.Sprintf("%q", name)}) + if err := tpls.ExecuteTemplate(w, "stream.html", map[string]any{ + "Name": name, + "JSONName": fmt.Sprintf("%q", name), + }); err != nil { + http.Error(w, err.Error(), 500) + } } +// Basic helper bleibt, falls du Basic Auth nutzt func basic(creds string) string { const tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" b := []byte(creds) diff --git a/internal/ui/templates.go b/internal/ui/templates.go deleted file mode 100644 index 4193c62..0000000 --- a/internal/ui/templates.go +++ /dev/null @@ -1,10 +0,0 @@ -package ui - -import "embed" - -//go:embed *.html -var FS embed.FS - -// Embed nur echte Dateien; keine leeren Ordner! -//go:embed static/**/*.js static/**/*.css static/**/*.woff2 -var StaticFS embed.FS diff --git a/internal/ui/static/css/style.css b/web/static/css/style.css similarity index 100% rename from internal/ui/static/css/style.css rename to web/static/css/style.css diff --git a/internal/ui/static/fonts/inter-extrabold.woff2 b/web/static/fonts/inter-extrabold.woff2 similarity index 100% rename from internal/ui/static/fonts/inter-extrabold.woff2 rename to web/static/fonts/inter-extrabold.woff2 diff --git a/internal/ui/static/fonts/inter-regular.woff2 b/web/static/fonts/inter-regular.woff2 similarity index 100% rename from internal/ui/static/fonts/inter-regular.woff2 rename to web/static/fonts/inter-regular.woff2 diff --git a/internal/ui/static/fonts/inter-semibold.woff2 b/web/static/fonts/inter-semibold.woff2 similarity index 100% rename from internal/ui/static/fonts/inter-semibold.woff2 rename to web/static/fonts/inter-semibold.woff2 diff --git a/internal/ui/static/fonts/inter.css b/web/static/fonts/inter.css similarity index 100% rename from internal/ui/static/fonts/inter.css rename to web/static/fonts/inter.css diff --git a/internal/ui/static/js/hls.min.js b/web/static/js/hls.min.js similarity index 100% rename from internal/ui/static/js/hls.min.js rename to web/static/js/hls.min.js diff --git a/internal/ui/static/js/index.js b/web/static/js/index.js similarity index 100% rename from internal/ui/static/js/index.js rename to web/static/js/index.js diff --git a/internal/ui/static/js/stream.js b/web/static/js/stream.js similarity index 100% rename from internal/ui/static/js/stream.js rename to web/static/js/stream.js diff --git a/internal/ui/index.html b/web/templates/index.html similarity index 100% rename from internal/ui/index.html rename to web/templates/index.html diff --git a/internal/ui/stream.html b/web/templates/stream.html similarity index 100% rename from internal/ui/stream.html rename to web/templates/stream.html