operations_rate_limiter.rb 1.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class OperationsRateLimiter
  3. class ThrottleLimitExceeded < Exceptions::Forbidden; end
  4. def initialize(limit:, period:, operation:)
  5. @limit = limit
  6. @period = period
  7. @operation = operation
  8. end
  9. def ensure_within_limits!(by_ip:, by_identifier: nil)
  10. ensure_within_identifier_limit(by_identifier) if by_identifier.present?
  11. ensure_within_ip_limit(by_ip)
  12. end
  13. private
  14. def ensure_within_identifier_limit(value)
  15. value = value.downcase.gsub(%r{\s+}, '')
  16. fingerprint = Digest::MD5.hexdigest(value)
  17. ensure_within :identifier, fingerprint
  18. end
  19. def ensure_within_ip_limit(ip_addr)
  20. ensure_within :ip, ip_addr
  21. end
  22. def ensure_within(key, value)
  23. period_identifier, lapsed_time = Time.now.to_i.divmod(@period.to_i)
  24. cache_key = cache_key(key, value, period_identifier)
  25. expires_in = cache_expires_in(lapsed_time)
  26. value = increment(cache_key, expires_in)
  27. return true if value <= @limit
  28. raise ThrottleLimitExceeded, __('The request limit for this operation was exceeded.')
  29. end
  30. def cache_key(key, value, period_identifier)
  31. [
  32. self.class.name,
  33. @operation,
  34. key,
  35. value,
  36. period_identifier
  37. ].join('::')
  38. end
  39. def cache_expires_in(lapsed_time)
  40. @period.to_i - lapsed_time + 1.minute # make sure there's no race condition with cache expiring during processing
  41. end
  42. def increment(cache_key, expires_in)
  43. # Rails.cache.increment has surpising behaviours/bugs in Rails 7.0, so we don't use it.
  44. # This may be working better in 7.1 and could be cleaned up after upgrading to Rails 7.1
  45. #
  46. # https://github.com/rails/rails/commit/f48bf3975f62e875a1cf4264b18ce3735915684d
  47. new_value = (Rails.cache.read(cache_key) || 0) + 1
  48. new_value.tap do
  49. Rails.cache.write(cache_key, new_value, expires_in:)
  50. end
  51. end
  52. end