class Rack::Attack
Public Class Methods
Reverse Cache#key_and_expiry:
… "#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}" …
# File lib/rack/attack_extensions.rb, line 105 def _parse_key(unprefixed_key) unprefixed_key.match( /\A (?<time_bucket>\d+) # 1 or more digits # In the case of 'fail2ban:count:local_name', want name to onlybe 'local_name' (?::(?:fail|allow)2ban:count)?:(?<name>.+) :(?<discriminator>[^:]+) \Z/x ) end
# File lib/rack/attack_extensions.rb, line 9 def all_keys store, namespace = cache_store_and_namespace_to_strip keys = store.keys if namespace keys.map {|key| key.to_s.sub(/^#{namespace}:/, '') } else keys end end
# File lib/rack/attack_extensions.rb, line 251 def allow2ban(name, discriminator, &block) fail2ban(name, discriminator, klass: Allow2Ban, &block) end
# File lib/rack/attack_extensions.rb, line 42 def cache_namespace cache.store&.options&.[](:namespace) end
Returns an array of [cache_store, namespace_to_strip] This will either be [a Redis::Store, nil] or
[a Redis , namespace_to_strip]
# File lib/rack/attack_extensions.rb, line 49 def cache_store_and_namespace_to_strip store = cache.store # Store can be a ActiveSupport::Cache::RedisCacheStore, a Redis::Store object, or a Redis object. # If it is a ActiveSupport::Cache::RedisCacheStore, then we need to get the redis object in # order to get keys from it. store = store.redis if store.respond_to?(:redis) if store.respond_to?(:data) # Redis::Store already stripped namespaced store = store.data [store, nil] else # Redis object (which is all we have available in the case of a # ActiveSupport::Cache::RedisCacheStore) unfortunately returns keys with namespace prefix in # each key, so we need to strip this out (Redis::Store does this already; see store.data.keys above) [store, cache_namespace] end end
# File lib/rack/attack_extensions.rb, line 87 def counters_h (keys - Fail2Ban.banned_ip_keys).each_with_object({}) do |unprefixed_key, h| h[unprefixed_key] = cache.read(unprefixed_key) end end
# File lib/rack/attack_extensions.rb, line 236 def def_allow2ban(name, options) self.fail2bans[name] = Allow2Ban.new(name, options.merge(type: :allow2ban)) end
# File lib/rack/attack_extensions.rb, line 233 def def_fail2ban(name, options) self.fail2bans[name] = Fail2Ban.new( name, options.merge(type: :fail2ban)) end
# File lib/rack/attack_extensions.rb, line 149 def discriminator_from_key(unprefixed_key) _parse_key(unprefixed_key)&.[](:discriminator) end
# File lib/rack/attack_extensions.rb, line 240 def fail2ban(name, discriminator, klass: Fail2Ban, &block) instance = fail2bans[name] or raise "could not find a fail2ban rule named '#{name}'; make sure you define with def_fail2ban/def_allow2ban first" klass.filter( "#{name}:#{discriminator}", findtime: instance.period, maxretry: instance.limit, bantime: instance.bantime, &block ) end
# File lib/rack/attack_extensions.rb, line 231 def fail2bans; @fail2bans ||= {}; end
# File lib/rack/attack_extensions.rb, line 157 def find_rule(name) throttles[name] || blocklists[name] || fail2bans[name] end
# File lib/rack/attack_extensions.rb, line 97 def humanize_h(h) h.transform_keys do |key| humanize_key(key) end end
Transform
rack::attack:5179628:req/ip:127.0.0.1
into something like
throttle('req/ip'):127.0.0.1
so you can see which period it was for and what the limit for that period was. Would have to look up the rules stored in Rack::Attack
.
# File lib/rack/attack_extensions.rb, line 169 def humanize_key(key) key = unprefix_key(key) match = parse_key(key) return key unless match name = match[:name] rule = find_rule(name) rule_type = rule.type if rule "#{rule_type}('#{name}'):#{match[:discriminator]}" end
# File lib/rack/attack_extensions.rb, line 93 def ip_from_key(key) key.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)&.to_s end
Unlike the provided tracked?, this returns a boolean which is only true if one of the tracks matches. (The provided tracked? just returns the array of `tracks`.)
# File lib/rack/attack_extensions.rb, line 182 def is_tracked?(request) tracks.any? do |_name, track| track.matched_by?(request) end end
AKA unprefixed_keys
# File lib/rack/attack_extensions.rb, line 71 def keys prefixed_keys.map { |key| unprefix_key(key) } end
# File lib/rack/attack_extensions.rb, line 145 def name_from_key(unprefixed_key) _parse_key(unprefixed_key)&.[](:name) end
Reverse Cache#key_and_expiry:
… "#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}" …
@return [Hash]
:time_bucket [Number]: The raw time bucket (Time.now / period), like 5180595 :name [String]: The name of the rule, as passed to `throttle`, `def_fail2ban`, etc. :discriminator [String]: A discriminator such as a specific IP address. :time_range [Range]: (If we have enough information to calculate) A Range, like Time('12:35')..Time('12:40'). This Range has an extra duration method that returns a ActiveSupport::Duration representing the duration of the period.
# File lib/rack/attack_extensions.rb, line 126 def parse_key(unprefixed_key) match = _parse_key(unprefixed_key) return unless match match.named_captures.with_indifferent_access.tap do |hash| hash[:rule] = rule = find_rule(hash[:name]) if ( hash[:time_bucket] and rule and rule.respond_to?(:period) ) hash[:time_range] = rule.time_range(hash[:time_bucket]) end end end
The same as cache.prefix but prefixed with “{namespace}:” if namespace option is set. Needed when passing a key directly to a Redis command, like Redis#ttl, since Redis class doesn't know about namespacing.
# File lib/rack/attack_extensions.rb, line 34 def prefix_with_namespace prefix = cache.prefix if namespace = cache_namespace prefix = "#{namespace}:#{prefix}" end prefix end
The same as cache.prefix but prefixed with “{namespace}:” if namespace option is set and needs to be stripped from keys returned from store.keys. Like cache.prefix, this does not include the trailing ':'.
# File lib/rack/attack_extensions.rb, line 22 def prefix_with_namespace_to_strip prefix = cache.prefix store, namespace = cache_store_and_namespace_to_strip if namespace prefix = "#{namespace}:#{prefix}" end prefix end
# File lib/rack/attack_extensions.rb, line 66 def prefixed_keys all_keys.grep(/^#{cache.prefix}:/) end
# File lib/rack/attack_extensions.rb, line 141 def time_bucket_from_key(unprefixed_key) _parse_key(unprefixed_key)&.[](:time_bucket) end
# File lib/rack/attack_extensions.rb, line 153 def time_range(unprefixed_key) parse_key(unprefixed_key)&.[](:time_range) end
# File lib/rack/attack_extensions.rb, line 81 def to_h keys.each_with_object({}) do |k, h| h[k] = cache.store.read(k) end end
# File lib/rack/attack_extensions.rb, line 77 def unprefix_key(key) key.sub "#{cache.prefix}:", '' end