mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
66 lines
1.9 KiB
Ruby
66 lines
1.9 KiB
Ruby
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
|
|
|
class OperationsRateLimiter
|
|
class ThrottleLimitExceeded < Exceptions::Forbidden; end
|
|
|
|
def initialize(limit:, period:, operation:)
|
|
@limit = limit
|
|
@period = period
|
|
@operation = operation
|
|
end
|
|
|
|
def ensure_within_limits!(by_ip:, by_identifier: nil)
|
|
ensure_within_identifier_limit(by_identifier) if by_identifier.present?
|
|
ensure_within_ip_limit(by_ip)
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_within_identifier_limit(value)
|
|
value = value.downcase.gsub(%r{\s+}, '')
|
|
fingerprint = Digest::MD5.hexdigest(value)
|
|
|
|
ensure_within :identifier, fingerprint
|
|
end
|
|
|
|
def ensure_within_ip_limit(ip_addr)
|
|
ensure_within :ip, ip_addr
|
|
end
|
|
|
|
def ensure_within(key, value)
|
|
period_identifier, lapsed_time = Time.now.to_i.divmod(@period.to_i)
|
|
|
|
cache_key = cache_key(key, value, period_identifier)
|
|
expires_in = cache_expires_in(lapsed_time)
|
|
value = increment(cache_key, expires_in)
|
|
|
|
return true if value <= @limit
|
|
|
|
raise ThrottleLimitExceeded, __('The request limit for this operation was exceeded.')
|
|
end
|
|
|
|
def cache_key(key, value, period_identifier)
|
|
[
|
|
self.class.name,
|
|
@operation,
|
|
key,
|
|
value,
|
|
period_identifier
|
|
].join('::')
|
|
end
|
|
|
|
def cache_expires_in(lapsed_time)
|
|
@period.to_i - lapsed_time + 1.minute # make sure there's no race condition with cache expiring during processing
|
|
end
|
|
|
|
def increment(cache_key, expires_in)
|
|
# Rails.cache.increment has surpising behaviours/bugs in Rails 7.0, so we don't use it.
|
|
# This may be working better in 7.1 and could be cleaned up after upgrading to Rails 7.1
|
|
#
|
|
# https://github.com/rails/rails/commit/f48bf3975f62e875a1cf4264b18ce3735915684d
|
|
new_value = (Rails.cache.read(cache_key) || 0) + 1
|
|
new_value.tap do
|
|
Rails.cache.write(cache_key, new_value, expires_in:)
|
|
end
|
|
end
|
|
end
|