RateLimiter.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. const Settings = require('@overleaf/settings')
  2. const Metrics = require('@overleaf/metrics')
  3. const logger = require('@overleaf/logger')
  4. const RedisWrapper = require('./RedisWrapper')
  5. const RateLimiterFlexible = require('rate-limiter-flexible')
  6. const OError = require('@overleaf/o-error')
  7. const rclient = RedisWrapper.client('ratelimiter')
  8. /**
  9. * Wrapper over the RateLimiterRedis class
  10. */
  11. class RateLimiter {
  12. /**
  13. * Create a rate limiter.
  14. *
  15. * @param name {string} The name that identifies this rate limiter. Different
  16. * rate limiters must have different names.
  17. * @param opts {object} Options to pass to RateLimiterRedis
  18. *
  19. * Some useful options:
  20. *
  21. * points - number of points that can be consumed over the given duration
  22. * (default: 4)
  23. * subnetPoints - number of points that can be consumed over the given
  24. * duration accross a sub-network. This should only be used
  25. * ip-based rate limits.
  26. * duration - duration of the fixed window in seconds (default: 1)
  27. * blockDuration - additional seconds to block after all points are consumed
  28. * (default: 0)
  29. */
  30. constructor(name, opts = {}) {
  31. this.name = name
  32. this._rateLimiter = new RateLimiterFlexible.RateLimiterRedis({
  33. ...opts,
  34. keyPrefix: `rate-limit:${name}`,
  35. storeClient: rclient,
  36. })
  37. if (opts.subnetPoints) {
  38. this._subnetRateLimiter = new RateLimiterFlexible.RateLimiterRedis({
  39. ...opts,
  40. points: opts.subnetPoints,
  41. keyPrefix: `rate-limit:${name}`,
  42. storeClient: rclient,
  43. })
  44. }
  45. }
  46. async consume(key, points = 1, options = { method: 'unknown' }) {
  47. if (Settings.disableRateLimits) {
  48. // Return a fake result in case it's used somewhere
  49. return {
  50. msBeforeNext: 0,
  51. remainingPoints: 100,
  52. consumedPoints: 0,
  53. isFirstInDuration: false,
  54. }
  55. }
  56. await this.consumeForRateLimiter(this._rateLimiter, key, options, points)
  57. if (options.method === 'ip' && this._subnetRateLimiter) {
  58. const subnetKey = this.getSubnetKeyFromIp(key)
  59. await this.consumeForRateLimiter(
  60. this._subnetRateLimiter,
  61. subnetKey,
  62. options,
  63. points,
  64. 'ip-subnet'
  65. )
  66. }
  67. }
  68. async consumeForRateLimiter(rateLimiter, key, options, points, method) {
  69. try {
  70. const res = await rateLimiter.consume(key, points, options)
  71. return res
  72. } catch (err) {
  73. if (err instanceof Error) {
  74. throw err
  75. } else {
  76. // Only log the first time we exceed the rate limit for a given key and
  77. // duration. This happens when the previous amount of consumed points
  78. // was below the threshold.
  79. if (err.consumedPoints - points <= rateLimiter.points) {
  80. logger.warn({ path: this.name, key }, 'rate limit exceeded')
  81. }
  82. Metrics.inc('rate-limit-hit', 1, {
  83. path: this.name,
  84. method: method || options.method,
  85. })
  86. throw err
  87. }
  88. }
  89. }
  90. getSubnetKeyFromIp(ip) {
  91. if (!/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ip)) {
  92. throw new OError(
  93. 'Cannot generate subnet key as the ip address is not of the expected format.',
  94. { ip }
  95. )
  96. }
  97. return ip.split('.').slice(0, 3).join('.')
  98. }
  99. async delete(key) {
  100. return await this._rateLimiter.delete(key)
  101. }
  102. }
  103. /*
  104. * Shared rate limiters
  105. */
  106. const openProjectRateLimiter = new RateLimiter('open-project', {
  107. points: 15,
  108. duration: 60,
  109. })
  110. // Keep in sync with the can-skip-captcha options.
  111. const overleafLoginRateLimiter = new RateLimiter('overleaf-login', {
  112. points: 20,
  113. subnetPoints: 200,
  114. duration: 60,
  115. })
  116. module.exports = {
  117. RateLimiter,
  118. openProjectRateLimiter,
  119. overleafLoginRateLimiter,
  120. }