RateLimiter.js 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  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 rclient = RedisWrapper.client('ratelimiter')
  7. /**
  8. * Wrapper over the RateLimiterRedis class
  9. */
  10. class RateLimiter {
  11. /**
  12. * Create a rate limiter.
  13. *
  14. * @param name {string} The name that identifies this rate limiter. Different
  15. * rate limiters must have different names.
  16. * @param opts {object} Options to pass to RateLimiterRedis
  17. *
  18. * Some useful options:
  19. *
  20. * points - number of points that can be consumed over the given duration
  21. * (default: 4)
  22. * duration - duration of the fixed window in seconds (default: 1)
  23. * blockDuration - additional seconds to block after all points are consumed
  24. * (default: 0)
  25. */
  26. constructor(name, opts = {}) {
  27. this.name = name
  28. this._rateLimiter = new RateLimiterFlexible.RateLimiterRedis({
  29. ...opts,
  30. keyPrefix: `rate-limit:${name}`,
  31. storeClient: rclient,
  32. })
  33. }
  34. async consume(key, points = 1, options = {}) {
  35. if (Settings.disableRateLimits) {
  36. // Return a fake result in case it's used somewhere
  37. return {
  38. msBeforeNext: 0,
  39. remainingPoints: 100,
  40. consumedPoints: 0,
  41. isFirstInDuration: false,
  42. }
  43. }
  44. try {
  45. const res = await this._rateLimiter.consume(key, points, options)
  46. return res
  47. } catch (err) {
  48. if (err instanceof Error) {
  49. throw err
  50. } else {
  51. // Only log the first time we exceed the rate limit for a given key and
  52. // duration. This happens when the previous amount of consumed points
  53. // was below the threshold.
  54. if (err.consumedPoints - points <= this._rateLimiter.points) {
  55. logger.warn({ path: this.name, key }, 'rate limit exceeded')
  56. }
  57. Metrics.inc('rate-limit-hit', 1, { path: this.name })
  58. throw err
  59. }
  60. }
  61. }
  62. async delete(key) {
  63. return await this._rateLimiter.delete(key)
  64. }
  65. }
  66. /*
  67. * Shared rate limiters
  68. */
  69. const openProjectRateLimiter = new RateLimiter('open-project', {
  70. points: 15,
  71. duration: 60,
  72. })
  73. // Keep in sync with the can-skip-captcha options.
  74. const overleafLoginRateLimiter = new RateLimiter('overleaf-login', {
  75. points: 20,
  76. duration: 60,
  77. })
  78. module.exports = {
  79. RateLimiter,
  80. openProjectRateLimiter,
  81. overleafLoginRateLimiter,
  82. }