Files
2025-07-26 12:33:08 +03:00

199 lines
7.3 KiB
TypeScript

/*
* 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}.`);
});
})();