2019-05-02 17:17:27 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
# A redis backed rate limiter.
|
|
|
|
class RateLimiter
|
|
|
|
|
2015-04-15 18:44:30 -05:00
|
|
|
attr_reader :max, :secs, :user, :key
|
|
|
|
|
2015-02-02 11:44:21 -06:00
|
|
|
def self.key_prefix
|
2017-12-06 18:48:11 -06:00
|
|
|
"l-rate-limit3:"
|
2015-02-02 11:44:21 -06:00
|
|
|
end
|
2015-01-29 10:44:51 -06:00
|
|
|
|
2013-10-08 23:10:37 -05:00
|
|
|
def self.disable
|
|
|
|
@disabled = true
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.enable
|
|
|
|
@disabled = false
|
|
|
|
end
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
# We don't observe rate limits in test mode
|
|
|
|
def self.disabled?
|
2017-12-04 04:23:11 -06:00
|
|
|
@disabled
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
2017-12-04 04:23:11 -06:00
|
|
|
# Only used in test, only clears current namespace, does not clear globals
|
2015-01-29 10:44:51 -06:00
|
|
|
def self.clear_all!
|
2019-12-03 03:05:53 -06:00
|
|
|
Discourse.redis.delete_prefixed(RateLimiter.key_prefix)
|
2015-01-29 10:44:51 -06:00
|
|
|
end
|
|
|
|
|
2017-12-11 00:21:00 -06:00
|
|
|
def self.clear_all_global!
|
2019-12-03 03:05:53 -06:00
|
|
|
Discourse.redis.without_namespace.keys("GLOBAL::#{key_prefix}*").each do |k|
|
|
|
|
Discourse.redis.without_namespace.del k
|
2017-12-11 00:21:00 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-09-24 12:52:32 -05:00
|
|
|
def build_key(type)
|
|
|
|
"#{RateLimiter.key_prefix}:#{@user && @user.id}:#{type}"
|
|
|
|
end
|
|
|
|
|
2020-11-04 23:36:17 -06:00
|
|
|
def initialize(user, type, max, secs, global: false, aggressive: false)
|
2013-02-05 13:16:51 -06:00
|
|
|
@user = user
|
2015-09-24 12:52:32 -05:00
|
|
|
@type = type
|
|
|
|
@key = build_key(type)
|
2013-02-05 13:16:51 -06:00
|
|
|
@max = max
|
|
|
|
@secs = secs
|
2017-12-11 00:21:00 -06:00
|
|
|
@global = global
|
2020-11-04 23:36:17 -06:00
|
|
|
@aggressive = aggressive
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
def clear!
|
2017-12-04 04:23:11 -06:00
|
|
|
redis.del(prefixed_key)
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
def can_perform?
|
2013-05-23 19:18:59 -05:00
|
|
|
rate_unlimited? || is_under_limit?
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
2021-01-19 03:35:46 -06:00
|
|
|
def seconds_to_wait(now)
|
|
|
|
@secs - age_of_oldest(now)
|
|
|
|
end
|
|
|
|
|
2017-12-04 04:23:11 -06:00
|
|
|
# reloader friendly
|
|
|
|
unless defined? PERFORM_LUA
|
|
|
|
PERFORM_LUA = <<~LUA
|
|
|
|
local now = tonumber(ARGV[1])
|
|
|
|
local secs = tonumber(ARGV[2])
|
|
|
|
local max = tonumber(ARGV[3])
|
2017-12-04 04:19:28 -06:00
|
|
|
|
2017-12-04 04:23:11 -06:00
|
|
|
local key = KEYS[1]
|
2017-12-04 04:19:28 -06:00
|
|
|
|
2017-12-04 04:23:11 -06:00
|
|
|
|
|
|
|
if ((tonumber(redis.call("LLEN", key)) < max) or
|
FIX: sliding window end time in rate limiter (#11691)
If the sliding window size is N seconds, then a moment at the Nth second
should be considered as the moment outside of the sliding window.
Otherwise, if the sliding window is already full, at the Nth second,
a new call wouldn't be allowed, but a time to wait before the next call
would be equal to zero, which is confusing.
In other words, the end of the time range shouldn't be included in the
sliding window.
Let's say we start at the second 0, and the sliding window size is 10
seconds. In the current version of rate limiter, this sliding window will
be considered as a time range [0, 10] (including the end of the range),
which actually is 11 seconds in length.
After this fix, the time range will be considered as [0, 10)
(excluding the end of the range), which is exactly 10 seconds in length.
2021-01-12 12:26:43 -06:00
|
|
|
(now - tonumber(redis.call("LRANGE", key, -1, -1)[1])) >= secs) then
|
2017-12-04 04:23:11 -06:00
|
|
|
redis.call("LPUSH", key, now)
|
|
|
|
redis.call("LTRIM", key, 0, max - 1)
|
|
|
|
redis.call("EXPIRE", key, secs * 2)
|
|
|
|
|
|
|
|
return 1
|
|
|
|
else
|
|
|
|
return 0
|
|
|
|
end
|
|
|
|
LUA
|
|
|
|
|
|
|
|
PERFORM_LUA_SHA = Digest::SHA1.hexdigest(PERFORM_LUA)
|
|
|
|
end
|
|
|
|
|
2020-11-04 23:36:17 -06:00
|
|
|
unless defined? PERFORM_LUA_AGGRESSIVE
|
|
|
|
PERFORM_LUA_AGGRESSIVE = <<~LUA
|
|
|
|
local now = tonumber(ARGV[1])
|
|
|
|
local secs = tonumber(ARGV[2])
|
|
|
|
local max = tonumber(ARGV[3])
|
|
|
|
|
|
|
|
local key = KEYS[1]
|
|
|
|
|
|
|
|
local return_val = 0
|
|
|
|
|
|
|
|
if ((tonumber(redis.call("LLEN", key)) < max) or
|
FIX: sliding window end time in rate limiter (#11691)
If the sliding window size is N seconds, then a moment at the Nth second
should be considered as the moment outside of the sliding window.
Otherwise, if the sliding window is already full, at the Nth second,
a new call wouldn't be allowed, but a time to wait before the next call
would be equal to zero, which is confusing.
In other words, the end of the time range shouldn't be included in the
sliding window.
Let's say we start at the second 0, and the sliding window size is 10
seconds. In the current version of rate limiter, this sliding window will
be considered as a time range [0, 10] (including the end of the range),
which actually is 11 seconds in length.
After this fix, the time range will be considered as [0, 10)
(excluding the end of the range), which is exactly 10 seconds in length.
2021-01-12 12:26:43 -06:00
|
|
|
(now - tonumber(redis.call("LRANGE", key, -1, -1)[1])) >= secs) then
|
2020-11-04 23:36:17 -06:00
|
|
|
return_val = 1
|
|
|
|
else
|
|
|
|
return_val = 0
|
|
|
|
end
|
|
|
|
|
|
|
|
redis.call("LPUSH", key, now)
|
|
|
|
redis.call("LTRIM", key, 0, max - 1)
|
|
|
|
redis.call("EXPIRE", key, secs * 2)
|
|
|
|
|
|
|
|
return return_val
|
|
|
|
LUA
|
|
|
|
|
|
|
|
PERFORM_LUA_AGGRESSIVE_SHA = Digest::SHA1.hexdigest(PERFORM_LUA_AGGRESSIVE)
|
|
|
|
end
|
|
|
|
|
2018-04-18 01:58:40 -05:00
|
|
|
def performed!(raise_error: true)
|
2018-04-24 17:44:07 -05:00
|
|
|
return true if rate_unlimited?
|
2017-12-06 18:48:11 -06:00
|
|
|
now = Time.now.to_i
|
2018-02-28 23:20:42 -06:00
|
|
|
|
2020-11-04 23:36:17 -06:00
|
|
|
if ((max || 0) <= 0) || rate_limiter_allowed?(now)
|
2021-01-12 14:09:15 -06:00
|
|
|
raise RateLimiter::LimitExceeded.new(seconds_to_wait(now), @type) if raise_error
|
2018-04-18 01:58:40 -05:00
|
|
|
false
|
|
|
|
else
|
|
|
|
true
|
2017-12-04 01:17:18 -06:00
|
|
|
end
|
2017-12-04 04:23:11 -06:00
|
|
|
rescue Redis::CommandError => e
|
|
|
|
if e.message =~ /READONLY/
|
|
|
|
# TODO,switch to in-memory rate limiter
|
|
|
|
else
|
|
|
|
raise
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
def rollback!
|
|
|
|
return if RateLimiter.disabled?
|
2017-12-04 04:23:11 -06:00
|
|
|
redis.lpop(prefixed_key)
|
2020-06-11 02:09:12 -05:00
|
|
|
rescue Redis::CommandError => e
|
|
|
|
if e.message =~ /READONLY/
|
|
|
|
# TODO,switch to in-memory rate limiter
|
|
|
|
else
|
|
|
|
raise
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
2016-03-18 10:17:51 -05:00
|
|
|
def remaining
|
|
|
|
return @max if @user && @user.staff?
|
|
|
|
|
2017-12-04 04:23:11 -06:00
|
|
|
arr = redis.lrange(prefixed_key, 0, @max) || []
|
2017-12-06 18:48:11 -06:00
|
|
|
t0 = Time.now.to_i
|
2016-03-18 10:17:51 -05:00
|
|
|
arr.reject! { |a| (t0 - a.to_i) > @secs }
|
|
|
|
@max - arr.size
|
|
|
|
end
|
|
|
|
|
2013-05-23 19:18:59 -05:00
|
|
|
private
|
|
|
|
|
2020-11-04 23:36:17 -06:00
|
|
|
def rate_limiter_allowed?(now)
|
|
|
|
|
|
|
|
lua, lua_sha = nil
|
|
|
|
if @aggressive
|
|
|
|
lua = PERFORM_LUA_AGGRESSIVE
|
|
|
|
lua_sha = PERFORM_LUA_AGGRESSIVE_SHA
|
|
|
|
else
|
|
|
|
lua = PERFORM_LUA
|
|
|
|
lua_sha = PERFORM_LUA_SHA
|
|
|
|
end
|
|
|
|
|
|
|
|
eval_lua(lua, lua_sha, [prefixed_key], [now, @secs, @max]) == 0
|
|
|
|
end
|
|
|
|
|
2017-12-04 04:23:11 -06:00
|
|
|
def prefixed_key
|
|
|
|
if @global
|
|
|
|
"GLOBAL::#{key}"
|
|
|
|
else
|
2019-12-03 03:05:53 -06:00
|
|
|
Discourse.redis.namespace_key(key)
|
2017-12-04 04:23:11 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def redis
|
2019-12-03 03:05:53 -06:00
|
|
|
Discourse.redis.without_namespace
|
2017-12-04 04:23:11 -06:00
|
|
|
end
|
|
|
|
|
2021-01-12 14:09:15 -06:00
|
|
|
def age_of_oldest(now)
|
2013-05-25 14:37:28 -05:00
|
|
|
# age of oldest event in buffer, in seconds
|
2021-01-12 14:09:15 -06:00
|
|
|
now - redis.lrange(prefixed_key, -1, -1).first.to_i
|
2013-05-25 14:37:28 -05:00
|
|
|
end
|
|
|
|
|
2013-05-23 19:18:59 -05:00
|
|
|
def is_under_limit?
|
2021-01-12 14:09:15 -06:00
|
|
|
now = Time.now.to_i
|
|
|
|
|
2015-01-29 10:44:51 -06:00
|
|
|
# number of events in buffer less than max allowed? OR
|
2017-12-04 04:23:11 -06:00
|
|
|
(redis.llen(prefixed_key) < @max) ||
|
2021-05-20 20:43:47 -05:00
|
|
|
# age bigger than sliding window size?
|
2021-01-12 14:09:15 -06:00
|
|
|
(age_of_oldest(now) >= @secs)
|
2013-05-23 19:18:59 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def rate_unlimited?
|
2014-08-17 19:55:30 -05:00
|
|
|
!!(RateLimiter.disabled? || (@user && @user.staff?))
|
2013-05-23 19:18:59 -05:00
|
|
|
end
|
2017-12-04 04:23:11 -06:00
|
|
|
|
|
|
|
def eval_lua(lua, sha, keys, args)
|
|
|
|
redis.evalsha(sha, keys, args)
|
|
|
|
rescue Redis::CommandError => e
|
|
|
|
if e.to_s =~ /^NOSCRIPT/
|
|
|
|
redis.eval(lua, keys, args)
|
|
|
|
else
|
|
|
|
raise
|
|
|
|
end
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|