Add Rate Limits

- Add rate limits to /graphql and /upload that can be configured in the config
- Change the default response timeout to 30 secons
pull/4/head
trivernis 5 years ago
parent 21e31fad8a
commit 5c3ec38289

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- event and eventCount to UserData gql interface - event and eventCount to UserData gql interface
- joined field to Event gql type - joined field to Event gql type
- joined field to Group gql type - joined field to Group gql type
- rate limits with defaults of 10/min for `/upload` and 30/min for `/graphql`
### Removed ### Removed
@ -39,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- default findUser param limit to 20 - default findUser param limit to 20
- only group admins can create group events - only group admins can create group events
- config behaviour to use all files that reside in the ./config directory with the .toml format - config behaviour to use all files that reside in the ./config directory with the .toml format
- default response timeout from 2 minutes to 30 seconds
### Fixed ### Fixed

@ -14,6 +14,8 @@ connectionUri = "redis://localhost:6379"
port = 8080 port = 8080
# Allow cross origin requests # Allow cross origin requests
cors = false cors = false
# The timeout for a server response
timeout = 30000
# Configuration for the sessions # Configuration for the sessions
[session] [session]
@ -38,3 +40,23 @@ level = "info"
angularIndex = "index.html" angularIndex = "index.html"
# The path to the public files # The path to the public files
publicPath = "./public" publicPath = "./public"
# Configuration for the api
[api]
# if graphiql should be enabled
graphiql = true
# Configuration for the api rate limit
[api.rateLimit]
# rate limit of /upload
[api.rateLimit.upload]
# The time in milliseconds before the rate limit is reset
expire = 60000
# The total number of calls allowed before rate limiting
total = 10
# rate limit of /graphql
[api.rateLimit.graphql]
# The time in milliseconds before the rate limit is reset
expire = 60000
# The total number of calls allowed before rate limiting
total = 30

@ -44,6 +44,8 @@
"@types/socket.io-redis": "^1.0.25", "@types/socket.io-redis": "^1.0.25",
"@types/uuid": "^3.4.6", "@types/uuid": "^3.4.6",
"@types/validator": "^10.11.3", "@types/validator": "^10.11.3",
"@types/config": "^0.0.36",
"@types/redis": "^2.8.14",
"chai": "^4.2.0", "chai": "^4.2.0",
"delete": "^1.1.0", "delete": "^1.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
@ -56,7 +58,6 @@
"typescript": "^3.7.2" "typescript": "^3.7.2"
}, },
"dependencies": { "dependencies": {
"@types/config": "^0.0.36",
"compression": "^1.7.4", "compression": "^1.7.4",
"config": "^3.2.4", "config": "^3.2.4",
"connect-session-sequelize": "^6.0.0", "connect-session-sequelize": "^6.0.0",
@ -65,6 +66,7 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.1.6", "express-fileupload": "^1.1.6",
"express-graphql": "^0.9.0", "express-graphql": "^0.9.0",
"express-limiter": "^1.6.1",
"express-session": "^1.16.2", "express-session": "^1.16.2",
"express-socket.io-session": "^1.3.5", "express-socket.io-session": "^1.3.5",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",

@ -10,9 +10,12 @@ import sharedsession = require("express-socket.io-session");
import * as fsx from "fs-extra"; import * as fsx from "fs-extra";
import {buildSchema} from "graphql"; import {buildSchema} from "graphql";
import {importSchema} from "graphql-import"; import {importSchema} from "graphql-import";
import {IncomingMessage, ServerResponse} from "http";
import * as http from "http"; import * as http from "http";
import * as httpStatus from "http-status"; import * as httpStatus from "http-status";
import * as path from "path"; import * as path from "path";
import {RedisClient} from "redis";
import * as redis from "redis";
import {Sequelize} from "sequelize-typescript"; import {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import * as socketIoRedis from "socket.io-redis"; import * as socketIoRedis from "socket.io-redis";
@ -23,6 +26,7 @@ import HomeRoute from "./routes/HomeRoute";
import {UploadRoute} from "./routes/UploadRoute"; import {UploadRoute} from "./routes/UploadRoute";
const SequelizeStore = require("connect-session-sequelize")(session.Store); const SequelizeStore = require("connect-session-sequelize")(session.Store);
const createLimiter: (...args: any) => any = require("express-limiter");
const logger = globals.logger; const logger = globals.logger;
/** /**
@ -40,6 +44,16 @@ class App {
*/ */
public io: socketIo.Server; public io: socketIo.Server;
/**
* The instance of the redis client
*/
public redisClient: RedisClient;
/**
* The limiter for api requests
*/
public limiter: any;
/** /**
* An instance of the http server where the site is served * An instance of the http server where the site is served
*/ */
@ -63,6 +77,8 @@ class App {
constructor(id?: number) { constructor(id?: number) {
this.id = id; this.id = id;
this.app = express(); this.app = express();
this.redisClient = redis.createClient(null, null, {url: config.get("redis.connectionUri")});
this.limiter = createLimiter(this.app, this.redisClient);
this.server = new http.Server(this.app); this.server = new http.Server(this.app);
this.io = socketIo(this.server); this.io = socketIo(this.server);
this.sequelize = new Sequelize(config.get("database.connectionUri")); this.sequelize = new Sequelize(config.get("database.connectionUri"));
@ -102,6 +118,7 @@ class App {
this.io.use(sharedsession(appSession, {autoSave: true})); this.io.use(sharedsession(appSession, {autoSave: true}));
logger.info("Configuring express app."); logger.info("Configuring express app.");
this.server.setTimeout(config.get("server.timeout"));
this.app.set("views", path.join(__dirname, "views")); this.app.set("views", path.join(__dirname, "views"));
this.app.set("view engine", "pug"); this.app.set("view engine", "pug");
this.app.set("trust proxy", 1); this.app.set("trust proxy", 1);
@ -146,15 +163,37 @@ class App {
await homeRoute.init(this.io); await homeRoute.init(this.io);
this.app.use("/home", homeRoute.router); this.app.use("/home", homeRoute.router);
this.limiter({
expire: config.get("api.rateLimit.upload.expire"),
lookup: ["connection.remoteAddress"],
method: "all",
onRateLimited: (req: IncomingMessage, res: any) => {
res.status(httpStatus.TOO_MANY_REQUESTS);
res.json({error: "Rate Limit Exceeded"});
},
path: "/upload",
total: config.get("api.rateLimit.upload.total"),
});
this.app.use("/upload", uploadRoute.router); this.app.use("/upload", uploadRoute.router);
// listen for graphql requests // listen for graphql requests
this.limiter({
expire: config.get("api.rateLimit.graphql.expire"),
lookup: ["connection.remoteAddress"],
method: "all",
onRateLimited: (req: IncomingMessage, res: any) => {
res.status(httpStatus.TOO_MANY_REQUESTS);
res.json({errors: [{message: "Rate Limit Exceeded"}]});
},
path: "/graphql",
total: config.get("api.rateLimit.graphql.total"),
});
this.app.use("/graphql", graphqlHTTP((request, response) => { this.app.use("/graphql", graphqlHTTP((request, response) => {
return { return {
// @ts-ignore all // @ts-ignore all
context: {session: request.session}, context: {session: request.session},
graphiql: true, graphiql: config.get("api.graphiql"),
rootValue: resolver(request, response), rootValue: resolver(request, response),
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
}; };

File diff suppressed because it is too large Load Diff

@ -212,6 +212,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/redis@^2.8.14":
version "2.8.14"
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.14.tgz#2ed46d0f923f7ccd63fbe73a46a1241e606cf716"
integrity sha512-255dzsOLJdXFHBio9/aMHGozNkoiBUgc+g2nlNjbTSp5qcAlmpm4Z6Xs3pKOBLNIKdZbA2BkUxWvYSIwKra0Yw==
dependencies:
"@types/node" "*"
"@types/sequelize@^4.28.5": "@types/sequelize@^4.28.5":
version "4.28.6" version "4.28.6"
resolved "https://registry.yarnpkg.com/@types/sequelize/-/sequelize-4.28.6.tgz#01d2f1d3781cc34448cd63c2fd97bdb0612b15de" resolved "https://registry.yarnpkg.com/@types/sequelize/-/sequelize-4.28.6.tgz#01d2f1d3781cc34448cd63c2fd97bdb0612b15de"
@ -1745,6 +1752,11 @@ express-graphql@^0.9.0:
http-errors "^1.7.3" http-errors "^1.7.3"
raw-body "^2.4.1" raw-body "^2.4.1"
express-limiter@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/express-limiter/-/express-limiter-1.6.1.tgz#70ede144f9e875e3c7e120644018a6f7784bda71"
integrity sha512-w/Xz/FIHuAOIVIUeHSe6g2rSYTqCSKA9WFLO2CxX15BzEAK+avp7HoYd7pu/M2tEp5E/to253+4x8vQ6WcTJkQ==
express-session@^1.16.2: express-session@^1.16.2:
version "1.17.0" version "1.17.0"
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.0.tgz#9b50dbb5e8a03c3537368138f072736150b7f9b3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.0.tgz#9b50dbb5e8a03c3537368138f072736150b7f9b3"

Loading…
Cancel
Save