feat(sense): add loopback NVR management console
Harness governance / validate (pull_request) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user