v0.6.1: Code review bugfixes — 7 correctness/safety/quality fixes
- Fix http.NotFound(w, nil) → pass actual request in handlers
- Fix dashboard running/stopped counts to match displayed stacks
- Fix Secure cookie blocking HTTP login (dynamic based on request)
- Remove misleading subtle.ConstantTimeCompare in session check
- Fix cleanupSessions goroutine leak (proper ticker + done channel)
- Add http.MaxBytesReader (1MB) to API POST endpoints
- Cache time.LoadLocation("Europe/Budapest") in template funcmap
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user