first release 0.0.0.2

This commit is contained in:
2026-03-08 23:31:46 +01:00
parent ea52a4cec4
commit 5eb3ebf199
1432 changed files with 99065 additions and 60 deletions
+14
View File
@@ -0,0 +1,14 @@
FROM node:22-alpine
WORKDIR /app
# Erst package.json kopieren — Docker-Cache für npm install nutzen
COPY package.json ./
RUN npm install --production
# Restliche Dateien kopieren
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
+11
View File
@@ -0,0 +1,11 @@
services:
rvs:
build: .
ports:
- "443:3000"
restart: always
environment:
- RVS_PUBLIC_HOST=rvs.hackersoft.de
- RVS_PUBLIC_PORT=443
- TOKEN_EXPIRY=3600
- MAX_SESSIONS=10
+38
View File
@@ -0,0 +1,38 @@
"use strict";
const crypto = require("crypto");
const qrcode = require("qrcode-terminal");
// ── Token generieren (32 Bytes = 64 Hex-Zeichen) ───────────────────
const token = crypto.randomBytes(32).toString("hex");
// ── Verbindungsinfo aus Umgebungsvariablen ──────────────────────────
const host = process.env.RVS_PUBLIC_HOST || "localhost";
const port = parseInt(process.env.RVS_PUBLIC_PORT || "3000", 10);
// QR-Inhalt: alles was die App zum Verbinden braucht
const payload = JSON.stringify({ host, port, token });
// ── Ausgabe ─────────────────────────────────────────────────────────
console.log("");
console.log("═══════════════════════════════════════════════════");
console.log(" ARIA — Neues Pairing-Token generiert");
console.log("═══════════════════════════════════════════════════");
console.log("");
console.log(` Host: ${host}`);
console.log(` Port: ${port}`);
console.log(` Token: ${token}`);
console.log("");
console.log(" QR-Code scannen mit der ARIA App:");
console.log("");
// QR-Code im Terminal anzeigen (klein für bessere Lesbarkeit)
qrcode.generate(payload, { small: true }, (code) => {
console.log(code);
console.log("");
console.log(` Payload: ${payload}`);
console.log("");
console.log("═══════════════════════════════════════════════════");
console.log("");
});
+17
View File
@@ -0,0 +1,17 @@
{
"name": "aria-rvs",
"version": "1.0.0",
"description": "ARIA Rendezvous-Server — WebSocket Relay mit Token-Pairing",
"main": "server.js",
"scripts": {
"start": "node server.js",
"token": "node generate-token.js"
},
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"ws": "^8.18.0",
"qrcode-terminal": "^0.12.0"
}
}
+177
View File
@@ -0,0 +1,177 @@
"use strict";
const { WebSocketServer } = require("ws");
// ── Konfiguration aus Umgebungsvariablen ────────────────────────────
const PORT = parseInt(process.env.PORT || "3000", 10);
const TOKEN_EXPIRY = parseInt(process.env.TOKEN_EXPIRY || "3600", 10) * 1000; // in ms
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
// Erlaubte Nachrichtentypen — alles andere wird verworfen
const ALLOWED_TYPES = new Set([
"chat", "audio", "file", "location", "mode", "log", "event",
]);
// Token-Raum: token -> { clients: Set<ws>, createdAt: number }
const rooms = new Map();
// ── Hilfsfunktionen ─────────────────────────────────────────────────
function timestamp() {
return new Date().toISOString();
}
function log(msg) {
console.log(`[${timestamp()}] ${msg}`);
}
// Abgelaufene und leere Räume aufräumen
function cleanupRooms() {
const now = Date.now();
for (const [token, room] of rooms) {
// Tote Clients entfernen
for (const client of room.clients) {
if (client.readyState > 1) room.clients.delete(client);
}
// Raum löschen wenn leer oder abgelaufen
if (room.clients.size === 0 || now - room.createdAt > TOKEN_EXPIRY) {
// Verbleibende Clients sauber trennen
for (const client of room.clients) {
client.close(4001, "Token abgelaufen");
}
rooms.delete(token);
log(`Raum entfernt: ${token.slice(0, 8)}... (${room.clients.size} Clients)`);
}
}
}
// ── WebSocket-Server starten ────────────────────────────────────────
const wss = new WebSocketServer({ port: PORT });
wss.on("listening", () => {
log(`RVS läuft auf Port ${PORT}`);
log(`Token-Ablauf: ${TOKEN_EXPIRY / 1000}s | Max Sessions: ${MAX_SESSIONS}`);
});
wss.on("connection", (ws, req) => {
// Token aus URL-Query lesen: ws://host:port/?token=abc123
const url = new URL(req.url, `http://${req.headers.host}`);
let token = url.searchParams.get("token");
// Wenn kein Token in der URL, auf erste Nachricht warten
if (!token) {
ws.once("message", (raw) => {
try {
const msg = JSON.parse(raw);
if (msg.token) {
registerClient(ws, msg.token);
} else {
ws.close(4000, "Kein Token angegeben");
}
} catch {
ws.close(4000, "Ungültige erste Nachricht — Token erwartet");
}
});
return;
}
registerClient(ws, token);
});
function registerClient(ws, token) {
// Maximale Anzahl aktiver Sessions prüfen
if (!rooms.has(token) && rooms.size >= MAX_SESSIONS) {
ws.close(4002, "Maximale Anzahl aktiver Sessions erreicht");
log(`Abgelehnt: Session-Limit (${MAX_SESSIONS}) erreicht`);
return;
}
// Raum erstellen oder betreten
if (!rooms.has(token)) {
rooms.set(token, { clients: new Set(), createdAt: Date.now() });
log(`Neuer Raum: ${token.slice(0, 8)}...`);
}
const room = rooms.get(token);
room.clients.add(ws);
ws._token = token;
log(`Client verbunden: ${token.slice(0, 8)}... (${room.clients.size} im Raum)`);
// Nachrichten an alle anderen Clients im selben Raum weiterleiten
ws.on("message", (raw) => {
let msg;
try {
msg = JSON.parse(raw);
} catch {
return; // Keine gültige JSON-Nachricht — ignorieren
}
// Nur erlaubte Nachrichtentypen durchlassen
if (!msg.type || !ALLOWED_TYPES.has(msg.type)) {
return;
}
// An alle anderen Clients im Raum weiterleiten
for (const client of room.clients) {
if (client !== ws && client.readyState === 1) {
client.send(raw.toString());
}
}
});
ws.on("close", () => {
room.clients.delete(ws);
log(`Client getrennt: ${token.slice(0, 8)}... (${room.clients.size} verbleibend)`);
if (room.clients.size === 0) {
rooms.delete(token);
log(`Raum geschlossen: ${token.slice(0, 8)}...`);
}
});
ws.on("error", (err) => {
log(`Fehler: ${err.message}`);
room.clients.delete(ws);
});
}
// ── Heartbeat — hält Verbindungen am Leben ──────────────────────────
const HEARTBEAT_INTERVAL = 30_000;
const heartbeat = setInterval(() => {
for (const client of wss.clients) {
if (client.isAlive === false) {
client.terminate();
continue;
}
client.isAlive = false;
client.ping();
}
}, HEARTBEAT_INTERVAL);
wss.on("connection", (ws) => {
ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; });
});
// Aufräumen alle 60 Sekunden
const cleanup = setInterval(cleanupRooms, 60_000);
wss.on("close", () => {
clearInterval(heartbeat);
clearInterval(cleanup);
});
// ── Sauberes Herunterfahren ─────────────────────────────────────────
process.on("SIGTERM", () => {
log("SIGTERM empfangen — fahre herunter");
wss.close(() => process.exit(0));
});
process.on("SIGINT", () => {
log("SIGINT empfangen — fahre herunter");
wss.close(() => process.exit(0));
});