From ad5fbbdd950596454e4bef26ba1ea93fa1b35cd8 Mon Sep 17 00:00:00 2001 From: Tanya Byrne Date: Tue, 18 Aug 2020 18:05:59 +0100 Subject: [PATCH] update env --- api/src/cli/utils/create-env/index.ts | 2 + api/src/middleware/cache.ts | 64 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 api/src/middleware/cache.ts diff --git a/api/src/cli/utils/create-env/index.ts b/api/src/cli/utils/create-env/index.ts index 6c7aa4222e..8272b119ac 100644 --- a/api/src/cli/utils/create-env/index.ts +++ b/api/src/cli/utils/create-env/index.ts @@ -31,6 +31,8 @@ const defaults = { REDIS_PASSWORD: null, }, rateLimits: { + RATE_LIMIT_TYPE: 'redis', + CACHE_TYPE: 'redis', CONSUMED_POINTS_LIMIT: 5, CONSUMED_RESET_DURATION: 1, EXEC_EVENLY: true, diff --git a/api/src/middleware/cache.ts b/api/src/middleware/cache.ts new file mode 100644 index 0000000000..66ecfa5fa4 --- /dev/null +++ b/api/src/middleware/cache.ts @@ -0,0 +1,64 @@ +/** + * RateLimiter using Redis + * and rate-limiter-flexible + * can extend with further options + * in future + */ +import { RequestHandler } from 'express'; +import redis from 'redis'; +import { RateLimiterRedis } from 'rate-limiter-flexible'; +import { HitRateLimitException } from '../exceptions'; +import { RedisNotFoundException } from '../exceptions'; +import env from '../env'; + +const redisClient = redis.createClient({ + enable_offline_queue: false, + host: env.REDIS_HOST, + port: env.REDIS_PORT, + password: env.REDIS_PASSWORD, +}); + +const rateLimiter: RequestHandler = (req, res, next) => { + try { + // first need to check that redis is running! + if (!redisClient) { + throw new RedisNotFoundException('Redis client does not exist'); + } + // options for the rate limiter are set below. Opts can be found + // at https://github.com/animir/node-rate-limiter-flexible/wiki/Options + const opts = { + storeClient: redisClient, + points: 5, // Number of points + duration: 5, // Number of seconds before consumed points are reset. + + // Custom + execEvenly: true, // delay actions after first action - this may need adjusting (leaky bucket) + blockDuration: 0, // Do not block if consumed more than points + keyPrefix: 'rlflx', // must be unique for limiters with different purpose + }; + + const rateLimiterRedis = new RateLimiterRedis(opts); + + rateLimiterRedis + .consume(req.ip) + .then((rateLimiterRes) => { + // everything is ok - can put addition logic in there later for users etc + next(); + }) + .catch((rejRes) => { + if (rejRes instanceof RedisNotFoundException) { + throw new RedisNotFoundException('Redis insurance limiter not set up'); + } else { + // If there is no error, rateLimiterRedis promise rejected with number of ms before next request allowed + const secs = Math.round(rejRes.msBeforeNext / 1000) || 1; + res.set('Retry-After', String(secs)); + res.status(429).send('Too Many Requests'); + throw new HitRateLimitException(`Too many requests, retry after ${secs}.`); + } + }); + } catch (error) { + throw error; + } +}; + +export default rateLimiter;