class BBS::RateLimit
At most limit events per per seconds for each key (a caller, an address, a whole board), over a sliding window; thread-safe.
TIPS = BBS::RateLimit.new(limit: 3, per: 3600) if TIPS.allow?(BBS::RateLimit.caller_key(session)) then save(tip) else say("Again in #{TIPS.wait(key).ceil} seconds.") end
Several limits at once (a caller’s and the whole board’s) are checked with ::allow_all?, which counts the event in every limit or in none. room? and record check and count apart, for an event counted only when it went wrong (a failed login):
return locked_out unless LOGINS.room?(key) LOGINS.record(key) unless password_ok?
By default the events live in memory and a restart forgets them; with a BBS::SQLite store they are kept in its rate_limit_events table under the limit’s name, so a lockout outlasts a restart. Keys that have been quiet for per seconds are forgotten either way.
Constants
- LOCK
-
The one lock of every limit, so
::allow_all?can check several at once; reentrant.
Public Class Methods
Source
# File lib/bbs/rate_limit.rb, line 53 def self.allow_all?(checks) LOCK.synchronize do next false unless checks.all? { |limit, key| limit.room?(key) } checks.each { |limit, key| limit.record(key) } true end end
Returns whether every limit has room for its key, and then counts the event in each; when one has no room, nothing is counted. checks is an Array of [limit, key] pairs.
BBS::RateLimit.allow_all?([[PER_CALLER, key], [PER_BOARD, :all]])
Source
# File lib/bbs/rate_limit.rb, line 36 def self.caller_key(session) address = Address.parse(peer(session)) address.nil? || Address.local?(address) ? "line:#{session.session_id}" : address.to_s end
Returns the key a caller’s events are counted by: the IP address of a caller on the internet (an IPv4-mapped one as IPv4), or line:<session id> for one behind a local proxy such as bbs-web (BBS::Address.local?), whose address is the proxy’s, and for a line without an address.
# File lib/bbs/rate_limit.rb, line 65 def initialize(limit:, per:, store: nil, name: nil) raise ArgumentError, 'a stored RateLimit needs a name' if store && name.to_s.empty? @limit = limit @per = per @events = store ? Table.new(store, name.to_s) : Memory.new end
Creates a limit of limit events per per seconds. With a store (a BBS::SQLite) the events are kept there under name (a String, required then); without one, in memory.
Source
# File lib/bbs/rate_limit.rb, line 42 def self.peer(session) session.line.remote_address.ip_address rescue StandardError nil end
Returns the IP address of session‘s line as a String, or nil.
Public Instance Methods
Source
# File lib/bbs/rate_limit.rb, line 75 def allow?(key) = RateLimit.allow_all?([[self, key]])
Returns whether key may have one more event now, and counts it when it may.
Source
# File lib/bbs/rate_limit.rb, line 81 def record(key) LOCK.synchronize { @events.add(key, @events.now) } nil end
Counts one event of key now, room or not. Returns nil.
Source
# File lib/bbs/rate_limit.rb, line 78 def room?(key) = LOCK.synchronize { recent(key).size < @limit }
Returns whether key may have one more event now. Counts nothing.
Source
# File lib/bbs/rate_limit.rb, line 88 def wait(key) LOCK.synchronize do times = recent(key) times.size < @limit ? 0.0 : [times[-@limit] + @per - @events.now, 0.0].max end end
Returns the seconds until key may have an event, a Float; 0.0 when it may now. Counts nothing.