diff --git a/api/package-lock.json b/api/package-lock.json index ccbd6a9445..43c47a888f 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -2317,7 +2317,8 @@ "denque": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==", + "optional": true }, "depd": { "version": "1.1.2", @@ -5293,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", @@ -6294,7 +6303,8 @@ "rate-limiter-flexible": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.1.10.tgz", - "integrity": "sha512-Pa+8TPD4xYaiCUB5K4a/+j2FHDUe4HP1g49JmKEmkOkhqPaeVqxJsZuuVaza/svSCOT+V73vtsyBiSFK/e1yXw==" + "integrity": "sha512-Pa+8TPD4xYaiCUB5K4a/+j2FHDUe4HP1g49JmKEmkOkhqPaeVqxJsZuuVaza/svSCOT+V73vtsyBiSFK/e1yXw==", + "optional": true }, "raw-body": { "version": "2.4.0", @@ -6433,6 +6443,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "optional": true, "requires": { "denque": "^1.4.1", "redis-commands": "^1.5.0", @@ -6443,17 +6454,20 @@ "redis-commands": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", - "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==" + "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==", + "optional": true }, "redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=", + "optional": true }, "redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "optional": true, "requires": { "redis-errors": "^1.0.0" } diff --git a/api/package.json b/api/package.json index 94fb925105..27f0fc1de7 100644 --- a/api/package.json +++ b/api/package.json @@ -56,9 +56,9 @@ "dev": "cross-env NODE_ENV=development LOG_LEVEL=trace ts-node-dev --files src/server.ts --respawn --watch \"src/**/*.ts\" --transpile-only", "cli": "cross-env NODE_ENV=development ts-node --script-mode --transpile-only src/cli/index.ts", "prepublishOnly": "npm run build", - "build-windows":"rd /s /q dist 2>nul && tsc -b && copyfiles \"src\\**\\*.*\" -e \"src\\**\\*.ts\" -u 1 dist", - "dev-windows":"cross-env NODE_ENV=development LOG_LEVEL=trace ts-node-dev --files src\\server.ts --respawn --watch \"src\\**\\*.ts\" --transpile-only", - "cli-windows":"cross-env NODE_ENV=development ts-node --script-mode --transpile-only src\\cli\\index.ts", + "build-windows": "rd /s /q dist 2>nul && tsc -b && copyfiles \"src\\**\\*.*\" -e \"src\\**\\*.ts\" -u 1 dist", + "dev-windows": "cross-env NODE_ENV=development LOG_LEVEL=trace ts-node-dev --files src\\server.ts --respawn --watch \"src\\**\\*.ts\" --transpile-only", + "cli-windows": "cross-env NODE_ENV=development ts-node --script-mode --transpile-only src\\cli\\index.ts", "prepublishOnly-windows": "npm run build-windows" }, "files": [ @@ -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 207fc8dc8d..913aa8d6d8 100644 --- a/api/src/cli/utils/create-env/env-stub.liquid +++ b/api/src/cli/utils/create-env/env-stub.liquid @@ -14,7 +14,7 @@ {{ redisServer }} #################################################################################################### -# Rate Limiting +# Rate Limiting and caching {{ rateLimits}} #################################################################################################### diff --git a/api/src/middleware/rate-limiter.ts b/api/src/middleware/rate-limiter.ts index 8281d0bd69..6e0241019e 100644 --- a/api/src/middleware/rate-limiter.ts +++ b/api/src/middleware/rate-limiter.ts @@ -20,33 +20,38 @@ const redisClient = redis.createClient({ }); const rateLimiter: RequestHandler = asyncHandler(async (req, res, next) => { - // 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 + // more basic for memory store const opts = { - storeClient: redisClient, points: env.CONSUMED_POINTS_LIMIT, // Number of points duration: env.CONSUMED_RESET_DURATION, // Number of seconds before consumed points are reset. - - // Custom - execEvenly: env.EXEC_EVENLY, // delay actions after first action - this may need adjusting (leaky bucket) - blockDuration: env.BLOCK_POINT_DURATION, // Do not block if consumed more than points - inmemoryBlockOnConsumed: env.INMEMORY_BLOCK_CONSUMED, // eg if 200 points consumed - inmemoryBlockDuration: env.INMEMEMORY_BLOCK_DURATION, // block for certain amount of seconds keyPrefix: 'rlflx', // must be unique for limiters with different purpose - insuranceLimiter: new RateLimiterMemory({ - points: 20, // 20 is fair if you have 5 workers and 1 cluster - duration: 1, - }), }; - const rateLimiterRedis = new RateLimiterRedis(opts); + let rateLimiterSet = new RateLimiterMemory(opts); + + if (env.RATE_LIMIT_TYPE === 'redis') { + const redisOpts = { + ...opts, + storeClient: redisClient, + // Custom + execEvenly: env.EXEC_EVENLY, // delay actions after first action - this may need adjusting (leaky bucket) + blockDuration: env.BLOCK_POINT_DURATION, // Do not block if consumed more than points + inmemoryBlockOnConsumed: env.INMEMORY_BLOCK_CONSUMED, // eg if 200 points consumed + inmemoryBlockDuration: env.INMEMEMORY_BLOCK_DURATION, // block for certain amount of seconds + }; + + rateLimiterSet = new RateLimiterRedis(redisOpts); + + // first need to check that redis is running! + if (!redisClient) { + throw new RedisNotFoundException('Redis client does not exist'); + } + } try { - await rateLimiterRedis.consume(req.ip); + await rateLimiterSet.consume(req.ip); } catch (rejRes) { // If there is no error, rateLimiterRedis promise rejected with number of ms before next request allowed const secs = Math.round(rejRes.msBeforeNext / 1000) || 1;