fix: code review — XSS, credentials 0600, interval leak, path bugs, title, unused imports
This commit is contained in:
parent
09592b90d2
commit
cda6ca4785
8 changed files with 63 additions and 25 deletions
13
api.go
13
api.go
|
|
@ -90,6 +90,8 @@ func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: safe})
|
||||
|
||||
case http.MethodPost:
|
||||
s.mu.Lock()
|
||||
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()})
|
||||
|
|
@ -135,7 +137,7 @@ func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
go2rtcURL := "http://127.0.0.1:1984/api/stream.mjpeg?src=" + camID + "_" + streamType
|
||||
|
||||
resp, err := http.Get(go2rtcURL)
|
||||
resp, err := httpClient.Get(go2rtcURL)
|
||||
if err != nil {
|
||||
http.Error(w, "stream unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
|
|
@ -363,7 +365,7 @@ func (s *Server) handleRecordings(w http.ResponseWriter, r *http.Request) {
|
|||
snapName := strings.Replace(name, "rec_", "snap_", 1)
|
||||
snapName = strings.Replace(snapName, ".mp4", ".jpg", 1)
|
||||
snapPath := camDir + "/" + snapName
|
||||
if _, err := os.Stat(recDir + "/" + snapName); err == nil {
|
||||
if _, err := os.Stat(scanDir + "/" + snapName); err == nil {
|
||||
clip.Snap = snapPath
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +383,12 @@ func (s *Server) handleRecordings(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
clips = append([]Clip{clip}, clips...) // newest first
|
||||
clips = append(clips, clip)
|
||||
}
|
||||
|
||||
// Reverse — newest first.
|
||||
for i, j := 0, len(clips)-1; i < j; i, j = i+1, j-1 {
|
||||
clips[i], clips[j] = clips[j], clips[i]
|
||||
}
|
||||
|
||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: clips})
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ func SaveConfig(cfg Config, path string) error {
|
|||
header := []byte("# NextNVR Configuration\n# Generated by NextNVR setup\n\n")
|
||||
out := append(header, data...)
|
||||
|
||||
if err := os.WriteFile(path, out, 0644); err != nil {
|
||||
if err := os.WriteFile(path, out, 0600); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ func (g *Go2RTCManager) Stop() {
|
|||
if g.cmd != nil && g.cmd.Process != nil {
|
||||
log.Println("go2rtc: stopping...")
|
||||
g.cmd.Process.Signal(os.Interrupt)
|
||||
g.mu.Unlock()
|
||||
time.Sleep(2 * time.Second)
|
||||
g.mu.Lock()
|
||||
g.cmd.Process.Kill()
|
||||
g.running = false
|
||||
log.Println("go2rtc: stopped")
|
||||
|
|
@ -139,5 +141,5 @@ func (g *Go2RTCManager) writeConfig(cameras []CameraConfig) error {
|
|||
sb.WriteString(fmt.Sprintf(" %s_main: %s\n", cam.ID, mainURL))
|
||||
}
|
||||
|
||||
return os.WriteFile("go2rtc.yaml", []byte(sb.String()), 0644)
|
||||
return os.WriteFile("go2rtc.yaml", []byte(sb.String()), 0600)
|
||||
}
|
||||
|
|
|
|||
4
onvif.go
4
onvif.go
|
|
@ -10,7 +10,6 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
|
@ -238,6 +237,3 @@ func buildRTSPURLs(ip, user, pass, manufacturer string) (string, string) {
|
|||
|
||||
return replacer.Replace(patterns.main), replacer.Replace(patterns.sub)
|
||||
}
|
||||
|
||||
// suppress unused import warning for encoding/xml.
|
||||
var _ = xml.Unmarshal
|
||||
|
|
|
|||
|
|
@ -61,8 +61,12 @@ async function loadCameras() {
|
|||
}
|
||||
|
||||
// ── Live Grid ──
|
||||
let gridRefreshTimer = null;
|
||||
|
||||
function renderLiveGrid() {
|
||||
const grid = document.getElementById('grid');
|
||||
// Clear previous refresh timer.
|
||||
if (gridRefreshTimer) { clearInterval(gridRefreshTimer); gridRefreshTimer = null; }
|
||||
grid.innerHTML = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const cam = cameras[i];
|
||||
|
|
@ -80,16 +84,23 @@ function renderLiveGrid() {
|
|||
<span class="tile-status online"></span>
|
||||
<span class="tile-label">${cam.name}</span>
|
||||
`;
|
||||
setInterval(() => {
|
||||
const img = tile.querySelector('img');
|
||||
if (img) img.src = `/recordings/${cam.name || cam.id}/latest.jpg?t=${Date.now()}`;
|
||||
}, 3000);
|
||||
tile.addEventListener('click', () => openFocus(cam));
|
||||
} else {
|
||||
tile.innerHTML = '<span class="tile-placeholder">No Camera</span>';
|
||||
}
|
||||
grid.appendChild(tile);
|
||||
}
|
||||
|
||||
// Single global refresh timer — avoids interval leak.
|
||||
gridRefreshTimer = setInterval(() => {
|
||||
grid.querySelectorAll('.grid-tile:not(.offline) img.grid-snap').forEach(img => {
|
||||
const camId = img.closest('.grid-tile').dataset.camId;
|
||||
if (camId) {
|
||||
const cam = cameras.find(c => c.id === camId);
|
||||
if (cam) img.src = `/recordings/${cam.name || cam.id}/latest.jpg?t=${Date.now()}`;
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ── Focus Overlay ──
|
||||
|
|
@ -153,16 +164,36 @@ async function loadClips() {
|
|||
}
|
||||
function renderClips(clips) {
|
||||
const grid = document.getElementById('pb-clips');
|
||||
if (clips.length === 0) { grid.innerHTML = '<p style="color:var(--text-muted);grid-column:1/-1">No recordings found for this selection.</p>'; return; }
|
||||
grid.innerHTML = clips.map(c => `
|
||||
<div class="clip-card" onclick="playClip('${c.path}')">
|
||||
<img src="${c.snap || ''}" class="clip-thumb" loading="lazy"
|
||||
onerror="this.style.display='none'" alt="${c.time}">
|
||||
<div>${c.live ? '🔴 ' : '🎬 '}${c.time}</div>
|
||||
<div class="clip-time">${c.name}</div>
|
||||
<div class="clip-size">${formatSize(c.size)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
grid.innerHTML = '';
|
||||
if (clips.length === 0) {
|
||||
grid.innerHTML = '<p style="color:var(--text-muted);grid-column:1/-1">No recordings found for this selection.</p>';
|
||||
return;
|
||||
}
|
||||
clips.forEach(c => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'clip-card';
|
||||
card.addEventListener('click', () => playClip(c.path));
|
||||
if (c.snap) {
|
||||
const thumb = document.createElement('img');
|
||||
thumb.className = 'clip-thumb';
|
||||
thumb.src = c.snap;
|
||||
thumb.loading = 'lazy';
|
||||
thumb.onerror = () => { thumb.style.display = 'none'; };
|
||||
card.appendChild(thumb);
|
||||
}
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.textContent = (c.live ? '🔴 ' : '🎬 ') + c.time;
|
||||
card.appendChild(timeDiv);
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.className = 'clip-time';
|
||||
nameDiv.textContent = c.name;
|
||||
card.appendChild(nameDiv);
|
||||
const sizeDiv = document.createElement('div');
|
||||
sizeDiv.className = 'clip-size';
|
||||
sizeDiv.textContent = formatSize(c.size);
|
||||
card.appendChild(sizeDiv);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
function playClip(path) {
|
||||
document.getElementById('pb-player').classList.remove('hidden');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NextNVR — Casa Alba Mindelo</title>
|
||||
<title>NextNVR</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ func (rm *RecorderManager) startRecorder(cam CameraConfig) {
|
|||
cam.Username, cam.Password, cam.IP)
|
||||
}
|
||||
|
||||
log.Printf("recorder: starting %s → %s", cam.ID, rtspURL)
|
||||
log.Printf("recorder: starting %s", cam.ID)
|
||||
|
||||
rm.mu.Lock()
|
||||
proc := &recorderProcess{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed public/*
|
||||
|
|
@ -22,6 +23,7 @@ type Server struct {
|
|||
http *http.Server
|
||||
mux *http.ServeMux
|
||||
appConfig *Config // mutable config reference for hot-reload
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewServer creates and configures the HTTP server.
|
||||
|
|
|
|||
Loading…
Reference in a new issue