/* eslint-disable no-process-env */ /** * Copyright (C) 2016-2020 Raphaƫl Jakse * * @licstart * This file is part of Trivabble. * * Trivabble is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * Trivabble is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Trivabble. If not, see . * @licend * * @source: https://trivabble.1s.fr/ * @source: https://gitlab.com/raphj/trivabble/ */ /*eslint strict: [2, "global"]*/ "use strict"; const port = parseInt(process.env.TRIVABBLE_PORT || "3000"); const SAVE_TIMEOUT = 5000; const KEEP_ALIVE = 30000; const GAMES_BACKUP = process.env.TRIVABBLE_GAMES_BACKUP || "games.backup.json"; const DEFAULT_BOARD_LANG = process.env.TRIVABBLE_DEFAULT_BOARD_LANG || "fr"; const VERSION = 202005070100; function envTrue(name) { return (process.env[name] || "").toLowerCase() === "true"; } const DEV_ENABLE_SERVING_FILES = envTrue("DEV_ENABLE_SERVING_FILES"); const DEBUG_LOG = DEV_ENABLE_SERVING_FILES || envTrue("DEBUG_LOG"); if (DEV_ENABLE_SERVING_FILES) { console.log("DEV_ENABLE_SERVING_FILES: Serving files in the current directory. Please never do this on a production server, this is for development purposes only."); } const debuglog = DEBUG_LOG ? console.log.bind(console) : () => null; const http = require("http"); const path = require("path"); const fs = require("fs"); const crypto = require("crypto"); const REQUEST_TYPE_LONG_POLLING = 1; const REQUEST_TYPE_SSE = 2; const REQUEST_TYPE_WEBSOCKET = 3; /* eslint no-sync: ["error", { allowAtRootLevel: true }] */ /* Manage multi language board */ const boardTilesPerLang = {}; const availableBoardLangs = {}; for (const lang of fs.readdirSync(path.join(__dirname, "lang"))) { const data = require(path.join(__dirname, "lang", lang)); // eslint-disable-line global-require boardTilesPerLang[data.code] = data; availableBoardLangs[data.code] = data.name; } const games = {}; let saveTimeout = null; let running = true; const server = http.createServer(handleRequest); server.setTimeout(0); // The default in node 13 is 0. Earlier versions have 120. function saveGames(callback) { fs.writeFile(GAMES_BACKUP, JSON.stringify(games), function (err) { if (err) { console.error("ERROR: Cannot save games!"); } if (typeof callback === "function") { return callback(err); } }); saveTimeout = null; } function shuffleInPlace(a) { // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const x = a[i]; a[i] = a[j]; a[j] = x; } } function Game() { this.init(); this.listeningPlayers = []; this.pendingEvents = []; } function writeMessage(responseAndType, data, terminate) { if (!responseAndType[0]) { return; } if (responseAndType[1] === REQUEST_TYPE_WEBSOCKET) { webSocketWrite(responseAndType[0], data); } else { responseAndType[0][terminate ? "end" : "write"]( responseAndType[1] === REQUEST_TYPE_SSE ? "data:" + data + "\n\n" : data.length + data ); } if (terminate) { stopKeepAlive(responseAndType); if (responseAndType[1] === REQUEST_TYPE_WEBSOCKET && responseAndType[0] && !responseAndType[0].isDestroyed) { responseAndType[0].end(webSocketCloseBuffer); responseAndType[0] = null; } } else { keepAlive(responseAndType); } } function stop() { console.log("Saving games a first time"); if (saveTimeout) { clearTimeout(saveTimeout); saveTimeout = null; } saveGames(function () { running = false; console.log("Closing connections and saving the game state..."); let listeningPlayerCount = 0; let gamesCount = 0; for (const gameID of Object.keys(games)) { const game = games[gameID]; for (const listeningPlayer of game.listeningPlayers) { writeMessage(listeningPlayer, '{"stopping": 2000}', true); } if (game.listeningPlayers.length) { listeningPlayerCount += game.listeningPlayers.length; gamesCount++; } } console.log( "Stopped", gamesCount, "game" + (gamesCount === 1 ? "" : "s"), "and", listeningPlayerCount, "player connection" + (listeningPlayerCount === 1 ? "" : "s") + "." ); server.close(saveGames); }); } const webSocketPingBuffer = Buffer.from([ 0b10001001, 0b00000000 ]); const webSocketCloseBuffer = Buffer.from([ 0b10001000, 0b00000000 ]); function stopKeepAlive(responseAndType) { if (responseAndType.keepAliveTimeout) { clearTimeout(responseAndType.keepAliveTimeout); responseAndType.keepAliveTimeout = 0; } } function keepAlive(responseAndType) { stopKeepAlive(responseAndType); responseAndType.keepAliveTimeout = setTimeout(function keepAliveTimeout() { responseAndType[0].write( responseAndType[1] === REQUEST_TYPE_WEBSOCKET ? webSocketPingBuffer : ( responseAndType[1] === REQUEST_TYPE_SSE ? ":\n\n" : "2[]" ) ); }, KEEP_ALIVE); } function newBoard() { const res = new Array(15 * 15); for (let i = 0; i < 15 * 15; i++) { res[i] = ""; } return res; } Game.prototype.init = function (lang) { this.board = newBoard(); this.lang = lang || DEFAULT_BOARD_LANG; this.bag = boardTilesPerLang[this.lang].bag.slice(); this.letterValues = boardTilesPerLang[this.lang].letterValues; this.racks = {}; this.scores = {}; this.lastUpdated = new Date(); this.currentPlayer = ""; shuffleInPlace(this.bag); }; Game.prototype.toJSON = function () { return { board: this.board, lang: this.lang, bag: this.bag, letterValues: this.letterValues, racks: this.racks, scores: this.scores, lastUpdated: this.lastUpdated.toISOString(), currentPlayer: this.currentPlayer }; }; Game.fromJSON = function (obj) { const game = new Game(); game.board = obj.board || newBoard(); game.lang = obj.lang || DEFAULT_BOARD_LANG; game.bag = boardTilesPerLang[game.lang].bag.slice(); game.letterValues = boardTilesPerLang[game.lang].letterValues; game.racks = obj.racks || {}; game.scores = obj.scores || {}; game.lastUpdated = obj.lastUpdated ? new Date(obj.lastUpdated) : new Date(); game.currentPlayer = obj.currentPlayer || ""; return game; }; Game.prototype.getPlayerRack = function (player) { const playerID = "#" + player; return (this.racks[playerID] || (this.racks[playerID] = [])); }; Game.prototype.getPlayerScore = function (player) { const playerID = "#" + player; return (this.scores[playerID] || (this.scores[playerID] = 0)); }; Game.prototype.setPlayerScore = function (player, score) { const playerID = "#" + player; if (!Object.prototype.hasOwnProperty.call(this.racks, playerID) || typeof score !== "number") { return; } this.scores[playerID] = score; this.pendingEvents.push({ players: [{ player: player, score: score }] }); }; Game.prototype.setCurrentPlayer = function (player) { this.currentPlayer = player; this.pendingEvents.push({currentPlayer: player}); }; Game.prototype.playerJoined = function (playerName) { if (playerName) { this.getPlayerRack(playerName); // Create the player's rack } const players = []; for (let player of Object.keys(this.racks)) { player = player.slice(1); // '#' players.push({ player: player, score: this.getPlayerScore(player), rackCount: countTiles(this.getPlayerRack(player)) }); } this.pendingEvents.push({players: players}); }; Game.prototype.addListeningPlayer = function (playerName, responseAndType) { const that = this; that.listeningPlayers.push(responseAndType); keepAlive(responseAndType); let closed = false; function close() { if (closed) { return; } closed = true; stopKeepAlive(responseAndType); responseAndType[0] = null; const index = that.listeningPlayers.indexOf(responseAndType); if (index !== -1) { that.listeningPlayers[index] = that.listeningPlayers[that.listeningPlayers.length - 1]; that.listeningPlayers.pop(); } } responseAndType[0].on("error", close); responseAndType[0].on("close", close); responseAndType[0].on("finish", close); responseAndType[0].on("prefinish", close); this.playerJoined(playerName); this.commit(); }; Game.prototype.commit = function () { const msg = JSON.stringify(this.pendingEvents); this.pendingEvents = []; for (let i = 0; i < this.listeningPlayers.length; i++) { while (i < this.listeningPlayers.length && !this.listeningPlayers[i]) { this.listeningPlayers[i] = this.listeningPlayers[this.listeningPlayers.length - 1]; this.listeningPlayers.pop(); } if (this.listeningPlayers[i]) { writeMessage(this.listeningPlayers[i], msg); } } if (saveTimeout === null) { saveTimeout = setTimeout(saveGames, SAVE_TIMEOUT); } }; Game.prototype.bagPopLetter = function (player) { if (this.bag.length) { const letter = this.bag.pop(); this.pendingEvents.push({ player: player, action: "popBag", remainingLetters: this.bag.length }); return letter; } return ""; }; Game.prototype.getCell = function (index) { return this.board[index]; }; Game.prototype.setCell = function (index, letter, player) { this.board[index] = letter; this.pendingEvents.push({ player: player, action: "setCell", indexTo: index, letter: letter }); }; Game.prototype.bagPushLetter = function (letter, player) { if (letter) { this.bag.push(letter); shuffleInPlace(this.bag); this.pendingEvents.push({ player: player, action: "pushBag", remainingLetters: this.bag.length }); } }; Game.prototype.reset = function (player) { this.init(this.lang); this.pendingEvents.push({ player: player, action: "reset", board: this.board, remainingLetters: this.bag.length, rack: [] }); this.playerJoined(); }; function newGameId() { let number; let k = 10000; let retries = 0; do { number = Math.floor(Math.random() * k).toString(); if (retries > 10) { retries = 0; k *= 10; } else { retries++; } } while (games[number]); return number.toString(); } function countTiles(rack) { let count = 0; for (let i = 0; i < rack.length; i++) { if (rack[i]) { count++; } } return count; } function joinGame(gameNumber) { if (!gameNumber) { gameNumber = newGameId(); } const game = games[gameNumber] || (games[gameNumber] = new Game()); return {gameNumber, game}; } function reply(message, response, cmdNumber, r) { response.write( JSON.stringify({...r, id: message.id || message.cmds[cmdNumber].id}) ); } function handleCommand(cmdNumber, message, response) { const {gameNumber, game} = joinGame(message.gameNumber); game.lastUpdated = new Date(); const playerName = message.playerName; let rack = null; const cmd = message.cmds[cmdNumber]; switch (cmd.cmd) { case "sync": // DEPRECATED. Here for old clients. case "joinGame": { reply(message, response, cmdNumber, { error: 0, gameNumber: gameNumber, playerName: playerName, boardLang: game.lang, availableBoardLangs: availableBoardLangs, currentPlayer: game.currentPlayer, rack: game.getPlayerRack(playerName), board: game.board, remainingLetters: game.bag.length, letterletterValues: game.letterValues, version: VERSION }); break; } case "hello": { game.playerJoined(playerName); reply(message, response, cmdNumber, {error: 0, boardLang: game.lang, version: VERSION}); break; } case "score": { game.setPlayerScore(cmd.player, cmd.score); reply(message, response, cmdNumber, {error: 0}); break; } case "currentPlayer": { game.setCurrentPlayer(cmd.player); reply(message, response, cmdNumber, {error: 0}); break; } case "moveLetter": { let letter = ""; // This case can fail in various ways. Instead altering the state // of the game immediately, we store the operations to run them // at the very end, if nothing failed. const operations = []; switch (cmd.from) { case "rack": rack = game.getPlayerRack(playerName); if (cmd.indexFrom > 6 || cmd.indexFrom < 0) { reply(message, response, cmdNumber, {error: 1, reason: "Wrong indexFrom"}); return false; } letter = rack[cmd.indexFrom]; if (!letter) { reply(message, response, cmdNumber, {error: 1, reason: "Moving from an empty location"}); return false; } operations.push(() => { rack[cmd.indexFrom] = ""; }); break; case "board": if (cmd.indexFrom < 0 || cmd.indexFrom >= 15 * 15) { reply(message, response, cmdNumber, {error: 1, reason: "Wrong indexFrom"}); return false; } letter = game.getCell(cmd.indexFrom); operations.push(game.setCell.bind(game, cmd.indexFrom, "", playerName)); break; case "bag": if (!game.bag.length) { reply(message, response, cmdNumber, {error: 1, reason: "Empty bag"}); return false; } operations.push(() => { letter = game.bagPopLetter(playerName); }); break; default: reply(message, response, cmdNumber, {error: 1, reason: "Moving letter from an unknown place"}); return false; } switch (cmd.to) { case "rack": if (cmd.indexTo < 0 || cmd.indexTo > 6) { reply(message, response, cmdNumber, {error: 1, reason: "Wrong indexTo"}); return false; } rack = rack || game.getPlayerRack(playerName); if (rack[cmd.indexTo]) { reply(message, response, cmdNumber, {error: 1, reason: "Moving a tile to a non-empty location"}); return false; } operations.push(() => { rack[cmd.indexTo] = letter; }); break; case "board": if (cmd.indexTo < 0 || cmd.indexTo >= 15 * 15) { reply(message, response, cmdNumber, {error: 1, reason: "Wrong indexTo"}); return false; } operations.push(game.setCell.bind(game, cmd.indexTo, letter, playerName)); break; case "bag": operations.push(game.bagPushLetter.bind(game, letter, playerName)); break; default: response.write("{\"error\":1, \"reason\":\"Moving letter to an unknown place\"}"); return false; } for (const operation of operations) { operation(); } reply(message, response, cmdNumber, { error: 0, rack: ( (cmd.from === "bag" && cmd.to === "rack") ? rack : undefined // eslint-disable-line no-undefined ), remainingLetters: game.bag.length }); if (rack) { game.pendingEvents.push({ players: [{ player: playerName, rackCount: countTiles(rack) }] }); } break; } case "setRack": { if (cmd.rack.length > 7) { reply(message, response, cmdNumber, { error: 1, rack: rack, reason: "the new rack is not at the right size" }); return false; } rack = game.getPlayerRack(playerName); const oldRackSorted = rack.filter((l) => l); oldRackSorted.sort(); const newRackSorted = cmd.rack.filter((l) => l); newRackSorted.sort(); for (let i = 0; i < 7; i++) { if ((oldRackSorted[i] !== newRackSorted[i]) && (oldRackSorted[i] || newRackSorted[i])) { reply(message, response, cmdNumber, { error: 1, rack: rack, reason: "the new rack doesn't contain the same number of letters" }); return false; } } for (let i = 0; i < 7; i++) { rack[i] = cmd.rack[i]; } reply(message, response, cmdNumber, {error: 0}); break; } case "resetGame": { game.reset(); reply(message, response, cmdNumber, {error: 0}); break; } case "changeBoard": { game.lang = cmd.lang || DEFAULT_BOARD_LANG; game.reset(); reply(message, response, cmdNumber, {error: 0, boardLang: game.lang, letterValues: game.letterValues}); break; } case "msg": { game.pendingEvents.push({ msg: { sender: playerName, content: cmd.msg, specialMsg: cmd.specialMsg } }); reply(message, response, cmdNumber, {error: 0}); break; } default: { reply(message, response, cmdNumber, {error: 1, reason: "Unknown command"}); return false; } } return true; } function handleCommands(message, responseAndType) { if (!message.cmds || !message.cmds.length) { const {gameNumber, game} = joinGame(message.gameNumber); writeMessage(responseAndType, JSON.stringify({ playerName: message.playerName, currentPlayer: game.currentPlayer, gameNumber: gameNumber, boardLang: game.lang, availableBoardLangs: availableBoardLangs, letterValues: game.letterValues, rack: game.getPlayerRack(message.playerName), board: game.board, remainingLetters: game.bag.length, version: VERSION }) ); game.addListeningPlayer(message.playerName, responseAndType); return; } let wsMessage = ""; const response = ( responseAndType[1] === REQUEST_TYPE_WEBSOCKET ? { write: function (s) { wsMessage += s; }, end: function (s) { if (s) { wsMessage += s; } webSocketWrite(responseAndType[0], wsMessage); } } : responseAndType[0] ); response.write("["); for (let i = 0; i < message.cmds.length; i++) { if (i) { response.write(","); } if (!handleCommand(i, message, response)) { break; } } response.end("]"); if (games[message.gameNumber]) { games[message.gameNumber].commit(); } } // Thx https://medium.com/hackernoon/implementing-a-websocket-server-with-node-js-d9b78ec5ffa8 function webSocketWrite(socket, data) { /* eslint-disable capitalized-comments */ // Copy the data into a buffer data = Buffer.from(data); const byteLength = data.length; // Note: we're not supporting > 65535 byte payloads at this stage const lengthByteCount = byteLength < 126 ? 0 : 2; const payloadLength = lengthByteCount === 0 ? byteLength : 126; const buffer = Buffer.alloc(2 + lengthByteCount + byteLength); // Write out the first byte, using opcode `1` to indicate that the message // payload contains text data buffer.writeUInt8(0b10000001, 0); buffer.writeUInt8(payloadLength, 1); // Write the length of the payload to the second byte if (lengthByteCount === 2) { buffer.writeUInt16BE(byteLength, 2); } // Write the data to the data buffer data.copy(buffer, 2 + lengthByteCount); socket.write(buffer); } function webSocketGetMessage(buffer) { /* eslint-disable no-bitwise, capitalized-comments */ let dataAfter = ""; let finalOffset = -1; if (buffer.length < 2) { return ["", 0]; } const firstByte = buffer.readUInt8(0); const isFinalFrame = Boolean((firstByte >>> 7) & 0x1); // const [reserved1, reserved2, reserved3] = [ // Boolean((firstByte >>> 6) & 0x1), // Boolean((firstByte >>> 5) & 0x1), // Boolean((firstByte >>> 4) & 0x1) // ]; const opCode = firstByte & 0xF; // We can return null to signify that this is a connection termination frame if (opCode === 0x8) { return null; } const secondByte = buffer.readUInt8(1); const isMasked = (secondByte >>> 7) & 0x1; // Keep track of our current position as we advance through the buffer let currentOffset = 2; let payloadLength = secondByte & 0x7F; if (payloadLength > 125) { if (payloadLength === 126) { payloadLength = buffer.readUInt16BE(currentOffset); currentOffset += 2; } else { // 127 // If this has a value, the frame size is ridiculously huge! // const leftPart = buffer.readUInt32BE(currentOffset); // const rightPart = buffer.readUInt32BE(currentOffset += 4); // Honestly, if the frame length requires 64 bits, you're probably doing it wrong. // In Node.js you'll require the BigInt type, or a special library to handle this. throw new Error("Large websocket payloads not currently implemented"); } } if (payloadLength + currentOffset > buffer.length) { return ["", 0]; } if (!isFinalFrame) { const message = webSocketGetMessage(buffer.slice(payloadLength + currentOffset)); if (message) { if (message[1]) { dataAfter = message[0]; finalOffset = message[1]; } else { return ["", 0]; } } else if (message === null) { // ??!? return null; } } let maskingKey; if (isMasked) { maskingKey = buffer.readUInt32BE(currentOffset); currentOffset += 4; } // Allocate somewhere to store the final message data const data = Buffer.alloc(payloadLength); // Only unmask the data if the masking bit was set to 1 if (isMasked) { // Loop through the source buffer one byte at a time, keeping track of which // byte in the masking key to use in the next XOR calculation for (let i = 0, j = 0; i < payloadLength; ++i, j = i % 4) { // Extract the correct byte mask from the masking key const shift = (j === 3) ? 0 : (3 - j) << 3; const mask = (shift === 0 ? maskingKey : (maskingKey >>> shift)) & 0xFF; // Read a byte from the source buffer const source = buffer.readUInt8(currentOffset++); // XOR the source byte and write the result to the data buffer data.writeUInt8(mask ^ source, i); } } else { // Not masked - we can just read the data as-is buffer.copy(data, 0, currentOffset); } return [ opCode === 0x1 ? (data.toString("utf8") + dataAfter) : "", finalOffset === -1 ? currentOffset : finalOffset ]; } function handleRequest(request, response) { let post = ""; const responseAndType = [response, REQUEST_TYPE_LONG_POLLING]; let upgradedToWebSocket = false; response.on("error", function connectionError(e) { console.error("An error occurred while trying to write on a socket", e); stopKeepAlive(responseAndType); }); // Thx http://stackoverflow.com/questions/4295782/how-do-you-extract-post-data-in-node-js request.on("data", function (data) { if (upgradedToWebSocket) { return; } post += data; // Too much POST data, kill the connection! if (post.length > 1e6) { request.connection.destroy(); } }); request.on("upgrade", function () { upgradedToWebSocket = true; }); request.on("error", function (error) { console.error("Error while handling this request", request, error); }); request.on("end", function () { if (upgradedToWebSocket) { return; } if (!running) { response.statusCode = 503; response.statusMessage = "Server is stopping"; response.end("Server is stopping"); return; } if (DEV_ENABLE_SERVING_FILES && !request.url.startsWith("/:")) { if (request.url === "/") { request.url = "/index.html"; } debuglog("Serving " + request.url); const requestedPath = path.join(__dirname, "..", "public", request.url); fs.exists(requestedPath, function (exists) { if (exists) { fs.readFile(requestedPath, function (err, contents) { if (err) { response.statusCode = 500; response.setHeader("Content-Type", "text/plain; charset=utf-8"); response.end(err); } let mime = "application/xhtml+xml; charset=utf-8"; const mimes = { ".mp3": "audio/mpeg", ".ogg": "audio/ogg", ".js": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml" }; for (const i in mimes) { if (Object.prototype.hasOwnProperty.call(mimes, i) && request.url.endsWith(i)) { mime = mimes[i]; if (i === ".js") { contents = contents.toString().replace( /(?^|\s|\()(?:const|let)(?\s)/gu, "$1var$2" ); } break; } } response.setHeader("Content-Type", mime); response.end(contents); }); } else { response.statusCode = 404; response.end("404"); } }); return; } const sseMatch = request.url.match(/\/(?::[^/]+\/)?sse\/(?[\s\S]+)$/u); if (sseMatch) { response.setHeader("Content-Type", "text/event-stream"); post = decodeURIComponent(sseMatch.groups.data); responseAndType[1] = REQUEST_TYPE_SSE; } else { response.setHeader("Content-Type", "text/plain"); response.setHeader("Transfer-Encoding", "chunked"); } debuglog("REQ:", request.url, post, responseAndType[1]); try { post = post && JSON.parse(post); } catch (e) { response.statusCode = 400; response.setHeader("application/json"); response.end('{"error":1, "reason": "Unable to parse your JSON data"}'); return; } handleCommands(post, responseAndType); }); } fs.readFile(GAMES_BACKUP, function (err, data) { try { if (err) { console.error("WARNING: Could not restore previous backup of the games"); } else { const backup = JSON.parse(data); for (const gameNumber of Object.keys(backup)) { games[gameNumber] = Game.fromJSON(backup[gameNumber]); } } } catch (e) { console.error("WARNING: Could not restore previous backup of the games: file is broken:"); console.error("WARNING: ", e); } // See https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers server.on("upgrade", function upgradeToWebsocket(request, socket) { const responseAndType = [socket, REQUEST_TYPE_WEBSOCKET]; function closeSocket() { responseAndType[0] = null; } socket.on("error", function (e) { console.error("Socket error", e.toString()); closeSocket(); }); socket.on("close", closeSocket); const webSocketMatch = request.url.match(/\/(?::[^/]+\/)?ws\/(?[\s\S]+)$/u); let decodedData; try { decodedData = decodeURIComponent(webSocketMatch.groups.data); } catch (e) { socket.end("HTTP/1.1 400 Bad Request"); console.error("WS: FAILED - Could not decode the given data " + webSocketMatch.groups.data); return; } let message; if (webSocketMatch) { try { message = JSON.parse(decodedData); } catch (e) { socket.end("HTTP/1.1 400 Bad Request"); console.error("WS: FAILED - Could not parse the given data " + decodedData); return; } } else { socket.end("HTTP/1.1 400 Bad Request"); return; } if (request.headers.upgrade.toLowerCase() !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); console.error("WS: FAILED - HTTP header 'Upgrade' is not 'websocket' " + decodedData); return; } socket.write( "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" ); const webSocketKey = request.headers["sec-websocket-key"]; if (webSocketKey) { socket.write( "Sec-WebSocket-Accept: " + ( crypto .createHash("sha1") .update(webSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", "binary") .digest("base64") ) + "\r\n" ); } socket.write("\r\n"); debuglog("WS: established " + decodedData); let receivedBytes = null; socket.on("data", function webSocketData(buffer) { if (receivedBytes) { buffer = Buffer.concat([receivedBytes, buffer]); receivedBytes = null; } while (true) { const wsMessage = webSocketGetMessage(buffer); if (wsMessage && wsMessage[1]) { if (wsMessage[0]) { debuglog("WS message: " + wsMessage[0]); try { message.cmds = JSON.parse(wsMessage[0]); } catch (e) { writeMessage(responseAndType, '{"error":1, "reason": "Unable to parse your JSON data"}'); return; } handleCommands(message, responseAndType); message.cmds = null; } } else if (wsMessage === null) { if (responseAndType[0] && !socket.isDestroyed) { responseAndType[0] = null; socket.end(webSocketCloseBuffer); } return; } if (wsMessage[1] === buffer.length) { break; } if (wsMessage[1] === 0) { receivedBytes = buffer; return; } buffer = buffer.slice(wsMessage[1]); } }); handleCommands(message, responseAndType); }); server.on("error", function (error) { console.error("An error happened in the HTTP server", error); }); server.listen(port, function () { console.log("Server listening on: http://localhost:%s", port); }); }); process.once("SIGINT", function () { console.log("SIGINT received..."); stop(); }); process.once("SIGTERM", function () { console.log("SIGTERM received..."); stop(); }); process.on("uncaughtException", function (err) { console.log(err); });