diff --git a/package-lock.json b/package-lock.json index fdb9257..faa86ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,12 +93,29 @@ } } }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, "@types/engine.io": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.4.tgz", @@ -107,6 +124,27 @@ "@types/node": "*" } }, + "@types/express": { + "version": "4.17.8", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.8.tgz", + "integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz", + "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, "@types/fs-extra": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz", @@ -133,6 +171,11 @@ "@types/lodash": "*" } }, + "@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + }, "@types/mocha": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", @@ -151,6 +194,25 @@ "@types/node": "*" } }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz", + "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "@types/socket.io": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.10.tgz", diff --git a/package.json b/package.json index 6be33db..efb53b2 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "author": "frontblock.me", "license": "ISC", "dependencies": { + "@types/express": "^4.17.8", "@types/mocha": "^5.2.7", "@types/node": "^14.0.27", "bsert": "0.0.10", diff --git a/src/backend/Admin/Admin.ts b/src/backend/Admin/Admin.ts index 37fcaf3..0ec0676 100644 --- a/src/backend/Admin/Admin.ts +++ b/src/backend/Admin/Admin.ts @@ -15,12 +15,14 @@ import { CharacterManager } from '../Components/Character/CharacterManager'; import { UserManager } from '../Components/User/UserManager'; import { RootComponent } from '../Injector/ServiceDecorator'; import { TableDefinitionExporter } from '../Types/Interfaces'; -import { AdminConf, TableDefiniton } from '../Types/Types'; +import { AdminConf, TableDefiniton, SomeOf } from '../Types/Types'; import { RPCConfigLoader } from '../Components/RPCConfigLoader'; import { FrontworkComponent } from '../Types/FrontworkComponent'; import { IAdmin } from './Interface'; import { Injector } from '../Injector/Injector'; import { PubSub } from '../Components/PubSub/PubSub'; +import { BurstyRateLimiter, RateLimiterMemory, RateLimiterAbstract } from 'rate-limiter-flexible' + @RootComponent({ injectable: IAdmin, @@ -67,13 +69,24 @@ export class FrontworkAdmin async start() { let port: number = this.config.getConfig().httpPort + const rateLimiter = new BurstyRateLimiter( + new RateLimiterMemory({ + points: 2, + duration: 1, + }), + new RateLimiterMemory({ + keyPrefix: 'burst', + points: 60, + duration: 60, + }) + ) await this.makeKnex() - const app = await this.makeExpress() + const app = await this.makeExpress(rateLimiter) const httpServer = new http.Server(app) await this.startWebsocket(httpServer) httpServer.listen(port) - await this.attachAngularSSR(app) + await this.attachAngularSSR(app, rateLimiter) getLogger('Admin#startWebsocket').debug("Webserver up on", port) await Promise.all(this.frontworkComponents.map(c => c.initialize ? c.initialize() : undefined)) @@ -125,51 +138,53 @@ export class FrontworkAdmin } ], { errorHandler: (sock, err, rpc, args) => { - console.log("RPC", rpc) - console.log("ERR", err) - console.log("ARGS", args) + getLogger("startWebsocket#errorHandler").error("RPC", rpc) + getLogger("startWebsocket#errorHandler").error("ERR", err) + getLogger("startWebsocket#errorHandler").error("ARGS", args) } }) .attach(httpServer) } - private async makeExpress() { + private async makeExpress(rateLimiter : SomeOf = new RateLimiterMemory({ points: 6, duration: 3 })) { if (this.httpServer != null || this.express != null) { getLogger('Admin#startWebserver').warn("Webserver is already running") return } this.express = express() - const distFolder = "../../../../dist" - this.express.get('*.*', (req, res) => { - //console.log('*.*', req.path); - try{ - const filepath = Path.join(__dirname, distFolder, 'browser', decodeURIComponent(req.path)) - if(!existsSync(filepath)){ - throw new Error("FAILED REQUEST "+filepath+ " FROM "+req.ip) - } - res.sendFile(filepath) - res.status(200) - }catch(e){ - getLogger('Admin#startWebserver#serveFile').error(String(e)) - res.send("404 NOT FOUND") - res.status(404) - } + rateLimiter.consume(req.ip) + .then(_ => { + const filepath = Path.join(__dirname, distFolder, 'browser', decodeURIComponent(req.path)) + if(!existsSync(filepath)){ + getLogger('Admin#startWebserver#serveFile').error(String(new Error("404 BAD REQUEST "+filepath+ " FROM "+req.ip))) + res.send("404 Not found") + res.status(404) + return + } + res.sendFile(filepath) + res.status(200) + }) + .catch(_ => { + getLogger('Admin#startWebserver#serveFile').error(String(new Error("429 BAD REQUEST "+req.path+" FROM "+req.ip))) + res.send('429 Too many requests') + res.status(429) + }) }) return this.express } - attachAngularSSR = async (express : express.Application) => { + attachAngularSSR = async (express : express.Application, rateLimiter : SomeOf = new RateLimiterMemory({ points: 6, duration: 3 })) => { const distFolder = "../../../../dist" const ngExpressServer = Path.join(__dirname, distFolder, 'server.js') let port: number = this.config.getConfig().httpPort try { const req = require(distFolder + "/server.js") - await req.attachExpress(express, './dist', getLogger('angularSSR#')) + await req.attachExpress(express, './dist', getLogger('angularSSR#'), rateLimiter) getLogger('Admin#startWebserver').debug('Frontend from ' + ngExpressServer + " loaded") } catch (e) { getLogger('Admin#startWebserver').error(e) diff --git a/src/frontend/server.ts b/src/frontend/server.ts index f48d510..b1f2863 100644 --- a/src/frontend/server.ts +++ b/src/frontend/server.ts @@ -15,15 +15,21 @@ global['alert'] = console.log global['XMLHttpRequest'] = require('xmlhttprequest').XMLHttpRequest; global['fetch'] = fetch.default; - - -export async function attachExpress(app, staticDir = "./dist", logger = console) { +/** + * Function to attach this SSR application to an express server. Exported as function due to webpack inconsistencies. + * + * @param app express.Application to run on + * @param staticDir Where the static website content is (i.e. ./dist) + * @param logger A loggerservice or console + * @param rateLimiter a rate-limiter-flexible instance + */ +export async function attachExpress(app, staticDir, logger, rateLimiter) { const STATIC_FOLDER = resolve(process.cwd(), staticDir); enableProdMode(); const loggerService = { - warn: (...args) => logger.error(...args), - error: (...args) => logger.warn(...args), + warn: (...args) => logger.warn(...args), + error: (...args) => logger.error(...args), log: (...args) => logger.info(...args), table: (...args) => logger.info(...args), collapsed: (msg, type, ...args) => loggerService[type](msg, ...args) @@ -57,11 +63,14 @@ export async function attachExpress(app, staticDir = "./dist", logger = console) ); app.get('*', (req, res) => { - loggerService.log("REQUEST "+req.path +" FROM "+ req.ip) - try{ + rateLimiter.consume(req.ip, 3) + .then(_ => { res.render(resolve(STATIC_FOLDER, 'browser/index'), { req, res }) - }catch(e){ - loggerService.error(e) - } - }); + }) + .catch(_ => { + loggerService.error(String(new Error("429 BAD REQUEST "+req.path+" FROM "+req.ip))) + res.send("429 Too many requests") + res.status(429) + }) + }) }