From 308f09916b88c60f145db356ee41f8b795298dbd Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Mon, 15 Nov 2021 14:26:39 -0600 Subject: [PATCH] Possibly functional deno server --- .gitignore | 6 + .vim/coc-settings.json | 10 ++ default_env.tres | 5 + icon.png | Bin 0 -> 3305 bytes icon.png.import | 35 ++++++ project.godot | 25 ++++ readme.md | 3 + server.ts | 266 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 350 insertions(+) create mode 100644 .gitignore create mode 100644 .vim/coc-settings.json create mode 100644 default_env.tres create mode 100644 icon.png create mode 100644 icon.png.import create mode 100644 project.godot create mode 100644 readme.md create mode 100644 server.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3922fbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.import/ +export.cfg +export_presets.cfg +*.translation +.mono +data_*/ diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 0000000..a81fc3c --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,10 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true, + "prettier.disableLanguages": [ + "typescript", + "javascript" + ], + "tsserver.enable": false +} \ No newline at end of file diff --git a/default_env.tres b/default_env.tres new file mode 100644 index 0000000..98f26a7 --- /dev/null +++ b/default_env.tres @@ -0,0 +1,5 @@ +[gd_resource type="Environment" load_steps=2 format=2] +[sub_resource type="ProceduralSky" id=1] +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c98fbb601c83c81ec8c22b1dba7d1d57c62b323c GIT binary patch literal 3305 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 literal 0 HcmV?d00001 diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..a4c02e6 --- /dev/null +++ b/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..980bc49 --- /dev/null +++ b/project.godot @@ -0,0 +1,25 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +[application] + +config/name="kdt" +config/icon="res://icon.png" + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +quality/driver/driver_name="GLES2" +vram_compression/import_etc=true +vram_compression/import_etc2=false +environment/default_environment="res://default_env.tres" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5ad06d7 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# Signalling Server + + deno run -A server.ts diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..3884a74 --- /dev/null +++ b/server.ts @@ -0,0 +1,266 @@ +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"; + +function randomInt(low: number, high: number) { + return Math.floor(Math.random() * (high - low + 1) + low); +} + +function randomId() { + const arr = new Int32Array(1); + crypto.getRandomValues(arr); + return Math.abs(arr[0]); +} + +function randomSecret() { + let out = ""; + for (let i = 0; i < 16; i++) { + out += ALFNUM[randomInt(0, ALFNUM.length - 1)]; + } + return out; +} + +class ProtoError extends Error { + code: number; + + constructor(code: number, message: string) { + super(message); + this.code = code; + } +} + +class Peer { + id: number; + ws: WebSocket; + lobby: string; + timeout: number; + + constructor(id: number, ws: WebSocket) { + 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 { + name: string; + host: number; + peers: Peer[]; + sealed: boolean; + closeTimer: number; + + constructor(name: string, host: number) { + this.name = name; + this.host = host; + this.peers = []; + this.sealed = false; + this.closeTimer = -1; + } + + getPeerId(peer: Peer) { + if (this.host === peer.id) return 1; + return peer.id; + } + + join(peer: Peer) { + const assigned = this.getPeerId(peer); + peer.ws.send(`I: ${assigned}\n`); + this.peers.forEach((p) => { + p.ws.send(`N: ${assigned}\n`); + peer.ws.send(`N: ${this.getPeerId(p)}\n`); + }); + this.peers.push(peer); + } + leave(peer: Peer) { + const idx = this.peers.findIndex((p) => peer === p); + if (idx === -1) return false; + const assigned = this.getPeerId(peer); + const close = assigned === 1; + this.peers.forEach((p) => { + // Room host disconnected, must close. + if (close) p.ws.close(4000, STR_HOST_DISCONNECTED); + // Notify peer disconnect. + else p.ws.send(`D: ${assigned}\n`); + }); + this.peers.splice(idx, 1); + if (close && this.closeTimer >= 0) { + // We are closing already. + clearTimeout(this.closeTimer); + this.closeTimer = -1; + } + return close; + } + seal(peer: 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("S: \n"); + }); + 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: Peer, lobbyName: string) { + 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)); + 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(`J: ${lobbyName}\n`); +} + +function parseMsg(peer: Peer, msg: string) { + const sep = msg.indexOf("\n"); + if (sep < 0) throw new ProtoError(4000, STR_INVALID_FORMAT); + + const cmd = msg.slice(0, sep); + if (cmd.length < 3) throw new ProtoError(4000, STR_INVALID_FORMAT); + + const data = msg.slice(sep); + + // Lobby joining. + if (cmd.startsWith("J: ")) { + joinLobby(peer, cmd.substr(3).trim()); + 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 (cmd.startsWith("S: ")) { + lobby.seal(peer); + return; + } + + // Message relaying format: + // + // [O|A|C]: DEST_ID\n + // PAYLOAD + // + // O: Client is sending an offer. + // A: Client is sending an answer. + // C: Client is sending a candidate. + let destId = parseInt(cmd.substr(3).trim()); + // Dest is not an ID. + if (!destId) throw new ProtoError(4000, STR_INVALID_DEST); + if (destId === 1) destId = lobby.host; + const dest = lobby.peers.find((p: Peer) => p.id === destId); + // Dest is not in this room. + if (!dest) throw new ProtoError(4000, STR_INVALID_DEST); + + function isCmd(what: string) { + return cmd.startsWith(`${what}: `); + } + if (isCmd("O") || isCmd("A") || isCmd("C")) { + dest.ws.send(cmd[0] + ": " + lobby.getPeerId(peer) + data); + return; + } + throw new ProtoError(4000, STR_INVALID_CMD); +} + +console.log(`Server running on port ${PORT}`); +const conn = Deno.listen({ port: PORT }); +const httpConn = Deno.serveHttp(await conn.accept()); +const e = await httpConn.nextRequest(); +if (e) { + const { socket, response } = Deno.upgradeWebSocket(e.request); + const id = randomId(); + const peer = new Peer(id, socket); + socket.onopen = (_ev) => { + if (peersCount >= MAX_PEERS) { + socket.close(4000, STR_TOO_MANY_PEERS); + return; + } + peersCount++; + }; + socket.onmessage = (ev) => { + try { + parseMsg(peer, ev.data); + } catch (e) { + const code = e.code || 4000; + console.log(`Error parsing message from ${id}:\n` + ev.data); + socket.close(code, e.message); + } + }; + socket.onclose = (ev) => { + peersCount--; + console.log( + `Connection with peer ${peer.id} closed ` + + `with reason: ${ev.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; + } + }; + socket.onerror = (e) => console.error("WebSocket error:", e); + e.respondWith(response); +}