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:
|
case http.MethodPut:
|
||||||
var updated CameraConfig
|
var updated CameraConfig
|
||||||
if err := json.NewDecoder(r.Body).Decode(&updated); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
updated.ID = camID
|
updated.ID = camID
|
||||||
|
|
@ -97,7 +97,7 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
var newCfg Config
|
var newCfg Config
|
||||||
if err := json.NewDecoder(r.Body).Decode(&newCfg); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,8 +108,16 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
newCfg.Cameras[i].Password = old.Password
|
newCfg.Cameras[i].Password = old.Password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sanitize name: replace spaces with underscores for CLI-friendly paths.
|
// Sanitize name: replace unsafe chars with underscores.
|
||||||
newCfg.Cameras[i].Name = strings.ReplaceAll(newCfg.Cameras[i].Name, " ", "_")
|
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).
|
// 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)
|
// GET /stream/{cam_id}?type=sub (default: sub stream via go2rtc)
|
||||||
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||||
camID := strings.TrimPrefix(r.URL.Path, "/stream/")
|
camID := strings.TrimPrefix(r.URL.Path, "/stream/")
|
||||||
if camID == "" {
|
if camID == "" || camID == ".." || strings.ContainsAny(camID, "/\\?&#") {
|
||||||
http.Error(w, "camera ID required", http.StatusBadRequest)
|
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
|
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.
|
// authRequired redirects to /login if no valid session.
|
||||||
func (s *Server) authRequired(next http.Handler) http.Handler {
|
func (s *Server) authRequired(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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
|
path := r.URL.Path
|
||||||
if path == "/login" || path == "/login.html" ||
|
if path == "/login" || path == "/login.html" ||
|
||||||
strings.HasPrefix(path, "/api/login") ||
|
strings.HasPrefix(path, "/api/login") ||
|
||||||
strings.HasPrefix(path, "/api/logout") ||
|
strings.HasPrefix(path, "/api/logout") ||
|
||||||
strings.HasPrefix(path, "/api/session") ||
|
strings.HasPrefix(path, "/api/session") {
|
||||||
strings.HasPrefix(path, "/recordings/") ||
|
|
||||||
strings.HasPrefix(path, "/go2rtc/") ||
|
|
||||||
strings.HasPrefix(path, "/stream/") {
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -101,13 +101,15 @@ func main() {
|
||||||
errCh <- app.server.ListenAndServe()
|
errCh <- app.server.ListenAndServe()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Start viewer server (no auth, live wall only) — after main server is created.
|
// Start viewer server only if enabled.
|
||||||
if cfg.Server.ViewerPort != "" {
|
if cfg.Auth.Viewer.Enabled {
|
||||||
go func() {
|
go func() {
|
||||||
if err := app.server.StartViewerServer(); err != nil {
|
if err := app.server.StartViewerServer(); err != nil {
|
||||||
log.Printf("Viewer server: %v", err)
|
log.Printf("Viewer server: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
} else {
|
||||||
|
log.Println("Viewer server: disabled in config")
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
|
||||||
|
|
@ -262,23 +262,25 @@ async function renderCameraCards() {
|
||||||
}
|
}
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="camera-card" id="auth-card" style="grid-column:1/-1">
|
<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="display:flex;gap:16px;flex-wrap:wrap;margin-top:8px">
|
||||||
<div style="flex:1;min-width:200px">
|
<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>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>
|
<label>Password <input type="password" placeholder="new password" data-auth="master-pass"></label>
|
||||||
|
<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>
|
||||||
<div style="flex:1;min-width:200px">
|
<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>
|
<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>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>
|
<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 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>
|
||||||
<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>
|
||||||
<div style="grid-column:1/-1;display:flex;gap:8px;margin-bottom:4px">
|
<div style="grid-column:1/-1;display:flex;gap:8px;margin-bottom:4px">
|
||||||
<button class="btn-primary" onclick="addCamera()">+ Add Camera</button>
|
<button class="btn-primary" onclick="addCamera()">+ Add Camera</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue