Class: Rack::Attack
- Inherits:
-
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
-
._parse_key(unprefixed_key) ⇒ Object
Reverse Cache#key_and_expiry: … “#prefix:#/ period).to_i:#unprefixed_key” ….
-
.all_keys ⇒ Object
-
.allow2ban(name, discriminator, &block) ⇒ Object
-
.cache_namespace ⇒ Object
-
.cache_store_and_namespace_to_strip ⇒ Object
Returns an array of [cache_store, namespace_to_strip] This will either be [a Redis::Store, nil] or [a Redis , namespace_to_strip].
-
.counters_h ⇒ Object
-
.def_allow2ban(name, options) ⇒ Object
-
.def_fail2ban(name, options) ⇒ Object
-
.discriminator_from_key(unprefixed_key) ⇒ Object
-
.fail2ban(name, discriminator, klass: Fail2Ban, &block) ⇒ Object
-
.fail2bans ⇒ Object
-
.find_rule(name) ⇒ Object
-
.humanize_h(h) ⇒ Object
-
.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.
-
.ip_from_key(key) ⇒ Object
-
.is_tracked?(request) ⇒ Boolean
Unlike the provided #tracked?, this returns a boolean which is only true if one of the tracks matches.
-
.keys ⇒ Object
-
.name_from_key(unprefixed_key) ⇒ Object
-
.parse_key(unprefixed_key) ⇒ Hash
Reverse Cache#key_and_expiry: … “#prefix:#/ period).to_i:#unprefixed_key” ….
-
.prefix_with_namespace ⇒ Object
The same as cache.prefix but prefixed with “namespace:” if namespace option is set.
-
.prefix_with_namespace_to_strip ⇒ Object
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.
-
.prefixed_keys ⇒ Object
-
.time_bucket_from_key(unprefixed_key) ⇒ Object
-
.time_range(unprefixed_key) ⇒ Object
-
.to_h ⇒ Object
-
.unprefix_key(key) ⇒ Object
Class Method Details
._parse_key(unprefixed_key) ⇒ Object
Reverse Cache#key_and_expiry:
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_keys ⇒ Object
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_namespace ⇒ Object
42
43
44
|
# File 'lib/rack/attack_extensions.rb', line 42
def cache_namespace
cache.store&.options&.[](:namespace)
end
|
.cache_store_and_namespace_to_strip ⇒ Object
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 = store.redis if store.respond_to?(:redis)
if store.respond_to?(:data)
store = store.data
[store, nil]
else
[store, cache_namespace]
end
end
|
.counters_h ⇒ Object
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
|
.fail2bans ⇒ Object
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`.)
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
|
.keys ⇒ Object
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:
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_namespace ⇒ Object
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_strip ⇒ Object
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_keys ⇒ Object
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_h ⇒ Object
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
|