This commit is contained in:
26
Dockerfile
26
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"]
|
||||
ENTRYPOINT ["/app/dashboard"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user