RateLimiter.js 4.0 KB

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