2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
# A redis backed rate limiter.
|
|
|
|
class RateLimiter
|
|
|
|
|
2015-04-15 19:44:30 -04:00
|
|
|
attr_reader :max, :secs, :user, :key
|
|
|
|
|
2015-02-02 12:44:21 -05:00
|
|
|
def self.key_prefix
|
2017-12-06 19:48:11 -05:00
|
|
|
"l-rate-limit3:"
|
2015-02-02 12:44:21 -05:00
|
|
|
end
|
2015-01-29 11:44:51 -05:00
|
|
|
|
2013-10-09 00:10:37 -04:00
|
|
|
def self.disable
|
|
|
|
@disabled = true
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.enable
|
|
|
|
@disabled = false
|
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
# We don't observe rate limits in test mode
|
|
|
|
def self.disabled?
|
2017-12-04 05:23:11 -05:00
|
|
|
@disabled
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
# Only used in test, only clears current namespace, does not clear globals
|
2015-01-29 11:44:51 -05:00
|
|
|
def self.clear_all!
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.delete_prefixed(RateLimiter.key_prefix)
|
2015-01-29 11:44:51 -05:00
|
|
|
end
|
|
|
|
|
2017-12-11 01:21:00 -05:00
|
|
|
def self.clear_all_global!
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.without_namespace.keys("GLOBAL::#{key_prefix}*").each do |k|
|
|
|
|
Discourse.redis.without_namespace.del k
|
2017-12-11 01:21:00 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-09-24 13:52:32 -04:00
|
|
|
def build_key(type)
|
|
|
|
"#{RateLimiter.key_prefix}:#{@user && @user.id}:#{type}"
|
|
|
|
end
|
|
|
|
|
2020-11-05 00:36:17 -05:00
|
|
|
def initialize(user, type, max, secs, global: false, aggressive: false)
|
2013-02-05 14:16:51 -05:00
|
|
|
@user = user
|
2015-09-24 13:52:32 -04:00
|
|
|
@type = type
|
|
|
|
@key = build_key(type)
|
2013-02-05 14:16:51 -05:00
|
|
|
@max = max
|
|
|
|
@secs = secs
|
2017-12-11 01:21:00 -05:00
|
|
|
@global = global
|
2020-11-05 00:36:17 -05:00
|
|
|
@aggressive = aggressive
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def clear!
|
2017-12-04 05:23:11 -05:00
|
|
|
redis.del(prefixed_key)
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def can_perform?
|
2013-05-23 20:18:59 -04:00
|
|
|
rate_unlimited? || is_under_limit?
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2017-12-04 05:23:11 -05: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 05:19:28 -05:00
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
local key = KEYS[1]
|
2017-12-04 05:19:28 -05:00
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
|
|
|
|
if ((tonumber(redis.call("LLEN", key)) < max) or
|
|
|
|
(now - tonumber(redis.call("LRANGE", key, -1, -1)[1])) > secs) then
|
|
|
|
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-05 00:36:17 -05: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
|
|
|
|
(now - tonumber(redis.call("LRANGE", key, -1, -1)[1])) > secs) then
|
|
|
|
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 02:58:40 -04:00
|
|
|
def performed!(raise_error: true)
|
2018-04-24 18:44:07 -04:00
|
|
|
return true if rate_unlimited?
|
2017-12-06 19:48:11 -05:00
|
|
|
now = Time.now.to_i
|
2018-03-01 00:20:42 -05:00
|
|
|
|
2020-11-05 00:36:17 -05:00
|
|
|
if ((max || 0) <= 0) || rate_limiter_allowed?(now)
|
2018-04-18 02:58:40 -04:00
|
|
|
raise RateLimiter::LimitExceeded.new(seconds_to_wait, @type) if raise_error
|
|
|
|
false
|
|
|
|
else
|
|
|
|
true
|
2017-12-04 02:17:18 -05:00
|
|
|
end
|
2017-12-04 05:23:11 -05:00
|
|
|
rescue Redis::CommandError => e
|
|
|
|
if e.message =~ /READONLY/
|
|
|
|
# TODO,switch to in-memory rate limiter
|
|
|
|
else
|
|
|
|
raise
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def rollback!
|
|
|
|
return if RateLimiter.disabled?
|
2017-12-04 05:23:11 -05:00
|
|
|
redis.lpop(prefixed_key)
|
2020-06-11 03:09:12 -04:00
|
|
|
rescue Redis::CommandError => e
|
|
|
|
if e.message =~ /READONLY/
|
|
|
|
# TODO,switch to in-memory rate limiter
|
|
|
|
else
|
|
|
|
raise
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2016-03-18 11:17:51 -04:00
|
|
|
def remaining
|
|
|
|
return @max if @user && @user.staff?
|
|
|
|
|
2017-12-04 05:23:11 -05:00
|
|
|
arr = redis.lrange(prefixed_key, 0, @max) || []
|
2017-12-06 19:48:11 -05:00
|
|
|
t0 = Time.now.to_i
|
2016-03-18 11:17:51 -04:00
|
|
|
arr.reject! { |a| (t0 - a.to_i) > @secs }
|
|
|
|
@max - arr.size
|
|
|
|
end
|
|
|
|
|
2013-05-23 20:18:59 -04:00
|
|
|
private
|
|
|
|
|
2020-11-05 00:36:17 -05: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 05:23:11 -05:00
|
|
|
def prefixed_key
|
|
|
|
if @global
|
|
|
|
"GLOBAL::#{key}"
|
|
|
|
else
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.namespace_key(key)
|
2017-12-04 05:23:11 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def redis
|
2019-12-03 04:05:53 -05:00
|
|
|
Discourse.redis.without_namespace
|
2017-12-04 05:23:11 -05:00
|
|
|
end
|
|
|
|
|
2013-05-25 15:37:28 -04:00
|
|
|
def seconds_to_wait
|
|
|
|
@secs - age_of_oldest
|
|
|
|
end
|
|
|
|
|
|
|
|
def age_of_oldest
|
|
|
|
# age of oldest event in buffer, in seconds
|
2017-12-06 19:48:11 -05:00
|
|
|
Time.now.to_i - redis.lrange(prefixed_key, -1, -1).first.to_i
|
2013-05-25 15:37:28 -04:00
|
|
|
end
|
|
|
|
|
2013-05-23 20:18:59 -04:00
|
|
|
def is_under_limit?
|
2015-01-29 11:44:51 -05:00
|
|
|
# number of events in buffer less than max allowed? OR
|
2017-12-04 05:23:11 -05:00
|
|
|
(redis.llen(prefixed_key) < @max) ||
|
2015-01-29 11:44:51 -05:00
|
|
|
# age bigger than silding window size?
|
|
|
|
(age_of_oldest > @secs)
|
2013-05-23 20:18:59 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def rate_unlimited?
|
2014-08-17 20:55:30 -04:00
|
|
|
!!(RateLimiter.disabled? || (@user && @user.staff?))
|
2013-05-23 20:18:59 -04:00
|
|
|
end
|
2017-12-04 05:23:11 -05: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 14:16:51 -05:00
|
|
|
end
|