diff --git a/api.go b/api.go index dff06ba..dd530e7 100644 --- a/api.go +++ b/api.go @@ -65,7 +65,7 @@ func (s *Server) handleCameraByID(w http.ResponseWriter, r *http.Request) { case http.MethodPut: var updated CameraConfig if err := json.NewDecoder(r.Body).Decode(&updated); err != nil { - jsonResponse(w, http.StatusBadRequest, APIResponse{Error: "invalid JSON: " + err.Error()}) + jsonResponse(w, http.StatusBadRequest, APIResponse{Error: "invalid JSON"}) return } updated.ID = camID @@ -97,7 +97,7 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { defer s.mu.Unlock() var newCfg Config if err := json.NewDecoder(r.Body).Decode(&newCfg); err != nil { - jsonResponse(w, http.StatusBadRequest, APIResponse{Error: "invalid JSON: " + err.Error()}) + jsonResponse(w, http.StatusBadRequest, APIResponse{Error: "invalid JSON"}) return } @@ -108,8 +108,16 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { newCfg.Cameras[i].Password = old.Password } } - // Sanitize name: replace spaces with underscores for CLI-friendly paths. - newCfg.Cameras[i].Name = strings.ReplaceAll(newCfg.Cameras[i].Name, " ", "_") + // Sanitize name: replace unsafe chars with underscores. + n := newCfg.Cameras[i].Name + n = strings.Map(func(r rune) rune { + if r == '/' || r == '\\' || r == 0 || r == '.' || r == ':' { + return '_' + } + return r + }, n) + n = strings.ReplaceAll(n, " ", "_") + newCfg.Cameras[i].Name = n } // Hash new master password if provided (not masked, not empty, not already hashed). @@ -155,8 +163,13 @@ func (s *Server) handleConfigReload(w http.ResponseWriter, r *http.Request) { // GET /stream/{cam_id}?type=sub (default: sub stream via go2rtc) func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) { camID := strings.TrimPrefix(r.URL.Path, "/stream/") - if camID == "" { - http.Error(w, "camera ID required", http.StatusBadRequest) + if camID == "" || camID == ".." || strings.ContainsAny(camID, "/\\?&#") { + http.Error(w, "invalid camera ID", http.StatusBadRequest) + return + } + // Validate it's a known camera. + if _, idx := s.findCamera(camID); idx < 0 { + http.Error(w, "camera not found", http.StatusNotFound) return } diff --git a/auth.go b/auth.go index c953e1a..4013159 100644 --- a/auth.go +++ b/auth.go @@ -181,15 +181,12 @@ func (s *Server) handleSession(w http.ResponseWriter, r *http.Request) { // authRequired redirects to /login if no valid session. func (s *Server) authRequired(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Allow login page and API, static assets, and recordings. + // Allow login page and API, static assets. Everything else requires auth. path := r.URL.Path if path == "/login" || path == "/login.html" || strings.HasPrefix(path, "/api/login") || strings.HasPrefix(path, "/api/logout") || - strings.HasPrefix(path, "/api/session") || - strings.HasPrefix(path, "/recordings/") || - strings.HasPrefix(path, "/go2rtc/") || - strings.HasPrefix(path, "/stream/") { + strings.HasPrefix(path, "/api/session") { next.ServeHTTP(w, r) return } diff --git a/main.go b/main.go index 5214832..d438a95 100644 --- a/main.go +++ b/main.go @@ -101,13 +101,15 @@ func main() { errCh <- app.server.ListenAndServe() }() - // Start viewer server (no auth, live wall only) — after main server is created. - if cfg.Server.ViewerPort != "" { + // Start viewer server only if enabled. + if cfg.Auth.Viewer.Enabled { go func() { if err := app.server.StartViewerServer(); err != nil { log.Printf("Viewer server: %v", err) } }() + } else { + log.Println("Viewer server: disabled in config") } select { diff --git a/public/app.js b/public/app.js index c24b904..d7b44a2 100644 --- a/public/app.js +++ b/public/app.js @@ -262,23 +262,25 @@ async function renderCameraCards() { } container.innerHTML = `
-

🔐 Authentication

+

🔐 Access Setup

-

Master (full access)

+

Port 8080 — Admin

+
+ +
-

Viewer (live wall only, port 8090)

+

Port 8090 — Local View

-
+
+ +
-
- -