Files

433 lines
11 KiB
JavaScript

const { createClient } = require('redis');
const WebSocket = require("ws");
const url = require("url");
const dotenv = require('dotenv');
const path = require('path');
const envPath = path.resolve(__dirname, '..', 'config', '.env.redis');
dotenv.config({ path: envPath });
const redisClient = createClient({
password: process.env.REDDIS_PASSWORD,
socket: {
host: process.env.REDDIS_HOST,
port: process.env.REDDIS_PORT
}
});
redisClient.on('error', (err) => console.error('Redis Client Error', err));
async function startRedis() {
await redisClient.connect();
console.log('Connected and authenticated with Redis!');
}
startRedis();
const PORT = 8082;
const wss = new WebSocket.Server({ port: PORT });
const groups = new Map();
const authenticatedGroups = new Map();
const clients = new Set();
const HEARTBEAT_INTERVAL = 30000;
const MAX_MESSAGE_SIZE = 10 * 1024; // 10KB
function addToGroup(ws, wkId, accesstype, access, isAuthenticated = false) {
const targetMap = isAuthenticated ? authenticatedGroups : groups;
if (!targetMap.has(wkId)) {
targetMap.set(wkId, new Map());
}
const workspaceMap = targetMap.get(wkId);
if (!workspaceMap.has(accesstype)) {
workspaceMap.set(accesstype, new Map());
}
const accesstypeMap = workspaceMap.get(accesstype);
if (!accesstypeMap.has(access)) {
accesstypeMap.set(access, new Set());
}
const accessSet = accesstypeMap.get(access);
accessSet.add(ws);
if (isAuthenticated) {
ws.send(`Dieser Benutzer wurde einer Gruppe hinzugefügt: Wettkampf-Id: ${wkId}, Zugriffstyp: ${accesstype}, Zugriff: ${access}`);
}
}
function removeFromGroup(ws, wkId, accesstype, access, authenticted = false) {
let group;
const targetMap = authenticted ? authenticatedGroups : groups;
const workspaceMap = targetMap.get(wkId);
const accesstypeMap = workspaceMap.get(accesstype);
const accessSet = accesstypeMap.get(access);
if (accessSet) {
accessSet.delete(ws);
if (accessSet.size === 0) {
accessSet.delete(access);
}
if (accesstypeMap.size === 0) {
accesstypeMap.delete(accesstype);
}
if (workspaceMap.size === 0) {
workspaceMap.delete(wkId);
}
}
}
function sendToAccess(wkId, accesstype, access, messageObj, authenticted = false, excludeWs = null) {
const targetMap = authenticted ? authenticatedGroups : groups;
const group = targetMap.get(wkId)?.get(accesstype)?.get(access);
if (!group) return;
const message = JSON.stringify(messageObj);
for (const ws of group) {
if (ws.readyState === WebSocket.OPEN && ws !== excludeWs) {
ws.send(message);
}
}
}
function sendToAccessType(wkId, accessType, messageObj, authenticated = false, excludeWs = null) {
const targetMap = authenticated ? authenticatedGroups : groups;
const accessTypeMap = targetMap.get(wkId)?.get(accessType);
if (!accessTypeMap) return;
const message = JSON.stringify(messageObj);
for (const sAccessTypeMap of accessTypeMap.values()) {
for (const ws of sAccessTypeMap) {
if (ws.readyState === WebSocket.OPEN && ws !== excludeWs) {
ws.send(message);
}
}
}
}
function sendToSelf(ws, messageObj) {
const message = JSON.stringify(messageObj);
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
}
function sendToGroupsContaining(substring, messageObj, authenticted = false) {
const message = JSON.stringify(messageObj);
if (authenticted) {
for (const [access, group] of authenticatedGroups.entries()) {
if (access.includes(substring)) {
for (const ws of group) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
}
}
}
} else {
for (const [access, group] of groups.entries()) {
if (access.includes(substring)) {
for (const ws of group) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
}
}
}
}
}
function safeParse(data) {
try {
return JSON.parse(data);
} catch {
return null;
}
}
async function authenticateWithToken(token, ws) {
const tokenPermissions = await redisClient.get(token);
if (tokenPermissions === null) {
ws.send("unauthorized Access");
ws.terminate();
return null;
}
const authenticatedFreigaben = JSON.parse(tokenPermissions);
await redisClient.del(token);
return authenticatedFreigaben;
}
// ----------------------
// Connection handler
// ----------------------
wss.on("connection", async (ws, req) => {
const params = url.parse(req.url, true).query;
const access = typeof params.access === "string" ? params.access : "";
let authenticatedFreigaben = null;
ws.isAuthenticated = false;
if (access !== 'token' || typeof params.token !== "string") {
ws.send("unauthorized Access");
ws.terminate();
return;
}
authenticatedFreigaben = await authenticateWithToken(params.token, ws);
if (!authenticatedFreigaben) return;
ws.send("Authentifizierung mit Token erfolgreich");
ws.isAuthenticated = authenticatedFreigaben.authenticated;
ws.access = String(authenticatedFreigaben.access);
ws.accesstype = String(authenticatedFreigaben.type); // kampfrichter, wk_leitung
ws.wkId = String(authenticatedFreigaben.wk_id);
ws.isAlive = true;
clients.add(ws);
addToGroup(ws, ws.wkId, ws.accesstype, ws.access, ws.isAuthenticated);
ws.on("pong", () => {
ws.isAlive = true;
});
ws.on("message", (data) => {
if (data.length > MAX_MESSAGE_SIZE) return;
const msg = safeParse(data.toString());
if (!msg || typeof msg !== "object") return;
// Expected structure:
// {
// type: string,
// payload: object
// }
const { type, payload } = msg;
if (!type) return;
let geraetId;
switch (type) {
case "DISPLAY_CONTROL":
if (ws.accesstype !== 'wk_leitung' || ws.access !== "displaycontrol") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
} else {
sendToAccessType(ws.wkId, "display", {
type: "UPDATE_DISPLAYCONTROL",
payload
}, false);
}
break;
case "UPDATE_SCORE":
if (ws.accesstype !== "kampfrichter") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
};
if (!payload || typeof payload !== "object") return;
const geraetScreen = payload.geraet;
geraetId = String(payload.geraetId);
if (!geraetScreen || !geraetId) return;
if (ws.access !== geraetId && ws.access !== 'A') {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
}
sendToAccess(ws.wkId, "display", geraetId, {
type: "UPDATE_SCORE",
payload: payload.data
}, false);
break;
case "UPDATE_RANKLIVE_SCORE":
if (ws.accesstype !== "kampfrichter") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
};
if (!payload || typeof payload !== "object") return;
geraetId = String(payload.geraetId);
if (!geraetId) return;
if (ws.access !== geraetId && ws.access !== 'A') {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
}
sendToAccess(ws.wkId, "rankLive", geraetId, {
type: "UPDATE",
payload: payload
}, false);
sendToAccess(ws.wkId, "rankLive", 'A', {
type: "UPDATE",
payload: payload
}, false);
break;
case "UPDATE_RANKLIVE_C_SUBABT":
if (ws.accesstype !== "kampfrichter") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
};
if (ws.access !== 'A') {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
}
sendToAccessType(ws.wkId, "rankLive", {
type: "UPDATE_RANKLIVE_C_SUBABT",
payload: {}
}, false);
break;
case "SELF":
sendToSelf(ws, {
type: "SELF",
payload
});
break;
case "AUDIO":
if (ws.accesstype !== "kampfrichter") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
};
if (!payload || typeof payload !== "object") return;
const audioDiscipline = String(payload.audioDiscipline);
if (!audioDiscipline) return;
if (ws.access !== audioDiscipline && ws.access !== 'A' && audioDiscipline !== 'A') {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
}
sendToAccess(ws.wkId, "audio", audioDiscipline, {
type: "AUDIO",
payload: payload
}, false);
break;
case "KAMPFRICHTER_UPDATE":
if (ws.accesstype !== "kampfrichter") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
};
if (!payload || typeof payload !== "object") return;
const discipline = String(payload.discipline);
if (!discipline) return;
if (ws.access !== discipline && ws.access !== 'A') {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
}
sendToAccess(ws.wkId, "kampfrichter", discipline, {
type: "UPDATE",
payload: payload
}, true, ws);
sendToAccess(ws.wkId, "kampfrichter", 'A', {
type: "UPDATE",
payload: payload
}, true, ws);
break;
case "EINSTELLUNGEN_DISPLAY_UPDATE":
if (ws.accesstype !== 'wk_leitung' || ws.access !== "einstellungen") {
ws.send("Unauthorized Request");
ws.close(4003, "Unauthorized");
};
if (!payload || typeof payload !== "object") return;
const { key, value } = payload;
if (!key) return;
sendToAccessType(ws.wkId, "display", {
type: "EINSTELLUNGEN_DISPLAY_UPDATE",
payload: { key, value }
}, false);
sendToAccess(ws.wkId, 'wk_leitung', "einstellungen", {
type: "EINSTELLUNGEN_DISPLAY_UPDATE",
payload: { key, value }
}, true, true);
break;
default:
ws.send("Invalid Request");
}
});
ws.on("close", () => {
clients.delete(ws);
removeFromGroup(ws, ws.wkId, ws.accesstype, ws.access, ws.isAuthenticated);
});
ws.on("error", () => {
clients.delete(ws);
removeFromGroup(ws, ws.wkId, ws.accesstype, ws.access, ws.isAuthenticated);
});
});
// ----------------------
// Heartbeat (cleanup)
// ----------------------
setInterval(() => {
for (const ws of clients) {
if (!ws.isAlive) {
ws.terminate();
clients.delete(ws);
removeFromGroup(ws, ws.wkId, ws.accesstype, ws.access, ws.isAuthenticated);
continue;
}
ws.isAlive = false;
ws.ping();
}
}, HEARTBEAT_INTERVAL);
// ----------------------
console.log(`WebSocket server running on port ${PORT}`);