# 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`.
