webrtc_signaling/server_node/server.js
pietru 2098118844 Try adding is lobby public
If lobby is public it will allow it to be recived as public lobby for joining it via some kind of automatic system...
2024-05-31 09:54:56 +02:00

311 lines
8 KiB
JavaScript

const WebSocket = require('ws');
const crypto = require('crypto');
const MAX_PEERS = 4096;
const MAX_LOBBIES = 1024;
const PORT = 9080;
const ALFNUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const NO_LOBBY_TIMEOUT = 1000;
const SEAL_CLOSE_TIMEOUT = 10000;
const PING_INTERVAL = 10000;
const STR_NO_LOBBY = 'Have not joined lobby yet';
const STR_HOST_DISCONNECTED = 'Room host has disconnected';
const STR_ONLY_HOST_CAN_SEAL = 'Only host can seal the lobby';
const STR_SEAL_COMPLETE = 'Seal complete';
const STR_TOO_MANY_LOBBIES = 'Too many lobbies open, disconnecting';
const STR_ALREADY_IN_LOBBY = 'Already in a lobby';
const STR_LOBBY_DOES_NOT_EXISTS = 'Lobby does not exists';
const STR_LOBBY_IS_SEALED = 'Lobby is sealed';
const STR_INVALID_FORMAT = 'Invalid message format';
const STR_NEED_LOBBY = 'Invalid message when not in a lobby';
const STR_SERVER_ERROR = 'Server error, lobby not found';
const STR_INVALID_DEST = 'Invalid destination';
const STR_INVALID_CMD = 'Invalid command';
const STR_TOO_MANY_PEERS = 'Too many peers connected';
const STR_INVALID_TRANSFER_MODE = 'Invalid transfer mode, must be text';
const CMD = {
JOIN: 0, // eslint-disable-line sort-keys
ID: 1, // eslint-disable-line sort-keys
PEER_CONNECT: 2, // eslint-disable-line sort-keys
PEER_DISCONNECT: 3, // eslint-disable-line sort-keys
OFFER: 4, // eslint-disable-line sort-keys
ANSWER: 5, // eslint-disable-line sort-keys
CANDIDATE: 6, // eslint-disable-line sort-keys
SEAL: 7, // eslint-disable-line sort-keys
GET_PUBLIC: 8, // eslint-disable-line sort-keys
};
function randomInt(low, high) {
return Math.floor(Math.random() * (high - low + 1) + low);
}
function randomId() {
return Math.abs(new Int32Array(crypto.randomBytes(4).buffer)[0]);
}
function randomSecret() {
let out = '';
for (let i = 0; i < 16; i++) {
out += ALFNUM[randomInt(0, ALFNUM.length - 1)];
}
return out;
}
function ProtoMessage(type, id, data) {
return JSON.stringify({
'type': type,
'id': id,
'data': data || '',
});
}
const wss = new WebSocket.Server({ port: PORT });
class ProtoError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
class Peer {
constructor(id, ws) {
this.id = id;
this.ws = ws;
this.lobby = '';
// Close connection after 1 sec if client has not joined a lobby
this.timeout = setTimeout(() => {
if (!this.lobby) {
ws.close(4000, STR_NO_LOBBY);
}
}, NO_LOBBY_TIMEOUT);
}
}
class Lobby {
constructor(name, host, mesh, single_host, public) {
this.name = name;
this.host = host;
this.mesh = mesh;
this.single_host = single_host;
this.public = public;
this.peers = [];
this.sealed = false;
this.closeTimer = -1;
}
getPeerId(peer) {
return peer.id;
}
join(peer) {
const assigned = this.getPeerId(peer);
peer.ws.send(ProtoMessage(CMD.ID, assigned, this.mesh ? 'true' : ''));
this.peers.forEach((p) => {
p.ws.send(ProtoMessage(CMD.PEER_CONNECT, assigned));
peer.ws.send(ProtoMessage(CMD.PEER_CONNECT, this.getPeerId(p)));
});
this.peers.push(peer);
}
leave(peer) {
const idx = this.peers.findIndex((p) => peer === p);
if (idx === -1) {
return false;
}
if (this.peers.length>1 && !this.single_host) {
this.host=this.getPeerId(this.peers[1])
}
const assigned = this.getPeerId(peer);
const close = assigned === this.host;
this.peers.forEach((p) => {
if (close) { // Room host disconnected, must close.
p.ws.close(4000, STR_HOST_DISCONNECTED);
} else { // Notify peer disconnect.
p.ws.send(ProtoMessage(CMD.PEER_DISCONNECT, assigned));
}
});
this.peers.splice(idx, 1);
if (close && this.closeTimer >= 0) {
// We are closing already.
clearTimeout(this.closeTimer);
this.closeTimer = -1;
}
return close;
}
seal(peer) {
// Only host can seal
if (peer.id !== this.host) {
throw new ProtoError(4000, STR_ONLY_HOST_CAN_SEAL);
}
this.sealed = true;
this.peers.forEach((p) => {
p.ws.send(ProtoMessage(CMD.SEAL, 0));
});
console.log(`Peer ${peer.id} sealed lobby ${this.name} `
+ `with ${this.peers.length} peers`);
this.closeTimer = setTimeout(() => {
// Close peer connection to host (and thus the lobby)
this.peers.forEach((p) => {
p.ws.close(1000, STR_SEAL_COMPLETE);
});
}, SEAL_CLOSE_TIMEOUT);
}
}
const lobbies = new Map();
let peersCount = 0;
function joinLobby(peer, pLobby, mesh, single_host, public) {
let lobbyName = pLobby;
if (lobbyName === '') {
if (lobbies.size >= MAX_LOBBIES) {
throw new ProtoError(4000, STR_TOO_MANY_LOBBIES);
}
// Peer must not already be in a lobby
if (peer.lobby !== '') {
throw new ProtoError(4000, STR_ALREADY_IN_LOBBY);
}
lobbyName = randomSecret();
lobbies.set(lobbyName, new Lobby(lobbyName, peer.id, mesh, single_host, public));
console.log(`Peer ${peer.id} created lobby ${lobbyName}`);
console.log(`Open lobbies: ${lobbies.size}`);
}
const lobby = lobbies.get(lobbyName);
if (!lobby) {
throw new ProtoError(4000, STR_LOBBY_DOES_NOT_EXISTS);
}
if (lobby.sealed) {
throw new ProtoError(4000, STR_LOBBY_IS_SEALED);
}
peer.lobby = lobbyName;
console.log(`Peer ${peer.id} joining lobby ${lobbyName} `
+ `with ${lobby.peers.length} peers`);
lobby.join(peer);
peer.ws.send(ProtoMessage(CMD.JOIN, 0, lobbyName));
}
function parseMsg(peer, msg) {
let json = null;
try {
json = JSON.parse(msg);
} catch (e) {
throw new ProtoError(4000, STR_INVALID_FORMAT);
}
const type = typeof (json['type']) === 'number' ? Math.floor(json['type']) : -1;
const id = typeof (json['id']) === 'number' ? Math.floor(json['id']) : -1;
const data = typeof (json['data']) === 'string' ? json['data'] : '';
if (type < 0 || id < 0) {
throw new ProtoError(4000, STR_INVALID_FORMAT);
}
// Lobby joining.
if (type === CMD.JOIN) {
const dt = JSON.parse(data);
joinLobby(peer, dt['lobby'], id === 0, dt['single_host'], dt['public']);
return;
}
if (!peer.lobby) {
throw new ProtoError(4000, STR_NEED_LOBBY);
}
const lobby = lobbies.get(peer.lobby);
if (!lobby) {
throw new ProtoError(4000, STR_SERVER_ERROR);
}
// Lobby sealing.
if (type === CMD.SEAL) {
lobby.seal(peer);
return;
}
if (type === CMD.GET_PUBLIC) {
let pb = []
for (lb in lobbies.keys()){
if (lobbies[lb].public){
pb.append(lb)
}
}
const data = JSON.stringify(pb)
peer.ws.send(ProtoMessage(type, 0, data));
return;
}
// Message relaying format:
//
// {
// "type": CMD.[OFFER|ANSWER|CANDIDATE],
// "id": DEST_ID,
// "data": PAYLOAD
// }
if (type === CMD.OFFER || type === CMD.ANSWER || type === CMD.CANDIDATE) {
let destId = id;
if (id === 1) {
destId = lobby.host;
}
const dest = lobby.peers.find((e) => e.id === destId);
// Dest is not in this room.
if (!dest) {
throw new ProtoError(4000, STR_INVALID_DEST);
}
dest.ws.send(ProtoMessage(type, lobby.getPeerId(peer), data));
return;
}
throw new ProtoError(4000, STR_INVALID_CMD);
}
wss.on('connection', (ws) => {
if (peersCount >= MAX_PEERS) {
ws.close(4000, STR_TOO_MANY_PEERS);
return;
}
peersCount++;
const id = randomId();
const peer = new Peer(id, ws);
ws.on('message', (message) => {
if (typeof message !== 'string') {
ws.close(4000, STR_INVALID_TRANSFER_MODE);
return;
}
try {
parseMsg(peer, message);
} catch (e) {
const code = e.code || 4000;
console.log(`Error parsing message from ${id}:\n${
message}`);
ws.close(code, e.message);
}
});
ws.on('close', (code, reason) => {
peersCount--;
console.log(`Connection with peer ${peer.id} closed `
+ `with reason ${code}: ${reason}`);
if (peer.lobby && lobbies.has(peer.lobby)
&& lobbies.get(peer.lobby).leave(peer)) {
lobbies.delete(peer.lobby);
console.log(`Deleted lobby ${peer.lobby}`);
console.log(`Open lobbies: ${lobbies.size}`);
peer.lobby = '';
}
if (peer.timeout >= 0) {
clearTimeout(peer.timeout);
peer.timeout = -1;
}
});
ws.on('error', (error) => {
console.error(error);
});
});
const interval = setInterval(() => { // eslint-disable-line no-unused-vars
wss.clients.forEach((ws) => {
ws.ping();
});
}, PING_INTERVAL);