diff --git a/server/api/AdminAPI.js b/server/api/AdminAPI.js index 394c581..ee4e603 100644 --- a/server/api/AdminAPI.js +++ b/server/api/AdminAPI.js @@ -6,33 +6,9 @@ const socketManager = new (require('../modules/SocketManager.js'))().getInstance const gameManager = new (require('../modules/GameManager.js'))().getInstance(); const globals = require('../config/globals.js'); const cors = require('cors'); -const rateLimit = require('express-rate-limit').default; - -const KEY = process.env.NODE_ENV.trim() === 'development' - ? globals.MOCK_AUTH - : process.env.ADMIN_KEY; - -const apiLimiter = rateLimit({ - windowMs: 60000, - max: 50, - standardHeaders: true, - legacyHeaders: false -}); - -if (process.env.NODE_ENV.trim() === 'production') { - router.use(apiLimiter); -} router.use(cors(globals.CORS)); -router.use((req, res, next) => { - if (isAuthorized(req)) { - next(); - } else { - res.status(401).send('You are not authorized to make this request.'); - } -}); - router.post('/sockets/broadcast', (req, res, next) => { globals.CONTENT_TYPE_VALIDATOR(req, res, next); }); @@ -52,16 +28,4 @@ router.get('/games/state', function (req, res) { res.status(200).send(gamesArray); }); -/* validates Bearer Auth */ -function isAuthorized (req) { - const header = req.headers.authorization; - if (header) { - const token = header.split(/\s+/).pop() || ''; - const decodedToken = Buffer.from(token, 'base64').toString(); - return decodedToken.trim() === KEY.trim(); - } - - return false; -} - module.exports = router; diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index 8c1344b..5a800fd 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -9,16 +9,13 @@ const cors = require('cors'); const gameManager = new GameManager().getInstance(); -const apiLimiter = rateLimit({ - windowMs: 60000, - max: 100, - standardHeaders: true, - legacyHeaders: false -}); +const gameCreationLimit = process.env.NODE_ENV.trim() === 'production' + ? 20 + : 1000; -const gameEndpointLimiter = rateLimit({ // further limit the rate of game creation to 30 games per 10 minutes. +const gameEndpointLimiter = rateLimit({ windowMs: 600000, - max: 30, + max: gameCreationLimit, standardHeaders: true, legacyHeaders: false }); @@ -38,12 +35,7 @@ router.patch('/restart', (req, res, next) => { globals.CONTENT_TYPE_VALIDATOR(req, res, next); }); -if (process.env.NODE_ENV.trim() === 'production') { - router.use(apiLimiter); - router.use('/create', gameEndpointLimiter); -} - -router.post('/create', function (req, res) { +router.post('/create', gameEndpointLimiter, function (req, res) { logger.debug('Received request to create new game: ' + JSON.stringify(req.body, null, 4)); const gameCreationPromise = gameManager.createGame(req.body, false); gameCreationPromise.then((result) => { diff --git a/server/modules/ServerBootstrapper.js b/server/modules/ServerBootstrapper.js index 17b65d2..d831068 100644 --- a/server/modules/ServerBootstrapper.js +++ b/server/modules/ServerBootstrapper.js @@ -6,7 +6,9 @@ const fs = require('fs'); const crypto = require('crypto'); const SocketManager = require('./SocketManager.js'); const GameManager = require('./GameManager.js'); +const globals = require('../config/globals.js'); const { ENVIRONMENT } = require('../config/globals.js'); +const rateLimit = require('express-rate-limit').default; const ServerBootstrapper = { @@ -93,22 +95,33 @@ const ServerBootstrapper = { }, establishRouting: (app, express) => { - /* api endpoints */ - const games = require('../api/GamesAPI'); - const admin = require('../api/AdminAPI'); - app.use('/api/games', games); - app.use('/api/admin', admin); + const standardRateLimit = rateLimit({ + windowMs: 60000, + max: 100, + standardHeaders: true, + legacyHeaders: false + }); - /* serve all the app's pages */ - app.use('/manifest.json', (req, res) => { + // API endpoints + app.use('/api/games', standardRateLimit, require('../api/GamesAPI')); + app.use('/api/admin', (req, res, next) => { + if (isAuthorized(req)) { + next(); + } else { + res.status(401).send('You are not authorized to make this request.'); + } + }, standardRateLimit, require('../api/AdminAPI')); + + // miscellaneous assets + app.use('/manifest.json', standardRateLimit, (req, res) => { res.sendFile(path.join(__dirname, '../../manifest.json')); }); - app.use('/favicon.ico', (req, res) => { + app.use('/favicon.ico', standardRateLimit, (req, res) => { res.sendFile(path.join(__dirname, '../../client/favicon_package/favicon.ico')); }); - app.use('/apple-touch-icon.png', (req, res) => { + app.use('/apple-touch-icon.png', standardRateLimit, (req, res) => { res.sendFile(path.join(__dirname, '../../client/favicon_package/apple-touch-icon.png')); }); @@ -121,14 +134,28 @@ const ServerBootstrapper = { app.use('/images', express.static(path.join(__dirname, '../../client/src/images'))); app.use('/styles', express.static(path.join(__dirname, '../../client/src/styles'))); app.use('/webfonts', express.static(path.join(__dirname, '../../client/src/webfonts'))); - app.use('/robots.txt', (req, res) => { + app.use('/robots.txt', standardRateLimit, (req, res) => { res.sendFile(path.join(__dirname, '../../client/robots.txt')); }); - app.use(function (req, res) { + app.use(standardRateLimit, function (req, res) { res.sendFile(path.join(__dirname, '../../client/src/views/404.html')); }); } }; +function isAuthorized (req) { + const KEY = process.env.NODE_ENV.trim() === 'development' + ? globals.MOCK_AUTH + : process.env.ADMIN_KEY; + const header = req.headers.authorization; + if (header) { + const token = header.split(/\s+/).pop() || ''; + const decodedToken = Buffer.from(token, 'base64').toString(); + return decodedToken.trim() === KEY?.trim(); + } + + return false; +} + module.exports = ServerBootstrapper; diff --git a/server/routes/router.js b/server/routes/router.js index 7ee7263..494e9b9 100644 --- a/server/routes/router.js +++ b/server/routes/router.js @@ -1,32 +1,40 @@ const express = require('express'); const router = express.Router({ strict: true }); const path = require('path'); +const rateLimit = require('express-rate-limit').default; -router.get('/', function (request, response) { +const htmlPageLimiter = rateLimit({ + windowMs: 60000, + max: 100, + standardHeaders: true, + legacyHeaders: false +}); + +router.get('/', htmlPageLimiter, function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/home.html')); }); -router.get('/create', function (request, response) { +router.get('/create', htmlPageLimiter, function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/create.html')); }); -router.get('/join/:code', function (request, response) { +router.get('/join/:code', htmlPageLimiter, function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/join.html')); }); -router.get('/how-to-use', function (request, response) { +router.get('/how-to-use', htmlPageLimiter, function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/how-to-use.html')); }); -router.get('/game/:code', function (request, response) { +router.get('/game/:code', htmlPageLimiter, function (request, response) { response.sendFile(path.join(__dirname, '../../client/src/views/game.html')); }); -router.get('/liveness_check', (req, res) => { +router.get('/liveness_check', htmlPageLimiter, (req, res) => { res.sendStatus(200); }); -router.get('/readiness_check', (req, res) => { +router.get('/readiness_check', htmlPageLimiter, (req, res) => { res.sendStatus(200); });