feat(sense): add loopback NVR management console
Harness governance / validate (pull_request) Has been cancelled

This commit is contained in:
QiuSW
2026-08-11 14:38:22 +08:00
parent a4427a6d2b
commit 7db707e596
15 changed files with 1370 additions and 13 deletions
+100
View File
@@ -0,0 +1,100 @@
// Package console serves the loopback-only Sense engineering console.
package console
import (
"embed"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
)
//go:embed assets/index.html assets/app.css assets/app.js
var assets embed.FS
type handler struct {
previewBaseURL string
previewOrigin string
}
// NewHandler builds the self-contained console handler. Config validation also
// enforces this boundary; validating here keeps the package safe in isolation.
func NewHandler(previewBaseURL string) (http.Handler, error) {
parsed, err := url.Parse(previewBaseURL)
if err != nil || parsed.Host == "" ||
(parsed.Scheme != "http" && parsed.Scheme != "https") ||
parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" ||
(parsed.Path != "" && parsed.Path != "/") {
return nil, errors.New("invalid Sense console WebRTC base URL")
}
host := parsed.Hostname()
ip := net.ParseIP(host)
if host != "localhost" && (ip == nil || !ip.IsLoopback()) {
return nil, errors.New("Sense console WebRTC base URL is not loopback")
}
base := strings.TrimRight(parsed.String(), "/")
return &handler{
previewBaseURL: base,
previewOrigin: parsed.Scheme + "://" + parsed.Host,
}, nil
}
func (h *handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
h.securityHeaders(writer)
if request.Method != http.MethodGet && request.Method != http.MethodHead {
writer.Header().Set("Allow", "GET, HEAD")
http.Error(writer, "method not allowed", http.StatusMethodNotAllowed)
return
}
switch request.URL.Path {
case "/sense-console/":
h.serveAsset(writer, request, "assets/index.html", "text/html; charset=utf-8")
case "/sense-console/app.css":
h.serveAsset(writer, request, "assets/app.css", "text/css; charset=utf-8")
case "/sense-console/app.js":
h.serveAsset(writer, request, "assets/app.js", "text/javascript; charset=utf-8")
case "/sense-console/config":
writer.Header().Set("Content-Type", "application/json")
if request.Method == http.MethodHead {
writer.WriteHeader(http.StatusOK)
return
}
_ = json.NewEncoder(writer).Encode(map[string]any{
"webrtc_base_url": h.previewBaseURL,
"page_size": 16,
"max_active_previews": 4,
"recording_and_playback": false,
})
default:
http.NotFound(writer, request)
}
}
func (h *handler) securityHeaders(writer http.ResponseWriter) {
writer.Header().Set("Cache-Control", "no-store")
writer.Header().Set("Content-Security-Policy", fmt.Sprintf(
"default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-src %s; img-src 'self' data:; font-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'",
h.previewOrigin,
))
writer.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()")
writer.Header().Set("Referrer-Policy", "no-referrer")
writer.Header().Set("X-Content-Type-Options", "nosniff")
writer.Header().Set("X-Frame-Options", "DENY")
}
func (h *handler) serveAsset(writer http.ResponseWriter, request *http.Request, path, contentType string) {
contents, err := assets.ReadFile(path)
if err != nil {
http.Error(writer, "asset unavailable", http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", contentType)
writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(contents)))
writer.WriteHeader(http.StatusOK)
if request.Method == http.MethodGet {
_, _ = writer.Write(contents)
}
}