124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
// NextNVR v0.1.0 — HTTP server and routing
|
|
// Serves the embedded SPA and API endpoints.
|
|
package main
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
//go:embed public/*
|
|
var publicFiles embed.FS
|
|
|
|
// Server wraps the HTTP server and application state.
|
|
type Server struct {
|
|
config Config
|
|
http *http.Server
|
|
mux *http.ServeMux
|
|
appConfig *Config // mutable config reference for hot-reload
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewServer creates and configures the HTTP server.
|
|
func NewServer(cfg Config) (*Server, error) {
|
|
s := &Server{
|
|
config: cfg,
|
|
appConfig: &cfg,
|
|
mux: http.NewServeMux(),
|
|
}
|
|
|
|
s.registerRoutes()
|
|
|
|
s.http = &http.Server{
|
|
Addr: cfg.Server.BindHost + cfg.Server.Port,
|
|
Handler: s.middleware(s.mux),
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// ListenAndServe starts the HTTP server.
|
|
func (s *Server) ListenAndServe() error {
|
|
log.Printf("HTTP server listening on %s", s.http.Addr)
|
|
return s.http.ListenAndServe()
|
|
}
|
|
|
|
// Close shuts down the HTTP server.
|
|
func (s *Server) Close() {
|
|
if s.http != nil {
|
|
s.http.Close()
|
|
}
|
|
}
|
|
|
|
// go2rtcProxy returns a reverse proxy to go2rtc on port 1984.
|
|
// Used to avoid mixed-content blocking when NextNVR is behind HTTPS.
|
|
func (s *Server) go2rtcProxy() http.Handler {
|
|
target, _ := url.Parse("http://127.0.0.1:1984")
|
|
return httputil.NewSingleHostReverseProxy(target)
|
|
}
|
|
|
|
// registerRoutes sets up all API and static file routes.
|
|
func (s *Server) registerRoutes() {
|
|
// API endpoints.
|
|
s.mux.HandleFunc("/api/cameras", s.handleCameras)
|
|
s.mux.HandleFunc("/api/cameras/", s.handleCameraByID)
|
|
s.mux.HandleFunc("/api/config", s.handleConfig)
|
|
s.mux.HandleFunc("/api/scan", s.handleScan)
|
|
s.mux.HandleFunc("/api/status", s.handleStatus)
|
|
s.mux.HandleFunc("/api/recordings", s.handleRecordings)
|
|
|
|
// Live MJPEG stream proxy — proxied through NextNVR to avoid cross-origin issues.
|
|
s.mux.HandleFunc("/stream/", s.handleStream)
|
|
|
|
// go2rtc reverse proxy — serves the go2rtc web UI and API through NextNVR.
|
|
// This avoids mixed-content blocking when NextNVR is behind HTTPS.
|
|
s.mux.Handle("/go2rtc/", http.StripPrefix("/go2rtc",
|
|
s.go2rtcProxy()))
|
|
|
|
// Static file server for recordings (actual video files on disk).
|
|
s.mux.Handle("/recordings/", http.StripPrefix("/recordings/",
|
|
http.FileServer(http.Dir(s.config.Storage.RecordingsPath))))
|
|
|
|
// Static file server for embedded SPA.
|
|
publicFS, _ := fs.Sub(publicFiles, "public")
|
|
s.mux.Handle("/", http.FileServer(http.FS(publicFS)))
|
|
}
|
|
|
|
// middleware wraps handlers with CORS, no-cache, and logging.
|
|
func (s *Server) middleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
// Prevent caching of static files during development.
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// API logging.
|
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
log.Printf("%s %s", r.Method, r.URL.Path)
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// jsonResponse writes a JSON response with the given status code.
|
|
func jsonResponse(w http.ResponseWriter, status int, resp APIResponse) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|