From 1d42e691d320ebb7f25f65d6c584c7dd6b409f50 Mon Sep 17 00:00:00 2001 From: Tanya Byrne Date: Wed, 19 Aug 2020 18:10:43 +0100 Subject: [PATCH] node cache service --- api/package-lock.json | 8 ++ api/package.json | 1 + api/src/cli/utils/create-env/env-stub.liquid | 6 +- api/src/middleware/cache.ts | 64 --------------- api/src/services/node-cache.ts | 25 ++++++ api/src/utils/get-rate-limiter-config.ts | 83 ++++++++++++++++++++ 6 files changed, 122 insertions(+), 65 deletions(-) delete mode 100644 api/src/middleware/cache.ts create mode 100644 api/src/services/node-cache.ts create mode 100644 api/src/utils/get-rate-limiter-config.ts diff --git a/api/package-lock.json b/api/package-lock.json index f23f4d915f..43c47a888f 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -5294,6 +5294,14 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-exceptions": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/node-exceptions/-/node-exceptions-4.0.1.tgz", diff --git a/api/package.json b/api/package.json index 37bc05761e..27f0fc1de7 100644 --- a/api/package.json +++ b/api/package.json @@ -103,6 +103,7 @@ "lodash": "^4.17.19", "ms": "^2.1.2", "nanoid": "^3.1.12", + "node-cache": "^5.1.2", "nodemailer": "^6.4.11", "ora": "^4.1.1", "otplib": "^12.0.1", diff --git a/api/src/cli/utils/create-env/env-stub.liquid b/api/src/cli/utils/create-env/env-stub.liquid index 913aa8d6d8..9d41703db0 100644 --- a/api/src/cli/utils/create-env/env-stub.liquid +++ b/api/src/cli/utils/create-env/env-stub.liquid @@ -14,10 +14,14 @@ {{ redisServer }} #################################################################################################### -# Rate Limiting and caching +# Rate Limiting {{ rateLimits}} #################################################################################################### +# Caching + +{{ caching }} +#################################################################################################### # File Storage diff --git a/api/src/middleware/cache.ts b/api/src/middleware/cache.ts deleted file mode 100644 index 66ecfa5fa4..0000000000 --- a/api/src/middleware/cache.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 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; diff --git a/api/src/services/node-cache.ts b/api/src/services/node-cache.ts new file mode 100644 index 0000000000..123fcf2182 --- /dev/null +++ b/api/src/services/node-cache.ts @@ -0,0 +1,25 @@ +/** node cache service. + * Makes dealing with cache management easier + * in case a new cache is used in future + * sdtTTL is the amount of time a cache should be + * stored, in seconds, before being deleted. Keep + * this is mind + */ +import NodeCache from 'node-cache'; + +export default class NodeCacheService { + apiCache: NodeCache; + + constructor(stdTTLSecs: number, checkPeriodSecs: number) { + // options found at https://github.com/node-cache/node-cache + this.apiCache = new NodeCache({ + stdTTL: stdTTLSecs, + checkperiod: checkPeriodSecs, + useClones: false, + }); + } + + async flushCache() { + this.apiCache.flushAll(); + } +} diff --git a/api/src/utils/get-rate-limiter-config.ts b/api/src/utils/get-rate-limiter-config.ts new file mode 100644 index 0000000000..624256c257 --- /dev/null +++ b/api/src/utils/get-rate-limiter-config.ts @@ -0,0 +1,83 @@ +import redis from 'redis'; +import { + RateLimiterRedis, + RateLimiterMemory, + IRateLimiterStoreOptions, + IRateLimiterOptions, +} from 'rate-limiter-flexible'; +import { RedisNotFoundException } from '../exceptions'; +import env from '../env'; + +// options for the rate limiter are set below. Opts can be found +// at https://github.com/animir/node-rate-limiter-flexible/wiki/Options + +let rateLimiterConfig = new RateLimiterMemory(getRateLimiterConfig()); +// need to pick redis or memory +if (env.RATE_LIMIT_TYPE === 'redis') { + rateLimiterConfig = new RateLimiterRedis(getRateLimiterRedisConfig()); +} + +export default rateLimiterConfig; + +function getRateLimiterConfig(): IRateLimiterOptions { + const config: any = {}; + config.keyPrefix = 'rlflx'; + for (const [key, value] of Object.entries(env)) { + if (key === 'CONSUMED_POINTS_LIMIT') { + config.points = value; + continue; + } + if (key === 'CONSUMED_RESET_DURATION') { + config.duration = value; + continue; + } + } + + return config; +} + +function getRateLimiterRedisConfig(): IRateLimiterStoreOptions { + const redisConfig: any = {}; + const redisClient = redis.createClient({ + enable_offline_queue: false, + host: env.REDIS_HOST, + port: env.REDIS_PORT, + password: env.REDIS_PASSWORD, + }); + + if (!redisClient) { + throw new RedisNotFoundException('Redis client does not exist'); + } + + redisConfig.keyPrefix = 'rlflx'; + redisConfig.storeClient = redisClient; + + for (const [key, value] of Object.entries(env)) { + if (key === 'CONSUMED_POINTS_LIMIT') { + redisConfig.points = value; + continue; + } + if (key === 'CONSUMED_RESET_DURATION') { + redisConfig.duration = value; + continue; + } + if (key === 'EXEC_EVENLY') { + redisConfig.execEvenly = value; + continue; + } + if (key === 'BLOCK_POINT_DURATION') { + redisConfig.blockDuration = value; + continue; + } + if (key === 'INMEMORY_BLOCK_CONSUMED') { + redisConfig.inmemoryBlockOnConsumed = value; + continue; + } + if (key === 'INMEMEMORY_BLOCK_DURATION') { + redisConfig.inmemoryBlockDuration = value; + continue; + } + } + + return redisConfig; +}