Files
appRobotWebcam/doc/06_portForwarding_roadmap.md
2026-06-04 17:47:58 +02:00

180 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AppRobotWebcam Port-Forwarding / HTTPS-Proxy Roadmap
> Ziel: Die Viewer-Webseite über **einen** HTTPS-Reverse-Proxy nach aussen geben,
> ohne go2rtc oder interne Ports ins Internet zu hängen.
> Stand: 2026-06-04 · noch **nicht umgesetzt** (Plan zum Abarbeiten).
---
## TL;DR — welche Ports nach aussen?
| Port | Dienst | Internet? | Begründung |
|------|--------|-----------|------------|
| **443** | HTTPS-Reverse-Proxy | ✅ **ja einziger Internet-Port** | TLS-Terminierung + einziger Einstieg |
| **8444** | Node.js (Webseite, `/api`, Snapshots, Stream-WS) | nur Proxy→Backend (LAN/localhost) | Proxy leitet hierhin weiter; **nicht** ins Internet |
| 1984 | go2rtc API / WebSocket / Debug-UI | ❌ nein | bleibt intern (localhost) |
| 8555/udp | go2rtc WebRTC-Media | ❌ nein | im aktuellen **MJPEG-Modus ungenutzt** (siehe Caveat unten) |
**Merksatz:** Nach Umsetzung dieser Roadmap ist der einzige offene Port `443` am Proxy.
Alles andere läuft über genau eine Origin (`https://<host>`) → Proxy → `8444`.
> `network_mode: host`: Die Container binden direkt an Host-Ports — es gibt **kein**
> Docker-`ports:`-Mapping. „Offen/zu" steuerst du allein über die Host-Firewall.
> Läuft der Proxy auf demselben Host, muss `8444` gar nicht in der Firewall geöffnet
> werden (Proxy erreicht `127.0.0.1:8444`).
---
## Aktueller Stand (was läuft)
- Live-Bild kommt als **MJPEG über WebSocket** (`MODE = 'mjpeg'`, `public/viewer.js:9`).
Die WebRTC-Kommentare im `docker-compose.yaml` beschreiben einen **anderen** Aufbau,
der nicht aktiv ist → UDP 8555 wird derzeit nicht gebraucht.
- Node.js (`8444`) liefert: Webseite, `/config.json`, `/health`, `/api/snapshot/*`
und proxied `/api`, `/video-rtc.js`, `/video-stream.js` per HTTP an go2rtc (`server.js:34`).
- Snapshots und Skripte laufen bereits **relativ/same-origin** über `8444` → proxy-tauglich.
---
## Das Problem (warum es so noch nicht hinter HTTPS läuft)
Der Viewer baut die Stream-Verbindung aktuell **direkt** zu go2rtc auf — `public/viewer.js:36`:
```js
const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=...`; // GO2RTC_PORT = 1984
```
Hinter einem HTTPS-Proxy scheitert das doppelt:
1. **Mixed Content** — eine `https://`-Seite darf kein unverschlüsseltes `ws://` öffnen
→ Browser blockt die Verbindung hart.
2. **Proxy-Umgehung** — die URL zeigt direkt auf Port `1984`, den wir bewusst nicht
exponieren (dort hängt auch die offene go2rtc-Debug-UI).
→ Folge: Seite lädt, aber **kein Live-Bild**.
---
## Lösung — 3 Schritte
Danach geht der Stream über dieselbe Origin wie die Seite (`wss://<host>/api/ws`),
durch den Proxy, auf `8444`, intern weiter zu go2rtc `1984`.
### Schritt 1 — `public/viewer.js`: same-origin & protokoll-bewusste WS-URL
`startStream()`, aktuell `viewer.js:36`:
```js
// ALT:
const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=${encodeURIComponent(cam.id)}`;
// NEU:
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${proto}//${location.host}/api/ws?src=${encodeURIComponent(cam.id)}`;
```
- `location.host` enthält ggf. den Port → funktioniert **sowohl** direkt im LAN
(`http://host:8444``ws://host:8444/...`) **als auch** hinter HTTPS
(`https://cam.example.com``wss://cam.example.com/...`).
- `/api/ws` ist im Proxy-`pathFilter` bereits enthalten (`server.js:37`).
- `GO2RTC_PORT` / der `/config.json`-Fetch in `init()` werden für den Stream damit
**nicht mehr gebraucht** (dürfen als harmloser Toter Code bleiben oder raus).
### Schritt 2 — `server.js`: Proxy WebSockets durchreichen lassen
Im `createProxyMiddleware`-Block (`server.js:34`) `ws: true` ergänzen:
```js
const go2rtcProxy = createProxyMiddleware({
target: GO2RTC_URL,
changeOrigin: true,
ws: true, // ← NEU
pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'],
logger: console,
on: { /* … unverändert … */ },
});
```
Und nach dem Erstellen des HTTP-Servers (`server.js:113`) den Upgrade-Handler binden:
```js
const server = http.createServer(app);
server.on('upgrade', go2rtcProxy.upgrade); // ← NEU (WebSocket-Upgrades an go2rtc)
```
> `http-proxy-middleware@^3` (vorhanden, `package.json`): `ws: true` **plus** der
> explizite `server.on('upgrade', …)` ist die dokumentierte, zuverlässige Kombination.
### Schritt 3 — Reverse-Proxy: WebSocket-Upgrade durchreichen
**nginx:**
```nginx
server {
listen 443 ssl;
server_name cam.example.com;
# ssl_certificate … / ssl_certificate_key …;
location / {
proxy_pass http://127.0.0.1:8444;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # ← Pflicht für WS
proxy_set_header Connection "upgrade"; # ← Pflicht für WS
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s; # WS-Stream offen halten
}
}
```
**Synology DSM** (Systemsteuerung → Anmeldeportal → Erweitert → Reverse Proxy):
1. Regel anlegen: Quelle `HTTPS` / `cam.example.com` / `443` → Ziel `HTTP` / `localhost` / `8444`.
2. In der Regel → Reiter **Eigene Kopfzeile (Custom Header)****Erstellen → WebSocket**
(fügt `Upgrade`/`Connection` automatisch hinzu). **Ohne diesen Schritt kommt kein Bild.**
3. Optional Timeout erhöhen, damit der Stream nicht nach kurzer Zeit getrennt wird.
**Caddy** (zur Referenz — WS geht automatisch):
```caddy
cam.example.com {
reverse_proxy 127.0.0.1:8444
}
```
---
## Verifikation (nach Deploy abhaken)
- [ ] `https://cam.example.com/` lädt, Header + Kamera-Kacheln sichtbar.
- [ ] DevTools → Console: `[WebcamViewer][init] … <video-stream> definiert`, **kein** Mixed-Content-Fehler.
- [ ] DevTools → Network → Filter „WS": Eintrag `wss://cam.example.com/api/ws?src=cam0`
mit Status **101 Switching Protocols** (nicht 4xx/blocked).
- [ ] Live-Bild für cam0 **und** cam1 läuft; Status zeigt „MJPEG · live".
- [ ] „⬇ Snapshot alle" lädt JPG(s) herunter (`/api/snapshot/...` über Proxy).
- [ ] Von **aussen**: nur `443` erreichbar. `1984`, `8444`, `8555` aus dem Internet
**nicht** erreichbar (z. B. `curl https://<public>:1984` → Timeout/refused).
---
## Rollback
Kleine, isolierte Änderung in genau zwei Dateien — risikoarm:
1. `public/viewer.js` und `server.js` auf den alten Stand zurück (git).
2. `docker restart AppRobotWebcam` (lädt Code neu).
Der Live-Stream im LAN funktioniert mit der neuen `viewer.js` weiterhin
(`ws://host:8444/...`), d. h. die Änderung ist auch ohne Proxy gefahrlos.
---
## Caveat — falls je wieder WebRTC (MODE mit `webrtc`)
Wird `public/viewer.js` jemals zurück auf `MODE = 'webrtc,mse,mjpeg'` gestellt
(siehe `doc/04_Delay_roadmap.md`), ändert sich die Port-Lage:
- WebRTC-**Signaling** läuft weiter über die WebSocket (`/api/ws`) → durch den Proxy ok.
- WebRTC-**Media** läuft über **UDP 8555** und geht **nicht** durch einen HTTP(S)-Proxy.
Dann bräuchte man entweder UDP 8555 direkt erreichbar **oder** einen TURN-Server.
Für den aktuellen MJPEG-Betrieb ist das **irrelevant** — alles läuft über TCP/WS auf `8444`.