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})
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: safe})
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
|
s.mu.Lock()
|
||||||
|
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: " + 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
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "stream unavailable", http.StatusServiceUnavailable)
|
http.Error(w, "stream unavailable", http.StatusServiceUnavailable)
|
||||||
return
|
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(name, "rec_", "snap_", 1)
|
||||||
snapName = strings.Replace(snapName, ".mp4", ".jpg", 1)
|
snapName = strings.Replace(snapName, ".mp4", ".jpg", 1)
|
||||||
snapPath := camDir + "/" + snapName
|
snapPath := camDir + "/" + snapName
|
||||||
if _, err := os.Stat(recDir + "/" + snapName); err == nil {
|
if _, err := os.Stat(scanDir + "/" + snapName); err == nil {
|
||||||
clip.Snap = snapPath
|
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})
|
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")
|
header := []byte("# NextNVR Configuration\n# Generated by NextNVR setup\n\n")
|
||||||
out := append(header, data...)
|
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 fmt.Errorf("writing config: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@ func (g *Go2RTCManager) Stop() {
|
||||||
if g.cmd != nil && g.cmd.Process != nil {
|
if g.cmd != nil && g.cmd.Process != nil {
|
||||||
log.Println("go2rtc: stopping...")
|
log.Println("go2rtc: stopping...")
|
||||||
g.cmd.Process.Signal(os.Interrupt)
|
g.cmd.Process.Signal(os.Interrupt)
|
||||||
|
g.mu.Unlock()
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
|
g.mu.Lock()
|
||||||
g.cmd.Process.Kill()
|
g.cmd.Process.Kill()
|
||||||
g.running = false
|
g.running = false
|
||||||
log.Println("go2rtc: stopped")
|
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))
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
@ -238,6 +237,3 @@ func buildRTSPURLs(ip, user, pass, manufacturer string) (string, string) {
|
||||||
|
|
||||||
return replacer.Replace(patterns.main), replacer.Replace(patterns.sub)
|
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 ──
|
// ── Live Grid ──
|
||||||
|
let gridRefreshTimer = null;
|
||||||
|
|
||||||
function renderLiveGrid() {
|
function renderLiveGrid() {
|
||||||
const grid = document.getElementById('grid');
|
const grid = document.getElementById('grid');
|
||||||
|
// Clear previous refresh timer.
|
||||||
|
if (gridRefreshTimer) { clearInterval(gridRefreshTimer); gridRefreshTimer = null; }
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
const cam = cameras[i];
|
const cam = cameras[i];
|
||||||
|
|
@ -80,16 +84,23 @@ function renderLiveGrid() {
|
||||||
<span class="tile-status online"></span>
|
<span class="tile-status online"></span>
|
||||||
<span class="tile-label">${cam.name}</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));
|
tile.addEventListener('click', () => openFocus(cam));
|
||||||
} else {
|
} else {
|
||||||
tile.innerHTML = '<span class="tile-placeholder">No Camera</span>';
|
tile.innerHTML = '<span class="tile-placeholder">No Camera</span>';
|
||||||
}
|
}
|
||||||
grid.appendChild(tile);
|
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 ──
|
// ── Focus Overlay ──
|
||||||
|
|
@ -153,16 +164,36 @@ async function loadClips() {
|
||||||
}
|
}
|
||||||
function renderClips(clips) {
|
function renderClips(clips) {
|
||||||
const grid = document.getElementById('pb-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 = '';
|
||||||
grid.innerHTML = clips.map(c => `
|
if (clips.length === 0) {
|
||||||
<div class="clip-card" onclick="playClip('${c.path}')">
|
grid.innerHTML = '<p style="color:var(--text-muted);grid-column:1/-1">No recordings found for this selection.</p>';
|
||||||
<img src="${c.snap || ''}" class="clip-thumb" loading="lazy"
|
return;
|
||||||
onerror="this.style.display='none'" alt="${c.time}">
|
}
|
||||||
<div>${c.live ? '🔴 ' : '🎬 '}${c.time}</div>
|
clips.forEach(c => {
|
||||||
<div class="clip-time">${c.name}</div>
|
const card = document.createElement('div');
|
||||||
<div class="clip-size">${formatSize(c.size)}</div>
|
card.className = 'clip-card';
|
||||||
</div>
|
card.addEventListener('click', () => playClip(c.path));
|
||||||
`).join('');
|
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) {
|
function playClip(path) {
|
||||||
document.getElementById('pb-player').classList.remove('hidden');
|
document.getElementById('pb-player').classList.remove('hidden');
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ func (rm *RecorderManager) startRecorder(cam CameraConfig) {
|
||||||
cam.Username, cam.Password, cam.IP)
|
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()
|
rm.mu.Lock()
|
||||||
proc := &recorderProcess{
|
proc := &recorderProcess{
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed public/*
|
//go:embed public/*
|
||||||
|
|
@ -22,6 +23,7 @@ type Server struct {
|
||||||
http *http.Server
|
http *http.Server
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
appConfig *Config // mutable config reference for hot-reload
|
appConfig *Config // mutable config reference for hot-reload
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates and configures the HTTP server.
|
// NewServer creates and configures the HTTP server.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue