177 lines
6.3 KiB
TypeScript
177 lines
6.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.";
|
|
};
|
|
|
|
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." };
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function register(p2p: P2P, ws_client: WebSocket, data: Register) {
|
|
const { username, password } = data;
|
|
|
|
if ((username == config.authentication.client.username) && (password == config.authentication.client.password)) {
|
|
if (p2p.getServer() != null) {
|
|
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.");
|
|
}
|
|
} 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.");
|
|
}
|
|
}
|
|
|
|
function offer(p2p: P2P, ws_client: WebSocket, data: Offer): void {
|
|
const { id, offer } = data;
|
|
const peer = p2p.getPeer(id, ws_client);
|
|
|
|
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}].`);
|
|
}
|
|
}
|
|
|
|
function answer(p2p: P2P, ws_client: WebSocket, data: Answer): void {
|
|
const { id, answer } = data;
|
|
const peer = p2p.getPeer(id, ws_client);
|
|
|
|
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}].`);
|
|
}
|
|
}
|
|
|
|
function candidate(p2p: P2P, ws_client: WebSocket, data: Candidate): void {
|
|
const { id, candidate } = data;
|
|
const peer = p2p.getPeer(id, ws_client);
|
|
|
|
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}].`);
|
|
}
|
|
}
|
|
|
|
(() => {
|
|
const wss = new WebSocket.WebSocketServer({ port: 3000 });
|
|
|
|
const p2p = new P2P();
|
|
logger.info("Created P2P instance.");
|
|
|
|
wss.on("connection", async (ws_client) => {
|
|
ws_client.on("message", async (ws_msg) => {
|
|
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);
|
|
}
|
|
});
|
|
|
|
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}.`);
|
|
});
|
|
})(); |