Class: Rack::Attack

Inherits:
Object
  • Object
show all
Extended by:
Memoist
Defined in:
lib/rack/attack_extensions.rb

Defined Under Namespace

Modules: InspectWithOptions, InstantiableFail2Ban, PeriodIntrospection Classes: BannedIp, BannedIps, Fail2Ban, Request, Throttle

Class Method Summary collapse

Class Method Details

._parse_key(unprefixed_key) ⇒ Object

Reverse Cache#key_and_expiry:

… "#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}" …


105
106
107
108
109
110
111
112
113
114
# 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_keysObject



9
10
11
12
13
14
15
16
17
# 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) ⇒ Object



251
252
253
# File 'lib/rack/attack_extensions.rb', line 251

def allow2ban(name, discriminator, &block)
  fail2ban(name, discriminator, klass: Allow2Ban, &block)
end

.cache_namespaceObject



42
43
44
# File 'lib/rack/attack_extensions.rb', line 42

def cache_namespace
  cache.store&.options&.[](:namespace)
end

.cache_store_and_namespace_to_stripObject

Returns an array of [cache_store, namespace_to_strip] This will either be [a Redis::Store, nil] or

[a Redis       , namespace_to_strip]


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 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_hObject



87
88
89
90
91
# 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) ⇒ Object



236
237
238
# 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) ⇒ Object



233
234
235
# 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) ⇒ Object



149
150
151
# 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) ⇒ Object



240
241
242
243
244
245
246
247
248
249
# 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

.fail2bansObject



231
# File 'lib/rack/attack_extensions.rb', line 231

def fail2bans;  @fail2bans  ||= {}; end

.find_rule(name) ⇒ Object



157
158
159
160
161
# File 'lib/rack/attack_extensions.rb', line 157

def find_rule(name)
  throttles[name] ||
  blocklists[name] ||
  fail2bans[name]
end

.humanize_h(h) ⇒ Object



97
98
99
100
101
# 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) ⇒ Object

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.



169
170
171
172
173
174
175
176
177
178
# 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) ⇒ Object



93
94
95
# 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) ⇒ Boolean

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`.)

Returns:

  • (Boolean)


182
183
184
185
186
# File 'lib/rack/attack_extensions.rb', line 182

def is_tracked?(request)
  tracks.any? do |_name, track|
    track.matched_by?(request)
  end
end

.keysObject

AKA unprefixed_keys



71
72
73
74
75
# File 'lib/rack/attack_extensions.rb', line 71

def keys
  prefixed_keys.map { |key|
    unprefix_key(key)
  }
end

.name_from_key(unprefixed_key) ⇒ Object



145
146
147
# File 'lib/rack/attack_extensions.rb', line 145

def name_from_key(unprefixed_key)
  _parse_key(unprefixed_key)&.[](:name)
end

.parse_key(unprefixed_key) ⇒ Hash

Reverse Cache#key_and_expiry:

… "#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}" …

Returns:

  • (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.
    


126
127
128
129
130
131
132
133
134
135
136
137
138
139
# 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_namespaceObject

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.



34
35
36
37
38
39
40
# 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_stripObject

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 ‘:’.



22
23
24
25
26
27
28
29
# 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_keysObject



66
67
68
# File 'lib/rack/attack_extensions.rb', line 66

def prefixed_keys
  all_keys.grep(/^#{cache.prefix}:/)
end

.time_bucket_from_key(unprefixed_key) ⇒ Object



141
142
143
# 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) ⇒ Object



153
154
155
# File 'lib/rack/attack_extensions.rb', line 153

def time_range(unprefixed_key)
  parse_key(unprefixed_key)&.[](:time_range)
end

.to_hObject



81
82
83
84
85
# 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) ⇒ Object



77
78
79
# File 'lib/rack/attack_extensions.rb', line 77

def unprefix_key(key)
  key.sub "#{cache.prefix}:", ''
end