From cb07a0d9e1bb15c9972168ce5afa274ea9ad091d Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 23 Oct 2026 11:50:34 -0400 Subject: [PATCH] Isolate bundled go2rtc and run recorder containers without root --- Dockerfile | 17 +- Dockerfile.alpine | 15 +- README.md | 14 +- config/go2rtc/go2rtc-test.yaml | 3 +- config/go2rtc/go2rtc.yaml | 3 +- docker-compose.yml | 18 +- docker-entrypoint.sh | 60 +----- docs/DOCKER.md | 63 ++++-- docs/GO2RTC_INTEGRATION.md | 16 +- docs/HOME_ASSISTANT.md | 2 +- docs/INSTALLATION.md | 16 +- docs/REVERSE_PROXY.md | 185 ++++++++---------- docs/WINDOWS_PODMAN.md | 7 +- examples/go2rtc_config_example.yaml | 2 +- go2rtc | 2 +- src/video/go2rtc/go2rtc_process.c | 14 +- .../test_go2rtc_process_config_generation.c | 2 +- web/js/utils/settings-utils.js | 51 +---- web/tests/go2rtcSettings.spec.js | 8 +- 19 files changed, 229 insertions(+), 269 deletions(-) diff --git a/Dockerfile b/Dockerfile index db76d3c7c..912eb5e9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -276,9 +276,8 @@ RUN mkdir -p /bin /etc/lightnvr/go2rtc && \ # Create basic configuration file echo "# go2rtc configuration file" > /etc/lightnvr/go2rtc/go2rtc.yaml && \ echo "api:" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ - echo " listen: :1984" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ + echo " listen: 127.0.0.1:1984" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ echo " base_path: /go2rtc" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ - echo ' origin: "*"' >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ echo "" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ echo "webrtc:" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ echo " ice_servers:" >> /etc/lightnvr/go2rtc/go2rtc.yaml && \ @@ -415,12 +414,22 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh COPY docker-start.sh /bin/start.sh RUN chmod +x /bin/start.sh +# Run the recorder and its managed go2rtc child without root privileges. +RUN groupadd --gid 10001 lightnvr && useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin lightnvr && \ + mkdir -p /var/lib/lightnvr/data/recordings /var/lib/lightnvr/data/database \ + /var/lib/lightnvr/data/models /var/log/lightnvr /var/run/lightnvr && \ + chmod -R go-w /var/lib/lightnvr /var/log/lightnvr /var/run/lightnvr && \ + chown -R 10001:10001 /etc/lightnvr /var/lib/lightnvr/data \ + /var/log/lightnvr /var/run/lightnvr + +USER 10001:10001 + # Define volumes for persistent data only # Note: Do NOT mount /var/lib/lightnvr directly as it will overwrite web assets -VOLUME ["/etc/lightnvr", "/var/lib/lightnvr/data"] +VOLUME ["/etc/lightnvr", "/var/lib/lightnvr/data/database", "/var/lib/lightnvr/data/recordings", "/var/lib/lightnvr/data/models"] # Expose ports -EXPOSE 8080 8554 8555 8555/udp 1984 +EXPOSE 8080 8554 8555 8555/udp # Environment variables for configuration ENV GO2RTC_CONFIG_PERSIST=true \ diff --git a/Dockerfile.alpine b/Dockerfile.alpine index d6e0d06f1..4c82b58ac 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -118,14 +118,27 @@ COPY --from=builder /lib/libsod.so.1 /lib/libsod.so # Copy database migrations COPY --from=builder /opt/db/migrations/ /usr/share/lightnvr/migrations/ +# Use a PID location writable by the runtime user. +RUN sed -i 's|/var/run/lightnvr.pid|/var/run/lightnvr/lightnvr.pid|g' /etc/lightnvr/lightnvr.ini + # Expose required ports EXPOSE 8080 +# Run the recorder and its managed go2rtc child without root privileges. +RUN addgroup -g 10001 -S lightnvr && adduser -u 10001 -S -D -H -G lightnvr lightnvr && \ + mkdir -p /var/lib/lightnvr/data/recordings /var/lib/lightnvr/data/database \ + /var/lib/lightnvr/data/models /var/log/lightnvr /var/run/lightnvr && \ + chmod -R go-w /var/lib/lightnvr /var/log/lightnvr /var/run/lightnvr && \ + chown -R 10001:10001 /etc/lightnvr /var/lib/lightnvr/data \ + /var/log/lightnvr /var/run/lightnvr + +USER 10001:10001 + # Volume for configuration and data persistence # /etc/lightnvr - Configuration files # /var/lib/lightnvr/data - Persistent data (database, recordings, models, etc.) VOLUME /etc/lightnvr -VOLUME /var/lib/lightnvr/data +VOLUME /var/lib/lightnvr/data/database /var/lib/lightnvr/data/recordings /var/lib/lightnvr/data/models # Command to start the service CMD ["/bin/lightnvr", "-c", "/etc/lightnvr/lightnvr.ini"] diff --git a/README.md b/README.md index 1856243f1..ba9bb7b04 100644 --- a/README.md +++ b/README.md @@ -426,16 +426,20 @@ docker run -d \ -p 8554:8554 \ -p 8555:8555 \ -p 8555:8555/udp \ - -p 1984:1984 \ - -v ./config:/etc/lightnvr \ - -v ./data:/var/lib/lightnvr/data \ + --security-opt no-new-privileges --cap-drop ALL \ + -v lightnvr-config:/etc/lightnvr \ + -v lightnvr-database:/var/lib/lightnvr/data/database \ + -v lightnvr-recordings:/var/lib/lightnvr/data/recordings \ + -v lightnvr-models:/var/lib/lightnvr/data/models \ -e TZ=America/New_York \ ghcr.io/opensensor/lightnvr:latest ``` #### Volume Mounts Explained -The container uses two volume mounts for persistence: +The example uses named volumes for config, database, recordings, and models. Existing bind mounts must be owned by UID:GID `10001:10001`; see the [migration notes](docs/DOCKER.md#security-migration-non-root-runtime-and-private-go2rtc-api) before upgrading. MSE/WebRTC require the authenticated [reverse-proxy routes](docs/REVERSE_PROXY.md). + +Persistent paths: - **`/etc/lightnvr`** - Configuration files - `lightnvr.ini` - Main configuration @@ -453,7 +457,7 @@ The container uses two volume mounts for persistence: - **8080** - Web UI (HTTP) - **8554** - RTSP streaming - **8555** - WebRTC (TCP/UDP) -- **1984** - go2rtc API +- **1984** - internal go2rtc API, bound to loopback; do not publish #### Environment Variables diff --git a/config/go2rtc/go2rtc-test.yaml b/config/go2rtc/go2rtc-test.yaml index cbbb73cdd..e7f9db6fc 100755 --- a/config/go2rtc/go2rtc-test.yaml +++ b/config/go2rtc/go2rtc-test.yaml @@ -12,8 +12,7 @@ # Test configuration uses different ports to avoid conflicts with production api: - listen: :11984 - origin: "*" + listen: 127.0.0.1:11984 # Note: base_path removed for simpler API access during testing # Use base_path: /go2rtc (no trailing slash) when running behind lightNVR's reverse proxy diff --git a/config/go2rtc/go2rtc.yaml b/config/go2rtc/go2rtc.yaml index 7033f6944..f7d552ee9 100755 --- a/config/go2rtc/go2rtc.yaml +++ b/config/go2rtc/go2rtc.yaml @@ -1,7 +1,6 @@ # go2rtc configuration file api: - listen: :1984 - origin: "*" + listen: 127.0.0.1:1984 base_path: /go2rtc rtsp: diff --git a/docker-compose.yml b/docker-compose.yml index a66d9219e..6519af191 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,12 +15,16 @@ services: - "8554:8554" # RTSP - "8555:8555" # WebRTC TCP - "8555:8555/udp" # WebRTC UDP - - "1984:1984" # go2rtc API volumes: - # Only mount what needs persistence - # DO NOT mount /var/lib/lightnvr directly - it will overwrite web assets! - - ./config:/etc/lightnvr - - ./data:/var/lib/lightnvr/data + # Named config/database volumes avoid exposing broad host directories. + - lightnvr-config:/etc/lightnvr + - lightnvr-database:/var/lib/lightnvr/data/database + - ./data/recordings:/var/lib/lightnvr/data/recordings + - lightnvr-models:/var/lib/lightnvr/data/models + security_opt: + - no-new-privileges:true + cap_drop: + - ALL environment: # Timezone configuration - TZ=UTC # Change to your timezone (e.g., America/New_York, Europe/London) @@ -44,3 +48,7 @@ networks: lightnvr: driver: bridge +volumes: + lightnvr-config: + lightnvr-database: + lightnvr-models: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 177cf87da..009053404 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -66,7 +66,7 @@ init_config() { ; LightNVR Configuration File (INI format) [general] -pid_file = /var/run/lightnvr.pid +pid_file = /var/run/lightnvr/lightnvr.pid log_file = /var/log/lightnvr/lightnvr.log log_level = 2 ; 0=ERROR, 1=WARN, 2=INFO, 3=DEBUG syslog_enabled = false @@ -188,7 +188,7 @@ EOF # Create go2rtc config directory if it doesn't exist mkdir -p /etc/lightnvr/go2rtc - chown -R "$(id -u):$(id -g)" /etc/lightnvr/go2rtc + # Volume ownership must be prepared by the operator before startup. # Always regenerate go2rtc.yaml fresh at startup to avoid stale/corrupted # configs from prior versions causing stream errors (see issue #165). @@ -207,8 +207,7 @@ EOF cat > /etc/lightnvr/go2rtc/go2rtc.yaml << 'EOF' # go2rtc configuration file api: - listen: :1984 - origin: "*" + listen: 127.0.0.1:1984 base_path: /go2rtc rtsp: @@ -241,48 +240,8 @@ EOF log_info "No models to copy, skipping model initialization" fi - # Ensure proper permissions (may fail on NFS, which is okay) - # Skip entirely if the user opts out via environment variable - if [ "${SKIP_PERMISSIONS_CHECK:-false}" = "true" ]; then - log_info "Skipping permissions check (SKIP_PERMISSIONS_CHECK=true)" - else - log_info "Setting permissions..." - CURRENT_UID="$(id -u)" - CURRENT_GID="$(id -g)" - - # Recursive chown/chmod on small directories only (config, logs, database, models) - for dir in /etc/lightnvr /var/log/lightnvr \ - /var/lib/lightnvr/data/database /var/lib/lightnvr/data/models; do - [ -d "$dir" ] || continue - chown -R "${CURRENT_UID}:${CURRENT_GID}" "$dir" 2>/dev/null || log_warn "Could not set ownership on $dir (may be on NFS)" - chmod -R 755 "$dir" 2>/dev/null || log_warn "Could not set permissions on $dir (may be on NFS)" - done - - # For data directories that may contain hundreds of thousands of recording - # files, only fix the directory itself (non-recursive) to avoid an O(n) - # walk that can block startup for hours on large deployments (see #368). - for dir in /var/lib/lightnvr /var/lib/lightnvr/data \ - /var/lib/lightnvr/data/recordings /var/lib/lightnvr/data/recordings/mp4 \ - /var/lib/lightnvr/www; do - [ -d "$dir" ] || continue - chown "${CURRENT_UID}:${CURRENT_GID}" "$dir" 2>/dev/null || true - chmod 755 "$dir" 2>/dev/null || true - done - - # Per-stream subdirectories (one per camera) — chown one level deep - # so a data volume mounted from a host with a different UID still ends - # up writable. Bounded by camera count, NOT file count: -mindepth 1 - # -maxdepth 1 -type d skips the recording files inside. Even a - # 100-camera deployment touches at most ~200 entries here. - for parent in /var/lib/lightnvr/data/recordings /var/lib/lightnvr/data/recordings/mp4; do - [ -d "$parent" ] || continue - find "$parent" -mindepth 1 -maxdepth 1 -type d \ - -exec chown "${CURRENT_UID}:${CURRENT_GID}" {} + 2>/dev/null || true - find "$parent" -mindepth 1 -maxdepth 1 -type d \ - -exec chmod 755 {} + 2>/dev/null || true - done - fi - + # Do not recursively chown or chmod mounted operator files. In particular, + # retain 0600 permissions on credentials and go2rtc overrides. # Test write permissions on critical directories log_info "Testing write permissions..." for test_dir in "/var/lib/lightnvr/data" "/etc/lightnvr" "/var/log/lightnvr"; do @@ -291,7 +250,8 @@ EOF rm -f "$test_file" 2>/dev/null log_info "Write permission OK: $test_dir" else - log_error "No write permission: $test_dir - This may cause issues!" + log_error "No write permission: $test_dir - prepare volume ownership for UID:GID $(id -u):$(id -g)" + exit 1 fi done @@ -329,7 +289,7 @@ generate_go2rtc_config_from_settings() { # Remove any stale PID file left over from a previous container run. # When a container is stopped and restarted (without being recreated) the -# container filesystem is preserved, so /var/run/lightnvr.pid may still exist +# container filesystem is preserved, so /var/run/lightnvr/lightnvr.pid may still exist # with the PID from the old run. In the new container the PID namespace # restarts from 1, meaning that old PID can be reused by an unrelated process # (e.g. go2rtc or a shell). If LightNVR then finds that PID it would send @@ -337,8 +297,8 @@ generate_go2rtc_config_from_settings() { # Removing the stale file here, before LightNVR starts, prevents that. cleanup_stale_pid_file() { # Determine the PID file path. Try to read it from the config; fall back - # to the compiled-in default of /var/run/lightnvr.pid. - local pid_file="/var/run/lightnvr.pid" + # to the container default of /var/run/lightnvr/lightnvr.pid. + local pid_file="/var/run/lightnvr/lightnvr.pid" if [ -f /etc/lightnvr/lightnvr.ini ]; then local cfg_val cfg_val=$(grep -E '^\s*pid_file\s*=' /etc/lightnvr/lightnvr.ini \ diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 55e8ad0c7..3437fe3d2 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -1,5 +1,35 @@ # Docker Deployment Guide +## Security migration: non-root runtime and private go2rtc API + +Images run as UID:GID `10001:10001`. Port 1984 is internal and bound to loopback. +MSE/WebRTC now require the authenticated exact routes in [Reverse Proxy](REVERSE_PROXY.md), +with the proxy sharing LightNVR's network namespace. HLS and snapshots work through +LightNVR's authenticated HTTP proxy. + +The repository Compose file uses named volumes for config, database, and models, +and a bind mount only for `./data/recordings`. Before the first start, prepare that +recordings directory for UID:GID 10001:10001. Existing deployments must retain or +migrate their config, database, and models before switching volume layouts; otherwise +Docker creates empty volumes and the recorder appears unconfigured. Stop the recorder +and back up the data before migrating. You can keep existing mounts during migration, +provided they contain only LightNVR state and are writable by the runtime UID. + +For existing bind mounts, change ownership of the specific LightNVR directories to +10001:10001 on the host. The entrypoint no longer changes ownership or permissions +recursively. Preserve restrictive permissions on credentials and `override.yaml`. +Change an existing `pid_file = /var/run/lightnvr.pid` to +`pid_file = /var/run/lightnvr/lightnvr.pid` in `lightnvr.ini`. +Root-owned application binaries and web assets stay outside writable volumes. +Do not mount host roots, Docker sockets, or unrelated host data into the recorder. + +The bundled go2rtc rejects `exec:`, `echo:`, and `expr:` sources, including config-file +streams and redirect results. FFmpeg sources accept network URLs or registered stream +names and supported codec/input presets; raw command fragments, local file/device +inputs, and arbitrary filter/query parameters are rejected. The supported synthetic +`virtual?video=...` test patterns accept only validated pattern parameters. + + *Verified against LightNVR 0.37.x.* This guide provides comprehensive information about deploying LightNVR using Docker. @@ -39,15 +69,23 @@ services: - "8554:8554" # RTSP - "8555:8555" # WebRTC TCP - "8555:8555/udp" # WebRTC UDP - - "1984:1984" # go2rtc API volumes: - # DO NOT mount /var/lib/lightnvr directly - it will overwrite web assets! - - ./config:/etc/lightnvr - - ./data:/var/lib/lightnvr/data + - lightnvr-config:/etc/lightnvr + - lightnvr-database:/var/lib/lightnvr/data/database + - lightnvr-recordings:/var/lib/lightnvr/data/recordings + - lightnvr-models:/var/lib/lightnvr/data/models + security_opt: + - no-new-privileges:true + cap_drop: [ALL] environment: - TZ=UTC # Change to your timezone (e.g., America/New_York) # ONVIF discovery network for containers, e.g. 192.168.1.0/24 # - LIGHTNVR_ONVIF_NETWORK=192.168.1.0/24 +volumes: + lightnvr-config: + lightnvr-database: + lightnvr-recordings: + lightnvr-models: ``` Then start it: @@ -76,9 +114,11 @@ docker run -d \ -p 8554:8554 \ -p 8555:8555 \ -p 8555:8555/udp \ - -p 1984:1984 \ - -v ./config:/etc/lightnvr \ - -v ./data:/var/lib/lightnvr/data \ + --security-opt no-new-privileges --cap-drop ALL \ + -v lightnvr-config:/etc/lightnvr \ + -v lightnvr-database:/var/lib/lightnvr/data/database \ + -v lightnvr-recordings:/var/lib/lightnvr/data/recordings \ + -v lightnvr-models:/var/lib/lightnvr/data/models \ -e TZ=America/New_York \ ghcr.io/opensensor/lightnvr:latest ``` @@ -157,9 +197,7 @@ NFS volumes may have different permission models than local filesystems. If you device: ":/volume1/docker/lightnvr/config" ``` -2. **UID/GID mapping**: The container runs as root by default. Ensure your NFS export allows root access or map UIDs appropriately: - - On Synology NAS: Enable "Map all users to admin" or "Squash" options in NFS permissions - - Or use `all_squash,anonuid=1000,anongid=1000` in NFS export options +2. **UID/GID mapping**: The container runs as UID:GID `10001:10001`. Grant that identity access to the dedicated LightNVR export, or map it to a dedicated NAS service account. Keep root squashing enabled. 3. **Directory pre-creation**: For better reliability with NFS, pre-create the directory structure on your NAS: ```bash @@ -168,7 +206,7 @@ NFS volumes may have different permission models than local filesystems. If you mkdir -p /volume1/docker/lightnvr/data/recordings/mp4 mkdir -p /volume1/docker/lightnvr/data/database mkdir -p /volume1/docker/lightnvr/data/models - chmod -R 755 /volume1/docker/lightnvr + chown -R 10001:10001 /volume1/docker/lightnvr ``` #### Recording Issues on NFS @@ -227,7 +265,7 @@ copy-on-first-run step. This means: | 8080 | TCP | Web UI | Main web interface | | 8554 | TCP | RTSP | RTSP streaming server | | 8555 | TCP/UDP | WebRTC | WebRTC streaming | -| 1984 | TCP | go2rtc API | go2rtc REST API | +| 1984 | TCP | Internal go2rtc API | Loopback only; do not publish | ### Network Modes @@ -241,7 +279,6 @@ services: - "8554:8554" - "8555:8555" - "8555:8555/udp" - - "1984:1984" ``` #### Host Mode (For Better Performance) diff --git a/docs/GO2RTC_INTEGRATION.md b/docs/GO2RTC_INTEGRATION.md index bdf920924..e95cd7f6a 100644 --- a/docs/GO2RTC_INTEGRATION.md +++ b/docs/GO2RTC_INTEGRATION.md @@ -107,19 +107,7 @@ No additional configuration is required for basic functionality. ### WebRTC URLs -WebRTC stream URLs follow this format: - -``` -http://[server-address]:[go2rtc-port]/webrtc/[stream-id] -``` - -For example, if your server is at `192.168.1.100`, go2rtc is running on port `1984`, and your stream ID is `camera1`, the WebRTC URL would be: - -``` -http://192.168.1.100:1984/webrtc/camera1 -``` - -These URLs can be used in web applications that support WebRTC, including the LightNVR web interface. +Browser signaling uses the dashboard origin through the authenticated routes documented in [Reverse Proxy & HTTPS](REVERSE_PROXY.md). WebRTC SDP is posted to `/go2rtc/api/webrtc?src=`; MSE connects to `/go2rtc/api/ws?src=`. Keep port 1984 on loopback. ## Configuration @@ -135,7 +123,7 @@ Example configuration: ```yaml api: - listen: :1984 + listen: 127.0.0.1:1984 webrtc: ice_servers: diff --git a/docs/HOME_ASSISTANT.md b/docs/HOME_ASSISTANT.md index d5d9d0582..e12387ab4 100644 --- a/docs/HOME_ASSISTANT.md +++ b/docs/HOME_ASSISTANT.md @@ -59,7 +59,7 @@ change `web_port`, browse to `http://:/` directly instead. **Host networking.** Required, not cosmetic: ONVIF WS-Discovery and mDNS are multicast and do not survive a bridge network, and go2rtc's WebRTC candidates need to be directly reachable for live view to connect. The consequence is that LightNVR's ports appear on the -host itself — 7800, 8554 RTSP, 8555 TCP+UDP WebRTC, and 1984 for the go2rtc API. +host itself — 7800, 8554 RTSP, and 8555 TCP+UDP WebRTC. The go2rtc API stays on loopback; MSE/WebRTC signaling requires authenticated proxy routes in the same network namespace. **Recordings live outside backups.** This is the important one: diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index a740935ae..4cf9f8b60 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -164,9 +164,11 @@ docker run -d \ --name lightnvr \ --restart unless-stopped \ -p 8080:8080 \ - -p 1984:1984 \ - -v /path/to/config:/etc/lightnvr \ - -v /path/to/data:/var/lib/lightnvr/data \ + --security-opt no-new-privileges --cap-drop ALL \ + -v lightnvr-config:/etc/lightnvr \ + -v lightnvr-database:/var/lib/lightnvr/data/database \ + -v lightnvr-recordings:/var/lib/lightnvr/data/recordings \ + -v lightnvr-models:/var/lib/lightnvr/data/models \ ghcr.io/opensensor/lightnvr:latest ``` @@ -602,9 +604,11 @@ docker run -d \ --name lightnvr \ --restart unless-stopped \ -p 8080:8080 \ - -p 1984:1984 \ - -v /path/to/config:/etc/lightnvr \ - -v /path/to/data:/var/lib/lightnvr/data \ + --security-opt no-new-privileges --cap-drop ALL \ + -v lightnvr-config:/etc/lightnvr \ + -v lightnvr-database:/var/lib/lightnvr/data/database \ + -v lightnvr-recordings:/var/lib/lightnvr/data/recordings \ + -v lightnvr-models:/var/lib/lightnvr/data/models \ ghcr.io/opensensor/lightnvr:latest ``` diff --git a/docs/REVERSE_PROXY.md b/docs/REVERSE_PROXY.md index 1bd82be43..8570bb4c9 100644 --- a/docs/REVERSE_PROXY.md +++ b/docs/REVERSE_PROXY.md @@ -1,162 +1,149 @@ # Reverse Proxy & HTTPS -LightNVR's HTTP server does not terminate TLS itself. For HTTPS — and for any deployment that exposes the dashboard to a network you don't fully trust — run a reverse proxy (nginx, Caddy, Traefik, HAProxy, an ingress controller, …) in front and let it handle certificates. +Keep the managed go2rtc API bound to `127.0.0.1:1984`. Do not publish port 1984 or forward its entire `/go2rtc/` prefix. Browser media URLs use the dashboard's origin over both HTTP and HTTPS. -This doc covers the two things that aren't obvious if you've never put a reverse proxy in front of an NVR: +LightNVR's authenticated proxy serves HLS and snapshots. MSE and WebRTC signaling require a reverse proxy that can reach go2rtc on loopback and **authenticates requests before forwarding** these two exact paths: -1. **You have to forward two upstream services**, not one — lightNVR on port 8080 and go2rtc on port 1984. -2. **WebSocket upgrade headers are mandatory** for MSE. The same `/go2rtc/*` route carries ordinary WebRTC SDP HTTP requests directly to go2rtc without an extra LightNVR proxy hop. +| Public path | Upstream | +| --- | --- | +| `/go2rtc/api/ws` | `http://127.0.0.1:1984` (WebSocket upgrade) | +| `/go2rtc/api/webrtc` | `http://127.0.0.1:1984` (HTTP signaling) | +| All other paths | `http://127.0.0.1:8080` (LightNVR's authorization and route allowlist) | -## Upstream services +The go2rtc admin UI, config, restart, and exit endpoints are not live-view routes. Do not expose them. If using an overridden `api.base_path`, change both exact media paths in the proxy configuration to match. -| Path on the reverse proxy | Upstream | Why | -| --- | --- | --- | -| `/` (everything except `/go2rtc/`) | `http://127.0.0.1:8080` | lightNVR's HTTP server. Serves the dashboard, REST API (`/api/*`), HLS segments (`/hls/*`), and lightNVR's internal go2rtc HTTP-only proxy at `/go2rtc/api/streams`, `/go2rtc/api/stream.m3u8`, `/go2rtc/api/hls/*`, `/go2rtc/api/frame.jpeg`. | -| `/go2rtc/*` | `http://127.0.0.1:1984` | go2rtc's own HTTP/WS server. MSE requires its WebSocket endpoint; WebRTC SDP is also routed here directly to avoid LightNVR's buffered libcurl proxy. | +The examples below add a gateway login for the entire site, so the browser authenticates before opening a WebSocket. Keep LightNVR authentication enabled as well. The gateway credentials authorize access to go2rtc's media endpoints and **do not enforce LightNVR's per-camera permissions**. Only grant them to users permitted to access all streams. Use LightNVR's HLS/snapshot routes when per-camera authorization is required. -> **Why two upstreams instead of one?** lightNVR proxies a curated subset of go2rtc endpoints internally (HLS segments and snapshots) because those benefit from lightNVR's concurrency limiter and auth. Everything else — `/go2rtc/api/ws` for MSE, `/go2rtc/api/webrtc` SDP, and the go2rtc admin UI — needs to reach go2rtc directly. When you're proxying HTTPS, the cleanest fix is to route the whole `/go2rtc/*` prefix to go2rtc and let the frontend (which detects `https:` and switches routing automatically) pick the right path. See `web/js/utils/settings-utils.js#getGo2rtcBaseUrl` / `#getGo2rtcWebSocketUrl` for the client-side logic. +## Network placement -## The trust setting +These examples require the proxy and go2rtc to share a network namespace: native services on one host, containers using `network_mode: service:lightnvr`, or containers in the same Kubernetes Pod. A separate Docker bridge container or a proxy on the Docker host cannot reach the container's loopback listener. Place the proxy in the same namespace; do not restore a public API listener to work around this. -LightNVR refuses to honor `X-Forwarded-For` / `X-Real-IP` headers by default — otherwise any client could spoof a source IP for the audit log, login allow-list, and rate limiter. Tell lightNVR which proxies it can trust by setting `trusted_proxy_cidrs` in `/etc/lightnvr/lightnvr.ini`: +When a proxy container shares LightNVR's network namespace, publish the proxy's HTTPS port on the LightNVR service and mount only the proxy's config, certificates, and password file into the proxy container. Port 1984 remains unpublished. Direct access to port 8080 supports HLS and snapshots; MSE/WebRTC require the proxy. + +## Trusted proxy addresses + +Configure LightNVR to trust forwarded client addresses only from the proxy: ```ini [web] trusted_proxy_cidrs = 127.0.0.1/32,::1/128 ``` -The value is a comma- or newline-separated list of IPv4/IPv6 CIDRs that may legitimately set the forwarded headers. When the request arrives from one of those addresses, lightNVR uses the leftmost untrusted IP in `X-Forwarded-For` (or `X-Real-IP` if present) as the client address. If your reverse proxy runs on the same host, `127.0.0.1/32,::1/128` is correct; if it runs in a separate container on a Docker bridge, use that bridge's CIDR (e.g. `172.18.0.0/16`); for Kubernetes, the pod or node CIDR. +Use the actual proxy address range for any other network arrangement. This setting controls forwarded address handling; it does not authenticate media requests. -## Example: nginx +## nginx -This config — adapted from a working deployment by @dpw13 — terminates HTTPS, forwards `/` to lightNVR, and forwards `/go2rtc/*` to go2rtc. WebSocket upgrades are wired up on both locations because LightNVR uses WS for some live-view paths and go2rtc uses WS for MSE; WebRTC signaling uses ordinary HTTP on the same direct route. +Create `/etc/nginx/nvr.htpasswd` with `htpasswd`, restrict access to that file, and install your TLS certificate. Both exact media locations inherit the server's authentication requirement. ```nginx -# Redirect HTTP → HTTPS server { listen 80; - listen [::]:80; server_name nvr.example.com; return 301 https://$host$request_uri; } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + listen 443 ssl; server_name nvr.example.com; - ssl_certificate /etc/ssl/certs/nvr.crt; ssl_certificate_key /etc/ssl/private/nvr.key; - ssl_protocols TLSv1.2 TLSv1.3; + ssl_protocols TLSv1.2 TLSv1.3; - # HSTS — opt in once you're sure the cert chain is good. - # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; + auth_basic "NVR gateway"; + auth_basic_user_file /etc/nginx/nvr.htpasswd; - # WebSocket upgrade headers travel through the upstream connection. proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Long-lived streams (MSE WebSockets, HLS pulls) must not time out at - # nginx's default 60s read timeout. + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; - # Everything except /go2rtc/* → lightNVR - location / { - proxy_pass http://127.0.0.1:8080; + location = /go2rtc/api/ws { + proxy_pass http://127.0.0.1:1984; } - - # /go2rtc/* → go2rtc (bypasses LightNVR's buffered HTTP proxy; - # MSE is upgraded here and WebRTC SDP stays on the direct path) - location /go2rtc/ { + location = /go2rtc/api/webrtc { proxy_pass http://127.0.0.1:1984; } + location / { + proxy_pass http://127.0.0.1:8080; + } } ``` -If your nginx fronts containers, the upstreams will be container names or service IPs instead of `127.0.0.1`. - -## Example: Caddy +## Caddy -Caddy handles WebSocket upgrades, HSTS, and Let's Encrypt certificates without any extra knobs. +Generate a password hash with `caddy hash-password`, then replace the placeholder below. See Caddy's [basic_auth documentation](https://caddyserver.com/docs/caddyfile/directives/basic_auth). ```caddy nvr.example.com { encode zstd gzip - - # /go2rtc/* must come BEFORE the catch-all - handle /go2rtc/* { - reverse_proxy 127.0.0.1:1984 { - transport http { - read_timeout 1h - write_timeout 1h - } - } + basic_auth { + operator REPLACE_WITH_PASSWORD_HASH } + @media path /go2rtc/api/ws /go2rtc/api/webrtc + handle @media { + reverse_proxy 127.0.0.1:1984 + } handle { reverse_proxy 127.0.0.1:8080 } } ``` -## Example: Traefik +Caddy handles WebSocket upgrades automatically. Both handlers inherit the site authentication requirement. -For a Docker/Kubernetes deployment that's already using Traefik, use two routers — one for `/go2rtc/*` and one for everything else — so the path-based split routes WebSocket traffic to the right service. Compose example: +## Traefik + +Use the file provider with Traefik in the same network namespace as LightNVR. This dynamic configuration assumes a TLS `websecure` entry point and a `letsencrypt` certificate resolver are already configured. Create the password file inside the **Traefik** container; see [BasicAuth configuration](https://doc.traefik.io/traefik/v2.11/middlewares/http/basicauth/). ```yaml -services: - lightnvr: - image: ghcr.io/opensensor/lightnvr:latest - labels: - # Catch-all → lightNVR's HTTP port - traefik.enable: "true" - traefik.http.routers.lightnvr.rule: "Host(`nvr.example.com`)" - traefik.http.routers.lightnvr.tls: "true" - traefik.http.routers.lightnvr.tls.certresolver: "letsencrypt" - traefik.http.services.lightnvr.loadbalancer.server.port: "8080" - - # /go2rtc/* → go2rtc's port on the same container - # Higher priority wins when both rules match the host. - traefik.http.routers.go2rtc.rule: "Host(`nvr.example.com`) && PathPrefix(`/go2rtc`)" - traefik.http.routers.go2rtc.priority: "100" - traefik.http.routers.go2rtc.tls: "true" - traefik.http.routers.go2rtc.tls.certresolver: "letsencrypt" - traefik.http.routers.go2rtc.service: "go2rtc" - traefik.http.services.go2rtc.loadbalancer.server.port: "1984" +http: + routers: + nvr-media: + rule: "Host(`nvr.example.com`) && (Path(`/go2rtc/api/ws`) || Path(`/go2rtc/api/webrtc`))" + priority: 100 + entryPoints: [websecure] + middlewares: [nvr-auth] + service: go2rtc-media + tls: + certResolver: letsencrypt + nvr: + rule: "Host(`nvr.example.com`)" + entryPoints: [websecure] + middlewares: [nvr-auth] + service: lightnvr + tls: + certResolver: letsencrypt + middlewares: + nvr-auth: + basicAuth: + usersFile: /etc/traefik/nvr.htpasswd + services: + go2rtc-media: + loadBalancer: + servers: + - url: http://127.0.0.1:1984 + lightnvr: + loadBalancer: + servers: + - url: http://127.0.0.1:8080 ``` -Traefik upgrades WebSockets by default; no extra middleware is needed. - -## Verification - -After starting the proxy, walk through this checklist with the browser dev tools open: - -1. **Dashboard loads over `https://`** — confirms the `/` route reaches lightNVR. -2. **Live view shows video** (any backend — WebRTC, HLS, or MSE) — confirms at least the matching subset of go2rtc routing works. -3. **Open the Network tab and reload Live View** — there should be no requests to `:1984` or `:8555`. Every request should hit your reverse proxy hostname over `https:` / `wss:`. A request to `:1984` over plain `http://` is a mixed-content bug and the browser will refuse it. -4. **Check `wss://yourhost/go2rtc/api/ws?src=`** — this is the MSE WebSocket. Status should be `101 Switching Protocols`. If it's `404` or hangs, your `/go2rtc/*` route is missing or doesn't upgrade. -5. **Switch a camera to WebRTC** in the live view — the WebRTC SDP exchange is over POST to `/go2rtc/api/webrtc`, so this fails the same way an MSE WebSocket would if `/go2rtc/*` isn't routed. -6. **Open Settings → Users and check the audit log** — recent logins should show your laptop's actual IP, not `127.0.0.1`. If they show `127.0.0.1`, `trusted_proxy_cidrs` isn't set or doesn't cover the proxy's source address. +## Verify after upgrading -## Common pitfalls +1. From another host, confirm TCP port 1984 is unreachable. Inside the recorder's network namespace, confirm it listens on `127.0.0.1` only. +2. Without credentials, requests to both media routes must return `401` or `403`, including WebSocket upgrade requests and WebRTC POSTs. +3. With gateway credentials, confirm MSE upgrades to `101` and WebRTC negotiates video. Browser network requests must stay on the dashboard origin. +4. With and without gateway credentials, confirm `/go2rtc/api/config`, `/go2rtc/api/restart`, and `/go2rtc/api/exit` never reach go2rtc through the public proxy. Likewise check path suffixes and traversal variants. +5. Confirm HLS and snapshots still use LightNVR authorization, and audit logs show the correct client address. -- **Forgot `Upgrade` / `Connection` headers.** MSE live view fails to start; the WebSocket request returns 200 (or HTML) instead of 101. In nginx, this is `proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";` plus `proxy_http_version 1.1;` in the appropriate scope. -- **Routed only `/` to LightNVR and skipped `/go2rtc/*`.** HLS and snapshots work through LightNVR's internal HTTP proxy, but MSE WebSockets and WebRTC SDP never reach go2rtc directly. -- **Mixed `http:` / `https:` content.** The frontend detects `window.location.protocol` and switches its go2rtc URLs to `https://yourhost/go2rtc`. If you serve the dashboard over `https:` but the browser ends up requesting `http://yourhost:1984/...` directly, your reverse proxy isn't intercepting `/go2rtc/*` — re-check the location/router ordering. -- **`trusted_proxy_cidrs` left empty.** Audit log and rate limiter see every request as coming from the proxy (`127.0.0.1` or the container bridge IP). Login allow-lists effectively disable themselves because everyone looks local. -- **`proxy_read_timeout` at nginx's 60s default.** A WebSocket sitting idle (e.g., a paused live view, or a sub-stream with low frame rate) gets torn down after a minute. Bump it to something like an hour for the proxy paths that carry streaming traffic. -- **Reverse proxy on a separate host.** The proxy's source IP, not `127.0.0.1`, needs to be in `trusted_proxy_cidrs`. In Docker that's the bridge gateway (often `172.17.0.1/32`); in Kubernetes that's the pod or node CIDR. +## Existing deployments -## Related +Remove any old wildcard proxy route and port-1984 mapping. Review `override.yaml`: an explicit `api.listen` or `api.tls_listen` can override the generated loopback default. `/api/config`, `/api/restart`, and `/api/exit` are disabled without go2rtc credentials; when credentials are configured, authentication is mandatory even on loopback. Manage the subprocess through LightNVR instead of exposing administrative routes. -- `web/js/utils/settings-utils.js` — frontend logic that decides between direct go2rtc port and reverse-proxy paths based on `window.location.protocol`. -- `src/web/httpd_utils.c` — server-side `X-Forwarded-For` / `X-Real-IP` handling, gated by `trusted_proxy_cidrs`. -- [Configuration Guide](CONFIGURATION.md) — full `[web]` section reference, including `trusted_proxy_cidrs`. -- [Docker Deployment](DOCKER.md) — when running lightNVR + go2rtc as containers behind a host-level proxy. -- [go2rtc Config Override](GO2RTC_CONFIG_OVERRIDE.md) — if you need to change go2rtc's listen address or base path, edit `override.yaml` rather than hand-editing the generated `go2rtc.yaml`. +See [Docker deployment](DOCKER.md) for runtime user and volume migration, and [go2rtc overrides](GO2RTC_CONFIG_OVERRIDE.md) for configuration ownership. diff --git a/docs/WINDOWS_PODMAN.md b/docs/WINDOWS_PODMAN.md index 06b7abaf5..1df7fc219 100644 --- a/docs/WINDOWS_PODMAN.md +++ b/docs/WINDOWS_PODMAN.md @@ -189,7 +189,7 @@ netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 conne New-NetFirewallRule -DisplayName "LightNVR Web UI" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow ``` -Repeat for any other port you need to expose (8554 RTSP, 8555 WebRTC, 1984 go2rtc). +Repeat for any other port you need to expose (8554 RTSP, 8555 WebRTC). With NAT, **ONVIF auto-discovery by multicast will not work** — see [Adding cameras](#7-adding-cameras). @@ -207,7 +207,6 @@ podman run -d ` -p 8554:8554 ` -p 8555:8555 ` -p 8555:8555/udp ` - -p 1984:1984 ` -v lightnvr-config:/etc/lightnvr ` -v lightnvr-data:/var/lib/lightnvr/data ` -e TZ=America/New_York ` @@ -236,7 +235,7 @@ Then open **http://localhost:8080**. | Port | Purpose | Needed? | |---|---|---| | 8080 | Web UI and API | Always | -| 1984 | go2rtc API | Yes — the UI's live view uses it | +| 1984 | Internal go2rtc API | No — use authenticated same-origin proxy routes | | 8554 | RTSP restream | Only if other apps pull streams from LightNVR | | 8555 TCP+UDP | WebRTC | Only for low-latency live view from other devices | @@ -329,7 +328,6 @@ PublishPort=8080:8080 PublishPort=8554:8554 PublishPort=8555:8555 PublishPort=8555:8555/udp -PublishPort=1984:1984 Volume=lightnvr-config:/etc/lightnvr Volume=lightnvr-data:/var/lib/lightnvr/data Environment=TZ=America/New_York @@ -389,7 +387,6 @@ causes problems. | "database is locked", corruption | Database on a `C:\` bind mount | Move to a named volume — [Storage](#storage-decide-this-before-you-start) | | Container killed unexpectedly | WSL VM memory too low | Raise `memory=` in `.wslconfig`, `wsl --shutdown`, restart | | Everything stops after a reboot | Machine only starts at logon | [Keeping it running](#keeping-it-running) | -| Live view black, UI otherwise fine | go2rtc port not published | Ensure `-p 1984:1984` | Container logs are the first place to look: diff --git a/examples/go2rtc_config_example.yaml b/examples/go2rtc_config_example.yaml index 3299e4a6b..c88e9ec62 100644 --- a/examples/go2rtc_config_example.yaml +++ b/examples/go2rtc_config_example.yaml @@ -2,7 +2,7 @@ # This file is automatically generated with WebRTC NAT traversal support api: - listen: :1984 + listen: 127.0.0.1:1984 # IMPORTANT: no trailing slash ("/go2rtc", not "/go2rtc/") to avoid double-slash routes base_path: /go2rtc origin: 'http://localhost:8080' diff --git a/go2rtc b/go2rtc index 4381b7ce4..41c9dfd38 160000 --- a/go2rtc +++ b/go2rtc @@ -1 +1 @@ -Subproject commit 4381b7ce41b498ec436a615cefa6ac302e169083 +Subproject commit 41c9dfd38c513d73a8265259dcda19a1932f6174 diff --git a/src/video/go2rtc/go2rtc_process.c b/src/video/go2rtc/go2rtc_process.c index cdf582ff7..339b32672 100644 --- a/src/video/go2rtc/go2rtc_process.c +++ b/src/video/go2rtc/go2rtc_process.c @@ -1011,22 +1011,16 @@ static bool go2rtc_process_generate_config_locked(const char *config_path, int a // API configuration fprintf(config_file, "api:\n"); - fprintf(config_file, " listen: :%d\n", api_port); + fprintf(config_file, " listen: 127.0.0.1:%d\n", api_port); // The base config uses the default prefix; applying an override updates the client prefix. // IMPORTANT: do not include a trailing slash here. go2rtc registers routes as: // pattern = base_path + "/" + "api/..." // so a trailing slash would produce double slashes ("/go2rtc//api") and can cause 404s. fprintf(config_file, " base_path: %s\n", GO2RTC_BASE_PATH); - // Use wildcard for CORS origin to support both localhost and 127.0.0.1 - // This allows the web interface to access go2rtc API from any local address - fprintf(config_file, " origin: '*'\n"); - // NOTE: Do NOT pass username/password to go2rtc. go2rtc's middleware chain - // runs Auth before CORS, so when auth is enabled, unauthenticated browser - // requests (including CORS preflights) get rejected with 401 before the - // CORS headers are added — causing the browser to block the response. - // lightNVR's own auth on port 8080 protects the main application; go2rtc - // on its local port does not need a separate auth layer. + // Browsers reach media through authenticated same-origin proxies. Leave + // cross-origin access disabled. No go2rtc credentials are generated: its + // administrative config/restart/exit endpoints therefore remain disabled. // RTSP configuration - use configured port or default to 8554 int rtsp_port = global_config->go2rtc_rtsp_port > 0 ? global_config->go2rtc_rtsp_port : 8554; diff --git a/tests/unit/test_go2rtc_process_config_generation.c b/tests/unit/test_go2rtc_process_config_generation.c index a985f2d20..35826e69a 100644 --- a/tests/unit/test_go2rtc_process_config_generation.c +++ b/tests/unit/test_go2rtc_process_config_generation.c @@ -110,7 +110,7 @@ void test_generate_startup_config_uses_saved_webrtc_settings(void) { char *contents = read_text_file(config_path); TEST_ASSERT_NOT_NULL(contents); - TEST_ASSERT_NOT_NULL(strstr(contents, " listen: :31984")); + TEST_ASSERT_NOT_NULL(strstr(contents, " listen: 127.0.0.1:31984")); TEST_ASSERT_NOT_NULL(strstr(contents, " listen: \":31554\"")); TEST_ASSERT_NOT_NULL(strstr(contents, " listen: \":31555\"")); TEST_ASSERT_NOT_NULL(strstr(contents, " - \"192.168.50.10:31555\"")); diff --git a/web/js/utils/settings-utils.js b/web/js/utils/settings-utils.js index ab24ca449..d42c0a3cd 100644 --- a/web/js/utils/settings-utils.js +++ b/web/js/utils/settings-utils.js @@ -70,58 +70,19 @@ export async function getGo2rtcApiPort() { } /** - * Get the go2rtc base URL for HTTP API calls (HLS, WebRTC SDP, etc.) - * - * Uses the active api.base_path from the applied go2rtc override. - * The default prefix is /go2rtc; an empty prefix serves APIs at the root. - * - * Over HTTPS: Routes through an external reverse proxy at the configured prefix. - * The proxy must handle both HTTP and WebSocket connections. - * - * Over HTTP (local dev): lightNVR's built-in /go2rtc/* proxy only handles HLS - * and snapshots (not WebRTC). WebRTC must connect directly to go2rtc's port - * to avoid the latency and buffering of an extra HTTP proxy hop. - * - * @returns {Promise} - go2rtc base URL (e.g., "https://hostname/go2rtc" or "http://hostname:1984/go2rtc") + * Media signaling stays on the authenticated site origin. MSE/WebRTC require + * the exact reverse-proxy routes described in docs/REVERSE_PROXY.md. */ export async function getGo2rtcBaseUrl() { const settings = await getSettings(); - const basePath = settings.go2rtc_base_path ?? '/go2rtc'; - // Over HTTPS, use the external reverse proxy (nginx, ingress, etc.) - // which must handle HTTP and WebSocket at the configured prefix. - if (window.location.protocol === 'https:') { - return `${window.location.origin}${basePath}`; - } - - // Over HTTP (local dev), connect directly to go2rtc's port - // because lightNVR's proxy doesn't handle WebRTC endpoints - const port = settings.go2rtc_api_port || 1984; - return `http://${window.location.hostname}:${port}${basePath}`; + return `${window.location.origin}${settings.go2rtc_base_path ?? '/go2rtc'}`; } -/** - * Get the go2rtc WebSocket URL for MSE streaming. - * - * lightNVR's reverse proxy at /go2rtc/* uses curl (HTTP-only) and CANNOT - * handle WebSocket upgrade requests. MSE streaming requires a WebSocket - * connection, so it must connect directly to go2rtc, bypassing the proxy. - * - * Over HTTP: ws://hostname:go2rtc_port/go2rtc (direct to go2rtc) - * Over HTTPS: wss://hostname/go2rtc (external reverse proxy handles WS) - * - * @returns {Promise} - WebSocket-compatible go2rtc base URL - */ +/** Get the same-origin WebSocket base URL for MSE streaming. */ export async function getGo2rtcWebSocketUrl() { const settings = await getSettings(); - const basePath = settings.go2rtc_base_path ?? '/go2rtc'; - // Over HTTPS, the external reverse proxy (nginx, HA ingress, etc.) handles - // WebSocket upgrades natively at the configured prefix. - if (window.location.protocol === 'https:') { - return `wss://${window.location.host}${basePath}`; - } - // Over HTTP, connect directly to go2rtc's port (bypass lightNVR's HTTP-only proxy) - const port = settings.go2rtc_api_port || 1984; - return `ws://${window.location.hostname}:${port}${basePath}`; + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}${settings.go2rtc_base_path ?? '/go2rtc'}`; } // Default browser-side WebRTC live-view timeouts (milliseconds). These mirror diff --git a/web/tests/go2rtcSettings.spec.js b/web/tests/go2rtcSettings.spec.js index aff25f241..27b1662ca 100644 --- a/web/tests/go2rtcSettings.spec.js +++ b/web/tests/go2rtcSettings.spec.js @@ -9,11 +9,11 @@ beforeEach(() => { } }; }); -test.each(['/foo', '/nested/go2rtc', '', '/go2rtc'])('direct playback uses prefix %j', async (prefix) => { +test.each(['/foo', '/nested/go2rtc', '', '/go2rtc'])('HTTP playback stays on the site origin with prefix %j', async (prefix) => { fetchJSON.mockResolvedValue({ go2rtc_api_port: 31984, go2rtc_base_path: prefix }); await getSettings(true); - expect(await getGo2rtcBaseUrl()).toBe(`http://camera.test:31984${prefix}`); - expect(await getGo2rtcWebSocketUrl()).toBe(`ws://camera.test:31984${prefix}`); + expect(await getGo2rtcBaseUrl()).toBe(`http://camera.test:8080${prefix}`); + expect(await getGo2rtcWebSocketUrl()).toBe(`ws://camera.test:8080${prefix}`); }); test('HTTPS playback uses the custom external proxy path', async () => { @@ -28,5 +28,5 @@ test('HTTPS playback uses the custom external proxy path', async () => { test('older servers retain the default prefix', async () => { fetchJSON.mockResolvedValue({}); await getSettings(true); - expect(await getGo2rtcBaseUrl()).toBe('http://camera.test:1984/go2rtc'); + expect(await getGo2rtcBaseUrl()).toBe('http://camera.test:8080/go2rtc'); });