class Rack::Attack

Public Class Methods

_parse_key(unprefixed_key) click to toggle source

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
all_keys() click to toggle source
# 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
allow2ban(name, discriminator, &block) click to toggle source
# File lib/rack/attack_extensions.rb, line 251
def allow2ban(name, discriminator, &block)
  fail2ban(name, discriminator, klass: Allow2Ban, &block)
end
cache_namespace() click to toggle source
# File lib/rack/attack_extensions.rb, line 42
def cache_namespace
  cache.store&.options&.[](:namespace)
end
cache_store_and_namespace_to_strip() click to toggle source

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
counters_h() click to toggle source
# 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
def_allow2ban(name, options) click to toggle source
# File lib/rack/attack_extensions.rb, line 236
def def_allow2ban(name, options)
  self.fail2bans[name] = Allow2Ban.new(name, options.merge(type: :allow2ban))
end
def_fail2ban(name, options) click to toggle source
# File lib/rack/attack_extensions.rb, line 233
def def_fail2ban(name, options)
  self.fail2bans[name] = Fail2Ban.new( name, options.merge(type: :fail2ban))
end
discriminator_from_key(unprefixed_key) click to toggle source
# File lib/rack/attack_extensions.rb, line 149
def discriminator_from_key(unprefixed_key)
  _parse_key(unprefixed_key)&.[](:discriminator)
end
fail2ban(name, discriminator, klass: Fail2Ban, &block) click to toggle source
# 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
fail2bans() click to toggle source
# File lib/rack/attack_extensions.rb, line 231
def fail2bans;  @fail2bans  ||= {}; end
find_rule(name) click to toggle source
# File lib/rack/attack_extensions.rb, line 157
def find_rule(name)
  throttles[name] ||
  blocklists[name] ||
  fail2bans[name]
end
humanize_h(h) click to toggle source
# File lib/rack/attack_extensions.rb, line 97
def humanize_h(h)
  h.transform_keys do |key|
    humanize_key(key)
  end
end
humanize_key(key) click to toggle source

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
ip_from_key(key) click to toggle source
# 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
is_tracked?(request) click to toggle source

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
keys() click to toggle source

AKA unprefixed_keys

# File lib/rack/attack_extensions.rb, line 71
def keys
  prefixed_keys.map { |key|
    unprefix_key(key)
  }
end
name_from_key(unprefixed_key) click to toggle source
# File lib/rack/attack_extensions.rb, line 145
def name_from_key(unprefixed_key)
  _parse_key(unprefixed_key)&.[](:name)
end
parse_key(unprefixed_key) click to toggle source

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
prefix_with_namespace() click to toggle source

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
prefix_with_namespace_to_strip() click to toggle source

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
prefixed_keys() click to toggle source
# File lib/rack/attack_extensions.rb, line 66
def prefixed_keys
  all_keys.grep(/^#{cache.prefix}:/)
end
time_bucket_from_key(unprefixed_key) click to toggle source
# File lib/rack/attack_extensions.rb, line 141
def time_bucket_from_key(unprefixed_key)
  _parse_key(unprefixed_key)&.[](:time_bucket)
end
time_range(unprefixed_key) click to toggle source
# File lib/rack/attack_extensions.rb, line 153
def time_range(unprefixed_key)
  parse_key(unprefixed_key)&.[](:time_range)
end
to_h() click to toggle source
# 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
unprefix_key(key) click to toggle source
# File lib/rack/attack_extensions.rb, line 77
def unprefix_key(key)
  key.sub "#{cache.prefix}:", ''
end