factory.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import type { IUserHasId } from '@growi/core';
  2. import type { Handler, Request } from 'express';
  3. import md5 from 'md5';
  4. import { connection } from 'mongoose';
  5. import { type IRateLimiterMongoOptions, type RateLimiterRes, RateLimiterMongo } from 'rate-limiter-flexible';
  6. import loggerFactory from '~/utils/logger';
  7. import {
  8. DEFAULT_DURATION_SEC, DEFAULT_MAX_REQUESTS, DEFAULT_USERS_PER_IP_PROSPECTION, type IApiRateLimitConfig,
  9. } from '../config';
  10. import { generateApiRateLimitConfig } from '../utils/config-generator';
  11. const logger = loggerFactory('growi:middleware:api-rate-limit');
  12. // config sample
  13. // API_RATE_LIMIT_010_FOO_ENDPOINT=/_api/v3/foo
  14. // API_RATE_LIMIT_010_FOO_METHODS=GET,POST
  15. // API_RATE_LIMIT_010_FOO_MAX_REQUESTS=10
  16. const opts: IRateLimiterMongoOptions = {
  17. storeClient: connection,
  18. duration: DEFAULT_DURATION_SEC, // set default value
  19. };
  20. const rateLimiter = new RateLimiterMongo(opts);
  21. // generate ApiRateLimitConfig for api rate limiter
  22. const apiRateLimitConfig = generateApiRateLimitConfig();
  23. const configWithoutRegExp = apiRateLimitConfig.withoutRegExp;
  24. const configWithRegExp = apiRateLimitConfig.withRegExp;
  25. const allRegExp = new RegExp(Object.keys(configWithRegExp).join('|'));
  26. const keysWithRegExp = Object.keys(configWithRegExp).map(key => new RegExp(`^${key}`));
  27. const valuesWithRegExp = Object.values(configWithRegExp);
  28. export const _consumePoints = async(
  29. method: string, key: string | null, customizedConfig?: IApiRateLimitConfig, maxRequestsMultiplier?: number,
  30. ): Promise<RateLimiterRes | undefined> => {
  31. if (key == null) {
  32. return;
  33. }
  34. let maxRequests = DEFAULT_MAX_REQUESTS;
  35. // use customizedConfig
  36. if (customizedConfig != null && (customizedConfig.method.includes(method) || customizedConfig.method === 'ALL')) {
  37. maxRequests = customizedConfig.maxRequests;
  38. }
  39. // multiply
  40. if (maxRequestsMultiplier != null) {
  41. maxRequests *= maxRequestsMultiplier;
  42. }
  43. rateLimiter.points = maxRequests;
  44. const rateLimiterRes = await rateLimiter.consume(key, 1);
  45. return rateLimiterRes;
  46. };
  47. /**
  48. * consume per user per endpoint
  49. * @param method
  50. * @param key
  51. * @param customizedConfig
  52. * @returns
  53. */
  54. const consumePointsByUser = async(method: string, key: string | null, customizedConfig?: IApiRateLimitConfig): Promise<RateLimiterRes | undefined> => {
  55. return _consumePoints(method, key, customizedConfig);
  56. };
  57. /**
  58. * consume per ip per endpoint
  59. * @param method
  60. * @param key
  61. * @param customizedConfig
  62. * @returns
  63. */
  64. const consumePointsByIp = async(method: string, key: string | null, customizedConfig?: IApiRateLimitConfig): Promise<RateLimiterRes | undefined> => {
  65. const maxRequestsMultiplier = customizedConfig?.usersPerIpProspection ?? DEFAULT_USERS_PER_IP_PROSPECTION;
  66. return _consumePoints(method, key, customizedConfig, maxRequestsMultiplier);
  67. };
  68. export const middlewareFactory = (): Handler => {
  69. return async(req: Request & { user?: IUserHasId }, res, next) => {
  70. const endpoint = req.path;
  71. // determine keys
  72. const keyForUser: string | null = req.user != null
  73. ? md5(`${req.user._id}_${endpoint}_${req.method}`)
  74. : null;
  75. const keyForIp: string = md5(`${req.ip}_${endpoint}_${req.method}`);
  76. // determine customized config
  77. let customizedConfig: IApiRateLimitConfig | undefined;
  78. const configForEndpoint = configWithoutRegExp[endpoint];
  79. if (configForEndpoint) {
  80. customizedConfig = configForEndpoint;
  81. }
  82. else if (allRegExp.test(endpoint)) {
  83. keysWithRegExp.forEach((key, index) => {
  84. if (key.test(endpoint)) {
  85. customizedConfig = valuesWithRegExp[index];
  86. }
  87. });
  88. }
  89. // check for the current user
  90. if (req.user != null) {
  91. try {
  92. await consumePointsByUser(req.method, keyForUser, customizedConfig);
  93. }
  94. catch {
  95. logger.error(`${req.user._id}: too many request at ${endpoint}`);
  96. return res.sendStatus(429);
  97. }
  98. }
  99. // check for ip
  100. try {
  101. await consumePointsByIp(req.method, keyForIp, customizedConfig);
  102. }
  103. catch {
  104. logger.error(`${req.ip}: too many request at ${endpoint}`);
  105. return res.sendStatus(429);
  106. }
  107. return next();
  108. };
  109. };