yuken пре 3 година
родитељ
комит
ee29e88285

+ 6 - 0
packages/app/src/server/interfaces/api-rate-limit-config.ts

@@ -0,0 +1,6 @@
+export type IApiRateLimitConfig = {
+  [endpoint: string]: {
+    method: string,
+    consumePoints: number
+  }
+}

+ 12 - 18
packages/app/src/server/middlewares/api-rate-limiter.ts

@@ -3,16 +3,10 @@ import { RateLimiterMemory } from 'rate-limiter-flexible';
 
 import loggerFactory from '~/utils/logger';
 
-import getCustomApiRateLimit from '../util/getCustomApiRateLimit';
-
+import { IApiRateLimitConfig } from '../interfaces/api-rate-limit-config';
 
 const logger = loggerFactory('growi:middleware:api-rate-limit');
 
-// e.g.
-// API_RATE_LIMIT_010_FOO_ENDPOINT=/_api/v3/foo
-// API_RATE_LIMIT_010_FOO_METHODS=GET,POST
-// API_RATE_LIMIT_010_FOO_CONSUME_POINTS=10
-
 const consumePoints = async(rateLimiter: RateLimiterMemory, key: string, points: number, next: NextFunction) => {
   await rateLimiter.consume(key, points)
     .then(() => {
@@ -23,25 +17,25 @@ const consumePoints = async(rateLimiter: RateLimiterMemory, key: string, points:
     });
 };
 
-module.exports = (rateLimiter: RateLimiterMemory, defaultPoints: number, envVarDicForApiRateLimiter: {[key: string]: string}) => {
+module.exports = (rateLimiter: RateLimiterMemory, defaultPoints: number, apiRateLimitConfig: IApiRateLimitConfig) => {
 
   return async(req: Request, res: Response, next: NextFunction) => {
 
     const endpoint = req.path;
     const key = req.ip + req.url;
 
-    const matchedEndpointKeys = Object.keys(envVarDicForApiRateLimiter).filter((key) => {
-      return envVarDicForApiRateLimiter[key] === endpoint;
+    let points;
+    Object.keys(apiRateLimitConfig).forEach((endpointInConfig) => {
+      if (endpointInConfig === endpoint) {
+        const consumePointsInConfig = apiRateLimitConfig[endpointInConfig].consumePoints;
+        points = consumePointsInConfig;
+      }
+      else {
+        points = defaultPoints;
+      }
     });
 
-    if (matchedEndpointKeys.length === 0) {
-      await consumePoints(rateLimiter, key, defaultPoints, next);
-      return;
-    }
-
-    const customizedConsumePoints = getCustomApiRateLimit(matchedEndpointKeys, req.method, envVarDicForApiRateLimiter);
-
-    await consumePoints(rateLimiter, key, customizedConsumePoints ?? defaultPoints, next);
+    await consumePoints(rateLimiter, key, points, next);
     return;
   };
 };

+ 4 - 4
packages/app/src/server/routes/index.js

@@ -9,7 +9,7 @@ import * as registerFormValidator from '../middlewares/register-form-validator';
 import {
   generateUnavailableWhenMaintenanceModeMiddleware, generateUnavailableWhenMaintenanceModeMiddlewareForApi,
 } from '../middlewares/unavailable-when-maintenance-mode';
-import generateEnvVarDicForApiRateLimiter from '../util/generateEnvVarDicForApiRateLimiter';
+import { generateApiRateLimitConfig } from '../util/generateApiRateLimitConfig';
 
 
 import * as allInAppNotifications from './all-in-app-notifications';
@@ -29,8 +29,8 @@ const opts = {
 };
 const rateLimiter = new RateLimiterMemory(opts);
 
-// generate EnvVarDic for api rate limiter
-const envVarDicForApiRateLimiter = generateEnvVarDicForApiRateLimiter();
+// generate ApiRateLimitConfig for api rate limiter
+const apiRateLimitConfig = generateApiRateLimitConfig();
 
 autoReap.options.reapOnError = true; // continue reaping the file even if an error occurs
 
@@ -45,7 +45,7 @@ module.exports = function(crowi, app) {
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
   const csrf = require('../middlewares/csrf')(crowi);
   const injectUserUISettings = require('../middlewares/inject-user-ui-settings-to-localvars')();
-  const apiRateLimiter = require('../middlewares/api-rate-limiter')(rateLimiter, defaultConsumePoints, envVarDicForApiRateLimiter);
+  const apiRateLimiter = require('../middlewares/api-rate-limiter')(rateLimiter, defaultConsumePoints, apiRateLimitConfig);
 
   const uploads = multer({ dest: `${crowi.tmpDir}uploads` });
   const page = require('./page')(crowi, app);

+ 88 - 0
packages/app/src/server/util/generateApiRateLimitConfig.ts

@@ -0,0 +1,88 @@
+// API_RATE_LIMIT_010_FOO_ENDPOINT=/_api/v3/foo
+// API_RATE_LIMIT_010_FOO_METHODS=GET,POST
+// API_RATE_LIMIT_010_FOO_CONSUME_POINTS=10
+
+export type ApiRateLimitConfig = {
+  [endpoint: string]: {
+    method: string,
+    consumePoints: number
+  }
+}
+
+const getKeyByValue = (object: Record<string, string>, value: string): string | undefined => {
+  return Object.keys(object).find(key => object[key] === value);
+};
+
+const getHighPriorityKey = (key1: string, key2: string): string => {
+  const key1Target = key1.replace('API_RATE_LIMIT_', '').replace('_ENDPOINT', '');
+  const key1Priority = Number(key1Target.split('_')[0]);
+
+  const key2Target = key2.replace('API_RATE_LIMIT_', '').replace('_ENDPOINT', '');
+  const key2Priority = Number(key2Target.split('_')[0]);
+
+  if (key1Priority > key2Priority) {
+    return key1;
+  }
+
+  return key2;
+};
+
+// this method is called only one server starts
+export const generateApiRateLimitConfig = (): ApiRateLimitConfig => {
+  const envVar = process.env;
+
+  const apiRateEndpointKeys = Object.keys(envVar).filter((key) => {
+    const endpointRegExp = /^API_RATE_LIMIT_.*_ENDPOINT/;
+    return endpointRegExp.test(key);
+  });
+
+  // pick up API_RATE_LIMIT_*_ENDPOINT from ENV
+  const envVarEndpoint: Record<string, string> = {};
+  apiRateEndpointKeys.forEach((key) => {
+    const value = envVar[key];
+    if (value === undefined) { return }
+    envVarEndpoint[key] = value;
+  });
+
+
+  // filter the same endpoint configs
+  const envVarEndpointFiltered: Record<string, string> = {};
+  apiRateEndpointKeys.forEach((key) => {
+    const endpointValue = envVarEndpoint[key];
+    if (endpointValue === undefined) { return }
+    if (Object.values(envVarEndpoint).includes(endpointValue)) {
+      const existingKey = getKeyByValue(envVarEndpoint, endpointValue);
+      if (existingKey === undefined) { return }
+      const highPriorityKey = getHighPriorityKey(key, existingKey);
+      envVarEndpointFiltered[highPriorityKey] = endpointValue;
+    }
+    else {
+      envVarEndpointFiltered[key] = endpointValue;
+    }
+  });
+
+  const apiRateLimitConfig: ApiRateLimitConfig = {};
+  Object.keys(envVarEndpointFiltered).forEach((key) => {
+    const target = key.replace('API_RATE_LIMIT_', '').replace('_ENDPOINT', '');
+    const endpoint = envVarEndpointFiltered[`API_RATE_LIMIT_${target}_ENDPOINT`];
+    const method = envVar[`API_RATE_LIMIT_${target}_METHODS`];
+    const consumePoints = Number(envVar[`API_RATE_LIMIT_${target}_CONSUME_POINTS`]);
+
+    if (endpoint === undefined || method === undefined || consumePoints) { return }
+
+    const config = {
+      method,
+      consumePoints,
+    };
+
+    apiRateLimitConfig[endpoint] = config;
+  });
+
+  // default setting e.g. healthchack
+  apiRateLimitConfig['/_api/v3/healthcheck'] = {
+    method: 'GET',
+    consumePoints: 0,
+  };
+
+  return apiRateLimitConfig;
+};

+ 0 - 31
packages/app/src/server/util/generateEnvVarDicForApiRateLimiter.ts

@@ -1,31 +0,0 @@
-// API_RATE_LIMIT_010_FOO_ENDPOINT=/_api/v3/foo
-// API_RATE_LIMIT_010_FOO_METHODS=GET,POST
-// API_RATE_LIMIT_010_FOO_CONSUME_POINTS=10
-
-const generateEnvVarDicForApiRateLimiter = (): {[key: string]: string} => {
-  const envVarDic = process.env;
-
-  // pick up API_RATE_LIMIT_* from ENV
-  const apiRateEndpointKeys = Object.keys(envVarDic).filter((key) => {
-    const endpointRegExp = /^API_RATE_LIMIT_.*/;
-    return endpointRegExp.test(key);
-  });
-
-  const apiRateEndpointDic: {[key: string]: string} = {};
-  apiRateEndpointKeys.forEach((key) => {
-    const value = envVarDic[key];
-    if (value != null) {
-      apiRateEndpointDic[key] = value;
-    }
-  });
-
-  // default setting e.g. healthchack
-  apiRateEndpointDic.API_RATE_LIMIT_010_HEALTHCHECK_ENDPOINT = '/_api/v3/healthcheck';
-  apiRateEndpointDic.API_RATE_LIMIT_010_HEALTHCHECK_METHODS = 'GET';
-  apiRateEndpointDic.API_RATE_LIMIT_010_HEALTHCHECK_CONSUME_POINTS = '0';
-
-
-  return apiRateEndpointDic;
-};
-
-export default generateEnvVarDicForApiRateLimiter;

+ 0 - 31
packages/app/src/server/util/getCustomApiRateLimit.ts

@@ -1,31 +0,0 @@
-const getCustomApiRateLimit = (matchedEndpointKeys: string[], method: string, envVarDic: {[key: string]: string}): number | null => {
-
-  let prioritizedTarget: [string, string] | null = null; // priprity and keyword
-  matchedEndpointKeys.forEach((key) => {
-    const target = key.replace('API_RATE_LIMIT_', '').replace('_ENDPOINT', '');
-    const priority = target.split('_')[0];
-    const keyword = target.split('_')[1];
-    if (prioritizedTarget === null || Number(priority) > Number(prioritizedTarget[0])) {
-      prioritizedTarget = [priority, keyword];
-    }
-  });
-
-  if (prioritizedTarget === null) {
-    return null;
-  }
-
-  const targetMethodsKey = `API_RATE_LIMIT_${prioritizedTarget[0]}_${prioritizedTarget[1]}_METHODS`;
-  const targetConsumePointsKey = `API_RATE_LIMIT_${prioritizedTarget[0]}_${prioritizedTarget[1]}_CONSUME_POINTS`;
-
-  const targetMethods = envVarDic[targetMethodsKey];
-  if (targetMethods === undefined || !targetMethods.includes(method)) {
-    return null;
-  }
-
-  const customizedConsumePoints = envVarDic[targetConsumePointsKey];
-
-  return Number(customizedConsumePoints);
-
-};
-
-export default getCustomApiRateLimit;