diff --git a/client/src/scripts/game.js b/client/src/scripts/game.js index 5703b9b..53066ea 100644 --- a/client/src/scripts/game.js +++ b/client/src/scripts/game.js @@ -34,6 +34,9 @@ const game = () => { UserUtility.validateAnonUserSignature(res.content) ); }); + socket.on('connect_error', (err) => { + toast(err, 'error', true, false); + }); socket.on('disconnect', () => { toast('Disconnected. Attempting reconnect...', 'error', true, false); }); diff --git a/package-lock.json b/package-lock.json index 04be469..5862ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4227,6 +4227,11 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, + "rate-limiter-flexible": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.6.tgz", + "integrity": "sha512-8DVFOe89rreyut/vzwBI7vgXJynyYoYnH5XogtAKs0F/neAbCTTglXxSJ7fZeZamcFXZDvMidCBvps4KM+1srw==" + }, "raw-body": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz", diff --git a/package.json b/package.json index 1f36708..d94643f 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jasmine": "^3.5.0", "karma-jasmine": "^4.0.1", "open": "^7.0.3", + "rate-limiter-flexible": "^2.3.6", "socket.io": "^4.4.0", "socket.io-client": "^4.4.0", "webpack": "^5.65.0", diff --git a/server/main.js b/server/main.js index ddb2faa..9aeb704 100644 --- a/server/main.js +++ b/server/main.js @@ -22,24 +22,23 @@ const main = ServerBootstrapper.createServerWithCorrectHTTPProtocol(app, args.us app.set('port', args.port); -const inGameSocketServer = ServerBootstrapper.createSocketServer(main, app, args.port); +const inGameSocketServer = ServerBootstrapper.createSocketServer(main, app, args.port, logger); +const gameNamespace = ServerBootstrapper.createGameSocketNamespace(inGameSocketServer, logger); let gameManager; /* Instantiate the singleton game manager */ if (process.env.NODE_ENV.trim() === 'development') { - gameManager = new GameManager(logger, globals.ENVIRONMENT.LOCAL).getInstance(); + gameManager = new GameManager(logger, globals.ENVIRONMENT.LOCAL, gameNamespace).getInstance(); } else { - gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION).getInstance(); + gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, gameNamespace).getInstance(); } -gameManager.namespace = inGameSocketServer; - -inGameSocketServer.on('connection', function (socket) { +gameNamespace.on('connection', function (socket) { socket.on('disconnecting', (reason) => { logger.trace('client socket disconnecting because: ' + reason); }); - gameManager.addGameSocketHandlers(inGameSocketServer, socket); + gameManager.addGameSocketHandlers(gameNamespace, socket); }); /* api endpoints */ diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index c665b07..1ba9d0d 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -6,11 +6,11 @@ const GameStateCurator = require('./GameStateCurator'); const UsernameGenerator = require('./UsernameGenerator'); class GameManager { - constructor (logger, environment) { + constructor (logger, environment, namespace) { this.logger = logger; this.environment = environment; this.activeGameRunner = new ActiveGameRunner(logger).getInstance(); - this.namespace = null; + this.namespace = namespace; } addGameSocketHandlers = (namespace, socket) => { @@ -477,10 +477,10 @@ function getGameSize (cards) { } class Singleton { - constructor (logger, environment) { + constructor (logger, environment, namespace) { if (!Singleton.instance) { logger.info('CREATING SINGLETON GAME MANAGER'); - Singleton.instance = new GameManager(logger, environment); + Singleton.instance = new GameManager(logger, environment, namespace); } } diff --git a/server/modules/ServerBootstrapper.js b/server/modules/ServerBootstrapper.js index 24cb62e..c24914a 100644 --- a/server/modules/ServerBootstrapper.js +++ b/server/modules/ServerBootstrapper.js @@ -4,6 +4,7 @@ const https = require('https'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); +const { RateLimiterMemory } = require('rate-limiter-flexible'); const ServerBootstrapper = { processCLIArgs: () => { @@ -79,7 +80,7 @@ const ServerBootstrapper = { return main; }, - createSocketServer: (main, app, port) => { + createSocketServer: (main, app, port, logger) => { let io; if (process.env.NODE_ENV.trim() === 'development') { io = require('socket.io')(main, { @@ -91,8 +92,34 @@ const ServerBootstrapper = { }); } - return io.of('/in-game'); + registerRateLimiter(io, logger); + + return io; + }, + + createGameSocketNamespace (server, logger) { + const namespace = server.of('/in-game'); + registerRateLimiter(namespace, logger); + return server.of('/in-game'); } }; +function registerRateLimiter (server, logger) { + const rateLimiter = new RateLimiterMemory( + { + points: 10, + duration: 1 + }); + + server.use(async (socket, next) => { + try { + await rateLimiter.consume(socket.handshake.address); + logger.trace('consumed point from ' + socket.handshake.address); + next(); + } catch (rejection) { + next(new Error('Your connection has been blocked.')); + } + }); +} + module.exports = ServerBootstrapper; diff --git a/spec/unit/server/modules/GameManager_Spec.js b/spec/unit/server/modules/GameManager_Spec.js index 3b958c3..9dd5be8 100644 --- a/spec/unit/server/modules/GameManager_Spec.js +++ b/spec/unit/server/modules/GameManager_Spec.js @@ -13,10 +13,10 @@ describe('GameManager', () => { beforeAll(() => { spyOn(logger, 'debug'); spyOn(logger, 'error'); - gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION).getInstance(); + const inObj = { emit: () => {} }; namespace = { in: () => { return inObj; } }; - gameManager.namespace = namespace; + gameManager = new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, namespace).getInstance(); }); beforeEach(() => {