/* * HTTP tunnel over WebRTC - Signaling Server * Author: Konstantinos Drakontidis * Email: gedra100sh@gmail.com */ import WebSocket from "ws"; import { P2P } from "./P2P/P2P"; import logger from "./logger/logger"; import { schemas, Message, Register, Offer, Answer, Candidate } from "./schemas/schemas"; import config from "./config/config"; import { validate } from "uuid"; enum WS_CODE { INVALID_CREDENTIALS = 3000, BACKEND_OFFLINE = 3001, UNKNOWN_TYPE = 3002, PARSE_ERROR = 3003, TIMEOUT = 3004, WS_ERROR = 3005 }; type ParseSuccess = { success: true; data: Message; }; type ParseFailure = { success: false; error: "Invalid JSON." | "Invalid Schema."; }; /* * Gets a JSON string, parses it and returns a JSON object. * On success, the success key is true and the data key contains the parsed * data. On failure, success is false and error key contains the error. */ function safeParse(raw: string): ParseSuccess | ParseFailure { let parsed; try { parsed = JSON.parse(raw); } catch (error) { return { success: false, error: "Invalid JSON." }; } parsed = schemas.safeParse(parsed); if (parsed.success) { return parsed; } else { return { success: false, error: "Invalid Schema." }; } } // Handler function that kicks inactive clients. function timeoutHandler(id: string, p2p: P2P, ws_client: WebSocket) { ws_client.close(WS_CODE.TIMEOUT, "Timeout."); logger.info("Kicked client due to inactivity."); p2p.removeClient(id); } // Handles register event function register(p2p: P2P, ws_client: WebSocket, data: Register) { const { username, password } = data; // Check if client credentials where provided if ((username == config.authentication.client.username) && (password == config.authentication.client.password)) { // Check if the server is online if (p2p.getServer() != null) { // Add client to the map and start the timer const id = p2p.addClient(ws_client); p2p.setClientTimeout(id, p2p, ws_client, timeoutHandler); logger.info(`Client registered, timeout set and assigned ID: [${id}].`); logger.info(`Registered clients: ${p2p.getMapSize()}.`); ws_client.send(JSON.stringify({ type: "id", id: id })); } else { logger.error("Client connected but server offline."); ws_client.close(WS_CODE.BACKEND_OFFLINE, "Backend offline."); } // Check if server credentials where provided and set the server } else if ((username == config.authentication.server.username) && (password == config.authentication.server.password)) { logger.info("Server set."); p2p.setServer(ws_client); } else { logger.error("Client entered invalid credentials."); ws_client.close(WS_CODE.INVALID_CREDENTIALS, "Invalid credentials."); } } // Handles offer event function offer(p2p: P2P, ws_client: WebSocket, data: Offer): void { const { id, offer } = data; const peer = p2p.getPeer(id, ws_client); // Get the peer and forward the offer to it if (peer != null) { p2p.clearClientTimeout(id); logger.info(`Forwarded offer, reset timeout ID: [${id}].`); peer.send(JSON.stringify({ type: "offer", id: id, offer: offer })); } else { logger.error(`Couldn't get peer, failed to forward offer ID: [${id}].`); } } // Handles answer event function answer(p2p: P2P, ws_client: WebSocket, data: Answer): void { const { id, answer } = data; const peer = p2p.getPeer(id, ws_client); // Get the peer and forward the answer to it if (peer != null) { logger.info(`Forwarded answer ID: [${id}].`); peer.send(JSON.stringify({ type: "answer", id: id, answer: answer })); } else { logger.error(`Couldn't get peer, failed to forward answer ID: [${id}].`); } } // Handles candidate event function candidate(p2p: P2P, ws_client: WebSocket, data: Candidate): void { const { id, candidate } = data; const peer = p2p.getPeer(id, ws_client); // Get the peer and forward the candidate to it if (peer != null) { logger.info(`Forwarded candidate ID: [${id}].`); peer.send(JSON.stringify({ type: "candidate", id: id, candidate: candidate })); } else { logger.error(`Couldn't get peer, failed to forward candidate ID: [${id}].`); } } (() => { // Start the websocket server const wss = new WebSocket.WebSocketServer({ port: config.wss_port }); logger.info(`Started WebSocket server. Listening on port: ${config.wss_port}`) // Create the P2P instance const p2p = new P2P(); logger.info("Created P2P instance."); wss.on("connection", async (ws_client) => { ws_client.on("message", async (ws_msg) => { // Parse received message and call the corresponding handler const parsed = safeParse(ws_msg.toString()); if (parsed.success) { switch (parsed.data.type) { case "register": register(p2p, ws_client, parsed.data); break; case "offer": offer(p2p, ws_client, parsed.data); break; case "answer": answer(p2p, ws_client, parsed.data); break; case "candidate": candidate(p2p, ws_client, parsed.data); break; default: logger.error(`Unknown message type. ${(parsed.data as any).type}`); ws_client.close(WS_CODE.UNKNOWN_TYPE, "Unknown message type."); break; } } else { logger.error(parsed.error); ws_client.close(WS_CODE.PARSE_ERROR, parsed.error); } }); // Remove the client on disconnect and inform the peer ws_client.on("close", (code, reason) => { if ((code != WS_CODE.INVALID_CREDENTIALS) && (code != WS_CODE.BACKEND_OFFLINE) && (code != WS_CODE.TIMEOUT)) { if (ws_client == p2p.getServer()) { logger.info("Server disconnected. Closing connections."); p2p.clear(); } else if (validate(reason.toString())) { logger.info(`Client disconnected, removed instance ID: [${reason.toString()}].`); p2p.removeClient(reason.toString()); p2p.getServer()?.send(JSON.stringify({ type: "terminate", id: reason.toString() })); } else { const id = p2p.getId(ws_client) if (id != null) { logger.error(`Client lost, found and removed instance ID: [${id}].`); p2p.removeClient(id); p2p.getServer()?.send(JSON.stringify({ type: "terminate", id: id })) } else { logger.error("Client lost, couldn't find any trace of existance... Probably a bug?"); } } } }); ws_client.on("error", (ws_error) => { logger.error(`Client caused an error: ${ws_error.message}.`); ws_client.close(WS_CODE.WS_ERROR, ws_error.message); }) }); wss.on("error", (wss_error) => { logger.error(`WebSocket server error: ${wss_error}.`); }); })();