Fix scripts in bin/ to work with the public folder
authorRaphaël Jakse <raphael.trivabble@jakse.fr>
Sun, 3 May 2020 11:06:52 +0000 (13:06 +0200)
committerRaphaël Jakse <raphael.trivabble@jakse.fr>
Sun, 3 May 2020 11:40:27 +0000 (13:40 +0200)
.gitignore
bin/disable-const-let.sh
bin/setup_prod.sh
bin/upgrade-prod.sh
server/trivabble-server.js [new file with mode: 0644]
trivabble-server.js [deleted file]

index 3f44abfbac3757f23593b85ad2ceefa08688752e..cd8a3ea3b6e81898091ada886437b55af0c4b6dc 100644 (file)
@@ -1,2 +1,2 @@
-/games.backup.json
+/server/games.backup.json
 /public/config.js
index 1b9b5103d55aef79acff29560a41fae7a83db02d..4aa56f247e2359acff9bbbcd4cff91538332a4bf 100755 (executable)
@@ -9,7 +9,7 @@ WORKING_DIRECTORY="$(dirname "$(dirname "$0")")"
 
 if [ -d "$WORKING_DIRECTORY/.git" ]; then
     if [ -z "$(git status --porcelain | grep '.js$')" ]; then
-        sed -i -E 's/(^|\(|\s)(let|const)(\s)/\1var\3/g' "$WORKING_DIRECTORY"/*.js
+        sed -i -E 's/(^|\(|\s)(let|const)(\s)/\1var\3/g' "$WORKING_DIRECTORY/public"/*.js
     else
         >&2 echo "You have untracked changes in JS files. I'm not going to mess up with your working copy."
        exit 1
index c8984e3bf3217238bb66df1a6d1af28f88ea32f6..01ec820b30044668b37764d3810d574b0c8077b5 100755 (executable)
@@ -2,6 +2,6 @@
 
 exec "$(dirname "$0")/disable-const-let.sh"
 
-if ! [ -f "$(dirname "$(dirname "$0")")/config.js" ]; then
-    touch "$(dirname "$(dirname "$0")")/config.js"
+if ! [ -f "$(dirname "$(dirname "$0")")/public/config.js" ]; then
+    touch "$(dirname "$(dirname "$0")")/public/config.js"
 fi
index 79ee061e64f432eeed967d9ded8ee04307e4b5c9..b8e60a47c2cb8c80875de43c99cb572ea5727c4c 100755 (executable)
@@ -7,22 +7,56 @@ error() {
     exit 1
 }
 
-if [ -z "${1+x}" ]; then
-   error "Please provide the production directory in parameter."
-fi
+help() {
+    cat <<EOF
+$0: - update the production.
+
+This program updates Trivabble to the latest version tagged for production, and copies and prepares Trivabble for use in production.
+Trivabble is composed of two parts: the server and the client.
+The client is to be served by a regular HTTP server and lives in the public folder.
+The server is to be stored somewhere on your system that is not served by your HTTP server.
+
+Parameters:
+    --prod-public-dir DIR: the path to the public directory, from which your HTTP server will serve the client
+    --prod-server-dir DIR: the path to the server directory, not served by your HTTP server.
+EOF
+    exit 0
+}
 
 if [ "$(id -u)" = "0" ]; then
    error "Not running as root. Please run me as your webserver's user for instance, using something like sudo -H -u www-data $0 $1"
 fi
 
-PROD_DIR="$1"
+while ! [ -z "${1+x}" ]; do
+    if [ "$1" = "--prod-public-dir" ]; then
+        PROD_PUBLIC_DIR=$2
+        shift
+        shift
+    elif [ "$1" = "--prod-server-dir" ]; then
+        PROD_SERVER_DIR=$2
+        shift
+        shift
+    elif [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
+        help
+    else
+        error "Unrecognized argument $1. Type --help for more information."
+    fi
+done
+
+if [ -z "${PROD_SERVER_DIR+x}" ]; then
+   error "The server production directory is not set. Type --help for more information."
+fi
 
-if ! [ -d "$PROD_DIR" ]; then
-   error "The production directory $PROD_DIR does not exist. I give up."
+if [ -z "${PROD_PUBLIC_DIR+x}" ]; then
+   error "The public production directory is not set. Type --help for more information."
 fi
 
-if [ -d "$PROD_DIR/.git" ]; then
-   error "The production folder should not be a git repository."
+if ! [ -d "$PROD_SERVER_DIR" ]; then
+   error "The server production directory $PROD_SERVER_DIR does not exist. I give up."
+fi
+
+if ! [ -d "$PROD_PUBLIC_DIR" ]; then
+   error "The public production directory $PROD_PUBLIC_DIR does not exist. I give up."
 fi
 
 WORKING_DIRECTORY="$(dirname "$(dirname "$0")")"
@@ -31,7 +65,7 @@ if ! [ -d "$WORKING_DIRECTORY/.git" ]; then
     error "Not a git repository. Cannot update."
 fi
 
-if [ -f "$WORKING_DIRECTORY/games.backup.json" ]; then
+if [ -f "$WORKING_DIRECTORY/server/games.backup.json" ] || [ -f "$WORKING_DIRECTORY/games.backup.json" ]; then
     error "Your working copy has a 'games.backup.json' file. I give up."
 fi
 
@@ -51,13 +85,9 @@ git checkout "$new_version"
 echo "Setting up production mode..."
 "$(dirname $0)"/setup_prod.sh
 
-BACKUP_DIR="$WORKING_DIRECTORY/../trivabble-prod.bak"
-
-echo "Backing up production to $BACKUP_DIR ..."
-rsync -avp --delete "$PROD_DIR/" "$BACKUP_DIR/"
-
 echo "Sending to production..."
-rsync -avp --delete --exclude 'games.backup.json' --exclude 'config.js' --exclude '/.git' "$WORKING_DIRECTORY/" "$PROD_DIR/"
+rsync -avp --delete --exclude 'config.js' "$WORKING_DIRECTORY/public/" "$PROD_PUBLIC_DIR/"
+rsync -avp --delete --exclude 'config.js' "$WORKING_DIRECTORY/server/" "$PROD_SERVER_DIR/"
 
 echo echo "Resetting the directory..."
 git reset --hard "$new_version"
diff --git a/server/trivabble-server.js b/server/trivabble-server.js
new file mode 100644 (file)
index 0000000..51dacba
--- /dev/null
@@ -0,0 +1,1202 @@
+/* eslint-disable no-process-env */
+/**
+ * Copyright (C) 2016-2020 Raphaël Jakse <raphael.trivabble@jakse.fr>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ * @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 VERSION = 202004281800;
+
+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 fs   = require("fs");
+const crypto = require("crypto");
+
+const REQUEST_TYPE_LONG_POLLING = 1;
+const REQUEST_TYPE_SSE          = 2;
+const REQUEST_TYPE_WEBSOCKET    = 3;
+
+const frBag = [
+    " ", " ", // Jokers
+    "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
+    "A", "A", "A", "A", "A", "A", "A", "A", "A",
+    "I", "I", "I", "I", "I", "I", "I", "I",
+    "N", "N", "N", "N", "N", "N",
+    "O", "O", "O", "O", "O", "O",
+    "R", "R", "R", "R", "R", "R",
+    "S", "S", "S", "S", "S", "S",
+    "T", "T", "T", "T", "T", "T",
+    "U", "U", "U", "U", "U", "U",
+    "L", "L", "L", "L", "L",
+    "D", "D", "D",
+    "M", "M", "M",
+    "G", "G",
+    "B", "B",
+    "C", "C",
+    "P", "P",
+    "F", "F",
+    "H", "H",
+    "V", "V",
+    "J",
+    "Q",
+    "K",
+    "W",
+    "X",
+    "Y",
+    "Z"
+];
+
+/* eslint-disable quote-props */
+
+const frValues = {
+    " ": 0,
+    "E": 1,
+    "A": 1,
+    "I": 1,
+    "N": 1,
+    "O": 1,
+    "R": 1,
+    "S": 1,
+    "T": 1,
+    "U": 1,
+    "L": 1,
+    "D": 2,
+    "M": 2,
+    "G": 2,
+    "B": 3,
+    "C": 3,
+    "P": 3,
+    "F": 4,
+    "H": 4,
+    "V": 4,
+    "J": 8,
+    "Q": 8,
+    "K": 10,
+    "W": 10,
+    "X": 10,
+    "Y": 10,
+    "Z": 10
+};
+
+/* eslint-enable quote-props */
+
+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.letterValues = frValues;
+    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 () {
+    this.board = newBoard();
+    this.bag = frBag.slice();
+    this.racks = {};
+    this.scores = {};
+    this.lastUpdated = new Date();
+    this.currentPlayer = "";
+
+    shuffleInPlace(this.bag);
+};
+
+Game.prototype.toJSON = function () {
+    return {
+        board:            this.board,
+        bag:              this.bag,
+        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.bag              = obj.bag    || frBag.slice();
+    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 in this.racks) {
+        if (Object.prototype.hasOwnProperty.call(this.racks, player)) {
+            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.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,
+                currentPlayer: game.currentPlayer,
+                rack: game.getPlayerRack(playerName),
+                board: game.board,
+                remainingLetters: game.bag.length,
+                letterValues: frValues,
+                version: VERSION
+            });
+            break;
+        }
+
+        case "hello": {
+            game.playerJoined(playerName);
+            reply(message, response, cmdNumber, {error: 0, 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];
+                    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 "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,
+                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);
+
+            fs.exists("public/" + request.url, function (exists) {
+                if (exists) {
+                    fs.readFile("public/" + request.url, 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(
+                                        /(?<before>^|\s|\()(?:const|let)(?<after>\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\/(?<data>[\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 in backup) {
+                if (Object.prototype.hasOwnProperty.call(backup, gameNumber)) {
+                    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\/(?<data>[\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);
+});
diff --git a/trivabble-server.js b/trivabble-server.js
deleted file mode 100644 (file)
index 51dacba..0000000
+++ /dev/null
@@ -1,1202 +0,0 @@
-/* eslint-disable no-process-env */
-/**
- * Copyright (C) 2016-2020 Raphaël Jakse <raphael.trivabble@jakse.fr>
- *
- * @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 <http://www.gnu.org/licenses/>.
- * @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 VERSION = 202004281800;
-
-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 fs   = require("fs");
-const crypto = require("crypto");
-
-const REQUEST_TYPE_LONG_POLLING = 1;
-const REQUEST_TYPE_SSE          = 2;
-const REQUEST_TYPE_WEBSOCKET    = 3;
-
-const frBag = [
-    " ", " ", // Jokers
-    "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E", "E",
-    "A", "A", "A", "A", "A", "A", "A", "A", "A",
-    "I", "I", "I", "I", "I", "I", "I", "I",
-    "N", "N", "N", "N", "N", "N",
-    "O", "O", "O", "O", "O", "O",
-    "R", "R", "R", "R", "R", "R",
-    "S", "S", "S", "S", "S", "S",
-    "T", "T", "T", "T", "T", "T",
-    "U", "U", "U", "U", "U", "U",
-    "L", "L", "L", "L", "L",
-    "D", "D", "D",
-    "M", "M", "M",
-    "G", "G",
-    "B", "B",
-    "C", "C",
-    "P", "P",
-    "F", "F",
-    "H", "H",
-    "V", "V",
-    "J",
-    "Q",
-    "K",
-    "W",
-    "X",
-    "Y",
-    "Z"
-];
-
-/* eslint-disable quote-props */
-
-const frValues = {
-    " ": 0,
-    "E": 1,
-    "A": 1,
-    "I": 1,
-    "N": 1,
-    "O": 1,
-    "R": 1,
-    "S": 1,
-    "T": 1,
-    "U": 1,
-    "L": 1,
-    "D": 2,
-    "M": 2,
-    "G": 2,
-    "B": 3,
-    "C": 3,
-    "P": 3,
-    "F": 4,
-    "H": 4,
-    "V": 4,
-    "J": 8,
-    "Q": 8,
-    "K": 10,
-    "W": 10,
-    "X": 10,
-    "Y": 10,
-    "Z": 10
-};
-
-/* eslint-enable quote-props */
-
-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.letterValues = frValues;
-    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 () {
-    this.board = newBoard();
-    this.bag = frBag.slice();
-    this.racks = {};
-    this.scores = {};
-    this.lastUpdated = new Date();
-    this.currentPlayer = "";
-
-    shuffleInPlace(this.bag);
-};
-
-Game.prototype.toJSON = function () {
-    return {
-        board:            this.board,
-        bag:              this.bag,
-        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.bag              = obj.bag    || frBag.slice();
-    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 in this.racks) {
-        if (Object.prototype.hasOwnProperty.call(this.racks, player)) {
-            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.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,
-                currentPlayer: game.currentPlayer,
-                rack: game.getPlayerRack(playerName),
-                board: game.board,
-                remainingLetters: game.bag.length,
-                letterValues: frValues,
-                version: VERSION
-            });
-            break;
-        }
-
-        case "hello": {
-            game.playerJoined(playerName);
-            reply(message, response, cmdNumber, {error: 0, 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];
-                    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 "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,
-                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);
-
-            fs.exists("public/" + request.url, function (exists) {
-                if (exists) {
-                    fs.readFile("public/" + request.url, 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(
-                                        /(?<before>^|\s|\()(?:const|let)(?<after>\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\/(?<data>[\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 in backup) {
-                if (Object.prototype.hasOwnProperty.call(backup, gameNumber)) {
-                    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\/(?<data>[\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);
-});