diff --git a/controller/README.md b/controller/README.md index 18e1d4b..b910d26 100644 --- a/controller/README.md +++ b/controller/README.md @@ -24,7 +24,7 @@ controller generates secrets, saves app.yaml, runs `docker compose up -d`, and t with Traefik routing and health checks. The dashboard correctly shows real-time container states including health substatus (starting → healthy → running). -Current version: **v0.6.0** +Current version: **v0.6.1** ### What works - Dashboard with live container state (green/orange/yellow/red) diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go index 2b4ebd2..d97faae 100644 --- a/controller/internal/api/router.go +++ b/controller/internal/api/router.go @@ -188,6 +188,7 @@ func (r *Router) getDeployFields(w http.ResponseWriter, _ *http.Request, name st } func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name string) { + limitBody(w, req) r.logger.Printf("[API] Deploy requested for stack: %s", name) var body struct { @@ -261,6 +262,7 @@ func (r *Router) actionStack(w http.ResponseWriter, action, name string) { } func (r *Router) updateOptionalConfig(w http.ResponseWriter, req *http.Request, name string) { + limitBody(w, req) r.logger.Printf("[API] Optional config update requested for stack: %s", name) var body struct { @@ -306,6 +308,7 @@ func (r *Router) getStackHDDData(w http.ResponseWriter, _ *http.Request, name st } func (r *Router) deleteStack(w http.ResponseWriter, req *http.Request, name string) { + limitBody(w, req) r.logger.Printf("[API] Delete requested for stack: %s", name) var body struct { @@ -585,3 +588,8 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) { log.Printf("[ERROR] Failed to write JSON response: %v", err) } } + +// limitBody wraps the request body with a size limit (default 1MB). +func limitBody(w http.ResponseWriter, req *http.Request) { + req.Body = http.MaxBytesReader(w, req.Body, 1<<20) // 1MB +} diff --git a/controller/internal/web/auth.go b/controller/internal/web/auth.go index c81c388..8f5bf7f 100644 --- a/controller/internal/web/auth.go +++ b/controller/internal/web/auth.go @@ -2,7 +2,6 @@ package web import ( "crypto/rand" - "crypto/subtle" "encoding/hex" "fmt" "net/http" @@ -13,7 +12,6 @@ import ( ) type session struct { - token string expiresAt time.Time } @@ -81,14 +79,15 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { } token := s.createSession() + isSecure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: token, Path: "/", MaxAge: int(sessionMaxAge.Seconds()), HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Secure: true, + SameSite: http.SameSiteLaxMode, + Secure: isSecure, }) s.logger.Printf("[INFO] Login from %s", r.RemoteAddr) @@ -111,7 +110,7 @@ func (s *Server) createSession() string { token := hex.EncodeToString(b) s.sessionsMu.Lock() - s.sessions[token] = &session{token: token, expiresAt: time.Now().Add(sessionMaxAge)} + s.sessions[token] = &session{expiresAt: time.Now().Add(sessionMaxAge)} s.sessionsMu.Unlock() return token @@ -120,27 +119,35 @@ func (s *Server) createSession() string { func (s *Server) isValidSession(token string) bool { s.sessionsMu.RLock() defer s.sessionsMu.RUnlock() - sess, ok := s.sessions[token] - if !ok || time.Now().After(sess.expiresAt) { - return false - } - return subtle.ConstantTimeCompare([]byte(sess.token), []byte(token)) == 1 + return ok && time.Now().Before(sess.expiresAt) } func (s *Server) cleanupSessions() { - for range time.Tick(15 * time.Minute) { - s.sessionsMu.Lock() - now := time.Now() - for t, sess := range s.sessions { - if now.After(sess.expiresAt) { - delete(s.sessions, t) + ticker := time.NewTicker(15 * time.Minute) + defer ticker.Stop() + for { + select { + case <-s.done: + return + case <-ticker.C: + s.sessionsMu.Lock() + now := time.Now() + for t, sess := range s.sessions { + if now.After(sess.expiresAt) { + delete(s.sessions, t) + } } + s.sessionsMu.Unlock() } - s.sessionsMu.Unlock() } } +// Close signals the server to stop background goroutines. +func (s *Server) Close() { + close(s.done) +} + func (s *Server) renderLogin(w http.ResponseWriter, errorMsg string) { data := map[string]interface{}{ "Title": "Bejelentkezés", diff --git a/controller/internal/web/funcmap.go b/controller/internal/web/funcmap.go index bba0047..8d55ff5 100644 --- a/controller/internal/web/funcmap.go +++ b/controller/internal/web/funcmap.go @@ -12,6 +12,11 @@ import ( // templateFuncMap returns the FuncMap used by all HTML templates. func (s *Server) templateFuncMap() template.FuncMap { + loc, err := time.LoadLocation("Europe/Budapest") + if err != nil { + loc = time.UTC + } + return template.FuncMap{ "stateColor": func(state stacks.ContainerState) string { switch state { @@ -164,10 +169,6 @@ func (s *Server) templateFuncMap() template.FuncMap { if t.IsZero() { return "–" } - loc, _ := time.LoadLocation("Europe/Budapest") - if loc == nil { - loc = time.UTC - } now := time.Now().In(loc) d := now.Sub(t.In(loc)) switch { @@ -187,20 +188,12 @@ func (s *Server) templateFuncMap() template.FuncMap { if t.IsZero() { return "–" } - loc, _ := time.LoadLocation("Europe/Budapest") - if loc == nil { - loc = time.UTC - } return t.In(loc).Format("2006-01-02 15:04") }, "fmtTimeShort": func(t time.Time) string { if t.IsZero() { return "–" } - loc, _ := time.LoadLocation("Europe/Budapest") - if loc == nil { - loc = time.UTC - } lt := t.In(loc) now := time.Now().In(loc) if lt.Year() == now.Year() && lt.YearDay() == now.YearDay() { @@ -222,10 +215,6 @@ func (s *Server) templateFuncMap() template.FuncMap { if t.IsZero() { return "–" } - loc, _ := time.LoadLocation("Europe/Budapest") - if loc == nil { - loc = time.UTC - } lt := t.In(loc) now := time.Now().In(loc) timeStr := lt.Format("15:04") @@ -250,10 +239,6 @@ func (s *Server) templateFuncMap() template.FuncMap { } }, "nextPruneLabel": func(schedule string) string { - loc, _ := time.LoadLocation("Europe/Budapest") - if loc == nil { - loc = time.UTC - } now := time.Now().In(loc) var next time.Time switch strings.ToLower(schedule) { diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index de9a9d5..61b9267 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -22,21 +22,7 @@ func (s *Server) baseData(page, title string) map[string]interface{} { func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) { stackList := s.stackMgr.GetStacks() - running, stopped := 0, 0 - for _, st := range stackList { - switch st.State { - case stacks.StateRunning: - running++ - case stacks.StateStopped, stacks.StateExited: - stopped++ - case stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting: - // Count starting/unhealthy/restarting as "running" for the dashboard stat - // (they have containers, they're just not fully healthy yet) - running++ - } - } - - // Filter to deployed + protected stacks only for dashboard display + // Filter to deployed + protected stacks first var deployedStacks []stacks.Stack for _, st := range stackList { if st.Deployed || st.Protected { @@ -44,6 +30,17 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) { } } + // Count from the DISPLAYED set only + running, stopped := 0, 0 + for _, st := range deployedStacks { + switch st.State { + case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting: + running++ + case stacks.StateStopped, stacks.StateExited: + stopped++ + } + } + sysInfo := system.GetInfo(s.cfg.Paths.HDDPath, s.cpuCollector) data := s.baseData("dashboard", "Vezérlőpult") @@ -99,10 +96,10 @@ func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string s.render(w, "logs", data) } -func (s *Server) deployHandler(w http.ResponseWriter, _ *http.Request, name string) { +func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name string) { meta, appCfg, err := s.stackMgr.GetDeployFields(name) if err != nil { - http.NotFound(w, nil) + http.NotFound(w, r) return } @@ -160,7 +157,7 @@ func (s *Server) deployHandler(w http.ResponseWriter, _ *http.Request, name stri s.render(w, "deploy", data) } -func (s *Server) appDetailHandler(w http.ResponseWriter, _ *http.Request, slug string) { +func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) { var found *stacks.Stack for _, stack := range s.stackMgr.GetStacks() { if stack.Meta.Slug == slug { @@ -169,7 +166,7 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, _ *http.Request, slug s } } if found == nil { - http.NotFound(w, nil) + http.NotFound(w, r) return } diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go index 702d622..cb4d353 100644 --- a/controller/internal/web/server.go +++ b/controller/internal/web/server.go @@ -29,6 +29,7 @@ type Server struct { sessions map[string]*session sessionsMu sync.RWMutex + done chan struct{} } func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *system.CPUCollector, backupMgr *backup.Manager, sched *scheduler.Scheduler, logger *log.Logger, version string) *Server { @@ -41,6 +42,7 @@ func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *syste logger: logger, version: version, sessions: make(map[string]*session), + done: make(chan struct{}), } s.loadTemplates() go s.cleanupSessions()