180 lines
7.2 KiB
Markdown
180 lines
7.2 KiB
Markdown
# 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`.
|