feat(rvs): interner Port optional verschluesselt (RVS_HTTP_TLS)

Mit RVS_HTTP_TLS=true spricht der RVS auf RVS_HTTP_PORT HTTPS/WSS statt
Klartext — damit ist auch die Strecke externer Proxy → RVS verschluesselt.
Das oeffentliche Zertifikat hat der Proxy; hier reicht ein selbstsigniertes,
das beim ersten Start in data/rvs-tls/ erzeugt wird (eigenes darf dort
liegen). Image bekommt dafuer openssl. Nur ohne den Caddy im Stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 12:23:24 +01:00
co-authored by Claude Opus 5.5
parent 88036bcad3
commit 8cc85c1fe1
5 changed files with 50 additions and 4 deletions
+11 -2
View File
@@ -876,7 +876,16 @@ RVS_HTTP_PORT=3000
```
Steht der Proxy auf einem anderen Rechner, den Port per Firewall **nur fuer den
Proxy** freigeben — dahinter ist alles Klartext. Umstellen von Caddy:
Proxy** freigeben.
**Strecke Proxy → RVS verschluesseln:** `RVS_HTTP_TLS=true` — der RVS spricht
dann HTTPS auf `RVS_HTTP_PORT`, mit einem selbstsignierten Zertifikat (beim
ersten Start in `rvs/data/rvs-tls/` erzeugt; eigenes `rvs.crt`/`rvs.key` darf
dort liegen). Im Proxy als Ziel `https://<ip>:<port>`, das Zertifikat dabei
**nicht** pruefen (nginx: Default; Nginx Proxy Manager: Scheme `https`,
„Websockets Support" an). Nur ohne Caddy — der spricht den RVS per HTTP an.
Umstellen von Caddy:
`docker compose rm -sf caddy && docker compose up -d`.
nginx-Beispiel (WebSocket braucht HTTP/1.1 + Upgrade-Header und lange Timeouts):
@@ -891,7 +900,7 @@ server {
ssl_certificate_key /etc/letsencrypt/live/rvs.example.de/privkey.pem;
location / {
proxy_pass http://192.168.1.10:3000; # RVS_HTTP_BIND-IP : RVS_HTTP_PORT
proxy_pass http://192.168.1.10:3000; # RVS_HTTP_BIND-IP : RVS_HTTP_PORT (https:// bei RVS_HTTP_TLS=true)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
+6
View File
@@ -27,3 +27,9 @@ CADDY_HTTPS_PORT=444
# den Proxy freigeben (Klartext!).
RVS_HTTP_BIND=127.0.0.1
RVS_HTTP_PORT=3000
# Auch die Strecke Proxy → RVS verschluesseln: der RVS spricht dann HTTPS
# auf RVS_HTTP_PORT, mit selbstsigniertem Zertifikat (wird beim ersten Start
# in ./data/rvs-tls/ erzeugt; eigenes rvs.crt/rvs.key darf dort liegen).
# Im Proxy dann https://<ip>:<port> als Ziel, Zertifikat NICHT pruefen.
# Nur ohne Caddy — der Caddy im Stack spricht den RVS per HTTP an.
RVS_HTTP_TLS=false
+3
View File
@@ -2,6 +2,9 @@ FROM node:22-alpine
WORKDIR /app
# openssl: erzeugt bei RVS_HTTP_TLS=true das interne Zertifikat
RUN apk add --no-cache openssl
# Erst package.json kopieren — Docker-Cache für npm install nutzen
COPY package.json ./
RUN npm install --production
+2
View File
@@ -28,7 +28,9 @@ services:
- "${RVS_HTTP_BIND:-127.0.0.1}:${RVS_HTTP_PORT:-3000}:3000"
volumes:
- ./updates:/updates # APK-Dateien fuer Auto-Update
- ./data/rvs-tls:/certs # internes Zertifikat (RVS_HTTP_TLS)
environment:
- RVS_HTTP_TLS=${RVS_HTTP_TLS:-false}
- MAX_SESSIONS=10
# 4 GB V8-Heap — sonst OOM beim Empfang von 1 GB-Files
# (base64 inflated ~1.34 GB plus WS-Frame-Margin).
+28 -2
View File
@@ -2,6 +2,8 @@
const { WebSocketServer } = require("ws");
const http = require("http");
const https = require("https");
const { execFileSync } = require("child_process");
const fs = require("fs");
const path = require("path");
@@ -9,6 +11,12 @@ const path = require("path");
const PORT = parseInt(process.env.PORT || "3000", 10);
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
const UPDATES_DIR = process.env.UPDATES_DIR || "/updates";
// HTTPS statt HTTP auf PORT — fuer die Strecke externer Reverse-Proxy → RVS.
// Das oeffentliche Zertifikat hat der Proxy; hier reicht ein selbstsigniertes
// (wird beim ersten Start erzeugt). NICHT zusammen mit dem Caddy im Stack
// (der spricht den RVS per HTTP an).
const HTTP_TLS = /^(1|true|yes|ja)$/i.test(process.env.RVS_HTTP_TLS || "");
const TLS_DIR = process.env.RVS_TLS_DIR || "/certs";
// Kein Polling — APK wird manuell per git pull bereitgestellt
// Erlaubte Nachrichtentypen — alles andere wird verworfen
@@ -274,7 +282,25 @@ function cleanupRooms() {
// WICHTIG: Node-Default-Heap ist ~1.5 GB. Fuer 1 GB-Files muss der
// Container mit --max-old-space-size=4096 (oder NODE_OPTIONS env var)
// gestartet werden, sonst OOM-Crash beim Empfang.
const httpServer = http.createServer(handleHttpRequest);
/** Zertifikat fuer den internen HTTPS-Port: vorhandenes aus TLS_DIR nehmen
* (eigenes darf man dort ablegen), sonst einmalig selbstsigniert erzeugen. */
function loadInternalTls() {
const key = path.join(TLS_DIR, "rvs.key");
const crt = path.join(TLS_DIR, "rvs.crt");
if (!fs.existsSync(key) || !fs.existsSync(crt)) {
fs.mkdirSync(TLS_DIR, { recursive: true });
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-sha256",
"-days", "3650", "-subj", "/CN=aria-rvs-intern", "-keyout", key, "-out", crt],
{ stdio: "ignore" });
fs.chmodSync(key, 0o600);
log(`Internes TLS-Zertifikat erzeugt: ${crt} (selbstsigniert, 10 Jahre)`);
}
return { key: fs.readFileSync(key), cert: fs.readFileSync(crt) };
}
const httpServer = HTTP_TLS
? https.createServer(loadInternalTls(), handleHttpRequest)
: http.createServer(handleHttpRequest);
const wss = new WebSocketServer({ noServer: true, maxPayload: 1500 * 1024 * 1024 });
// HTTP-Upgrade-Pfad → an WebSocket-Server reichen
@@ -285,7 +311,7 @@ httpServer.on("upgrade", (req, socket, head) => {
});
httpServer.listen(PORT, () => {
log(`RVS läuft auf Port ${PORT} (HTTP + WS) | Max Sessions: ${MAX_SESSIONS}`);
log(`RVS läuft auf Port ${PORT} (${HTTP_TLS ? "HTTPS + WSS, internes Zertifikat" : "HTTP + WS"}) | Max Sessions: ${MAX_SESSIONS}`);
// Beim Start pruefen ob eine APK da ist
const apkInfo = getLatestAPK();
if (apkInfo) log(`APK bereit: v${apkInfo.version} (${(fs.statSync(apkInfo.path).size / 1024 / 1024).toFixed(1)}MB)`);