fix: auth bypass removed, sanitize names, json errors, viewer port toggle
This commit is contained in:
parent
fbb40f4e26
commit
2032402597
4 changed files with 34 additions and 20 deletions
25
api.go
25
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
|
||||
}
|
||||
|
||||
|
|
|
|||
7
auth.go
7
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
|
||||
}
|
||||
|
|
|
|||
6
main.go
6
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 {
|
||||
|
|
|
|||
|
|
@ -262,23 +262,25 @@ async function renderCameraCards() {
|
|||
}
|
||||
container.innerHTML = `
|
||||
<div class="camera-card" id="auth-card" style="grid-column:1/-1">
|
||||
<h3>🔐 Authentication</h3>
|
||||
<h3>🔐 Access Setup</h3>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-top:8px">
|
||||
<div style="flex:1;min-width:200px">
|
||||
<h4 style="font-size:13px;color:var(--accent);margin-bottom:6px">Master (full access)</h4>
|
||||
<h4 style="font-size:13px;color:var(--accent);margin-bottom:6px">Port 8080 — Admin</h4>
|
||||
<label>Username <input type="text" value="${escAttr(config?.auth?.master?.username || '')}" data-auth="master-user"></label>
|
||||
<label>Password <input type="password" placeholder="new password" data-auth="master-pass"></label>
|
||||
</div>
|
||||
<div style="flex:1;min-width:200px">
|
||||
<h4 style="font-size:13px;color:var(--accent);margin-bottom:6px">Viewer (live wall only, port 8090)</h4>
|
||||
<label>Username <input type="text" value="${escAttr(config?.auth?.viewer?.username || '')}" data-auth="viewer-user"></label>
|
||||
<label>Password <input type="password" placeholder="new password" data-auth="viewer-pass"></label>
|
||||
<div class="row"><label>Enable viewer <input type="checkbox" ${config?.auth?.viewer?.enabled ? 'checked' : ''} data-auth="viewer-enabled" style="width:auto"></label></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<label>Require login on port 8080 <input type="checkbox" ${config?.auth?.enabled ? 'checked' : ''} data-auth="auth-enabled" style="width:auto"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:200px">
|
||||
<h4 style="font-size:13px;color:var(--accent);margin-bottom:6px">Port 8090 — Local View</h4>
|
||||
<label>Username <input type="text" value="${escAttr(config?.auth?.viewer?.username || '')}" data-auth="viewer-user"></label>
|
||||
<label>Password <input type="password" placeholder="new password" data-auth="viewer-pass"></label>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<label>Enable local view access <input type="checkbox" ${config?.auth?.viewer?.enabled ? 'checked' : ''} data-auth="viewer-enabled" style="width:auto"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-column:1/-1;display:flex;gap:8px;margin-bottom:4px">
|
||||
<button class="btn-primary" onclick="addCamera()">+ Add Camera</button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue