rack-attack 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +164 -79
- data/lib/rack/attack.rb +30 -8
- data/lib/rack/attack/cache.rb +24 -10
- data/lib/rack/attack/check.rb +1 -0
- data/lib/rack/attack/version.rb +1 -1
- data/spec/acceptance/allow2ban_spec.rb +71 -0
- data/spec/acceptance/blocking_ip_spec.rb +38 -0
- data/spec/acceptance/blocking_spec.rb +20 -0
- data/spec/acceptance/blocking_subnet_spec.rb +44 -0
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +111 -0
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +108 -0
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +48 -0
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +31 -0
- data/spec/acceptance/customizing_blocked_response_spec.rb +41 -0
- data/spec/acceptance/customizing_throttled_response_spec.rb +59 -0
- data/spec/acceptance/extending_request_object_spec.rb +34 -0
- data/spec/acceptance/fail2ban_spec.rb +76 -0
- data/spec/acceptance/safelisting_ip_spec.rb +49 -0
- data/spec/acceptance/safelisting_spec.rb +16 -0
- data/spec/acceptance/safelisting_subnet_spec.rb +48 -0
- data/spec/acceptance/throttling_spec.rb +130 -1
- data/spec/acceptance/track_spec.rb +27 -0
- data/spec/acceptance/track_throttle_spec.rb +53 -0
- data/spec/spec_helper.rb +12 -0
- metadata +60 -4
- data/spec/rack_attack_store_config_spec.rb +0 -20
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "Cache store config when throttling without Rails" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
6
|
+
request.ip
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "gives semantic error if no store was configured" do
|
11
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
12
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "gives semantic error if incompatible store was configured" do
|
17
|
+
Rack::Attack.cache.store = Object.new
|
18
|
+
|
19
|
+
assert_raises(Rack::Attack::MisconfiguredStoreError) do
|
20
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "works with any object that responds to #increment" do
|
25
|
+
basic_store_class = Class.new do
|
26
|
+
attr_accessor :counts
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@counts = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def increment(key, count, options)
|
33
|
+
@counts[key] ||= 0
|
34
|
+
@counts[key] += 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Rack::Attack.cache.store = basic_store_class.new
|
39
|
+
|
40
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
41
|
+
|
42
|
+
assert_equal 200, last_response.status
|
43
|
+
|
44
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
45
|
+
|
46
|
+
assert_equal 429, last_response.status
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
require "minitest/stub_const"
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
describe "Cache store config with Rails" do
|
6
|
+
before do
|
7
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
8
|
+
request.ip
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "fails when Rails.cache is not set" do
|
13
|
+
Object.stub_const(:Rails, OpenStruct.new(cache: nil)) do
|
14
|
+
assert_raises(Rack::Attack::MissingStoreError) do
|
15
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "works when Rails.cache is set" do
|
21
|
+
Object.stub_const(:Rails, OpenStruct.new(cache: ActiveSupport::Cache::MemoryStore.new)) do
|
22
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
23
|
+
|
24
|
+
assert_equal 200, last_response.status
|
25
|
+
|
26
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
27
|
+
|
28
|
+
assert_equal 429, last_response.status
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "Customizing block responses" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.blocklist("block 1.2.3.4") do |request|
|
6
|
+
request.ip == "1.2.3.4"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
it "can be customized" do
|
11
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
12
|
+
|
13
|
+
assert_equal 403, last_response.status
|
14
|
+
|
15
|
+
Rack::Attack.blocklisted_response = lambda do |env|
|
16
|
+
[503, {}, ["Blocked"]]
|
17
|
+
end
|
18
|
+
|
19
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
20
|
+
|
21
|
+
assert_equal 503, last_response.status
|
22
|
+
assert_equal "Blocked", last_response.body
|
23
|
+
end
|
24
|
+
|
25
|
+
it "exposes match data" do
|
26
|
+
matched = nil
|
27
|
+
match_type = nil
|
28
|
+
|
29
|
+
Rack::Attack.blocklisted_response = lambda do |env|
|
30
|
+
matched = env['rack.attack.matched']
|
31
|
+
match_type = env['rack.attack.match_type']
|
32
|
+
|
33
|
+
[503, {}, ["Blocked"]]
|
34
|
+
end
|
35
|
+
|
36
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
37
|
+
|
38
|
+
assert_equal "block 1.2.3.4", matched
|
39
|
+
assert_equal :blocklist, match_type
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "Customizing throttled response" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
6
|
+
|
7
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
8
|
+
request.ip
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can be customized" do
|
13
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
14
|
+
|
15
|
+
assert_equal 200, last_response.status
|
16
|
+
|
17
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
18
|
+
|
19
|
+
assert_equal 429, last_response.status
|
20
|
+
|
21
|
+
Rack::Attack.throttled_response = lambda do |env|
|
22
|
+
[503, {}, ["Throttled"]]
|
23
|
+
end
|
24
|
+
|
25
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
26
|
+
|
27
|
+
assert_equal 503, last_response.status
|
28
|
+
assert_equal "Throttled", last_response.body
|
29
|
+
end
|
30
|
+
|
31
|
+
it "exposes match data" do
|
32
|
+
matched = nil
|
33
|
+
match_type = nil
|
34
|
+
match_data = nil
|
35
|
+
match_discriminator = nil
|
36
|
+
|
37
|
+
Rack::Attack.throttled_response = lambda do |env|
|
38
|
+
matched = env['rack.attack.matched']
|
39
|
+
match_type = env['rack.attack.match_type']
|
40
|
+
match_data = env['rack.attack.match_data']
|
41
|
+
match_discriminator = env['rack.attack.match_discriminator']
|
42
|
+
|
43
|
+
[429, {}, ["Throttled"]]
|
44
|
+
end
|
45
|
+
|
46
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
47
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
48
|
+
|
49
|
+
assert_equal "by ip", matched
|
50
|
+
assert_equal :throttle, match_type
|
51
|
+
assert_equal 60, match_data[:period]
|
52
|
+
assert_equal 1, match_data[:limit]
|
53
|
+
assert_equal 2, match_data[:count]
|
54
|
+
assert_equal "1.2.3.4", match_discriminator
|
55
|
+
|
56
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
57
|
+
assert_equal 3, match_data[:count]
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "Extending the request object" do
|
4
|
+
before do
|
5
|
+
class Rack::Attack::Request
|
6
|
+
def authorized?
|
7
|
+
env["APIKey"] == "private-secret"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Rack::Attack.blocklist("unauthorized requests") do |request|
|
12
|
+
!request.authorized?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# We don't want the extension to leak to other test cases
|
17
|
+
after do
|
18
|
+
class Rack::Attack::Request
|
19
|
+
remove_method :authorized?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it "forbids request if blocklist condition is true" do
|
24
|
+
get "/"
|
25
|
+
|
26
|
+
assert_equal 403, last_response.status
|
27
|
+
end
|
28
|
+
|
29
|
+
it "succeeds if blocklist condition is false" do
|
30
|
+
get "/", {}, "APIKey" => "private-secret"
|
31
|
+
|
32
|
+
assert_equal 200, last_response.status
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
require "timecop"
|
3
|
+
|
4
|
+
describe "fail2ban" do
|
5
|
+
before do
|
6
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
7
|
+
|
8
|
+
Rack::Attack.blocklist("fail2ban pentesters") do |request|
|
9
|
+
Rack::Attack::Fail2Ban.filter(request.ip, maxretry: 2, findtime: 30, bantime: 60) do
|
10
|
+
request.path.include?("private-place")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns OK for many requests to non filtered path" do
|
16
|
+
get "/"
|
17
|
+
assert_equal 200, last_response.status
|
18
|
+
|
19
|
+
get "/"
|
20
|
+
assert_equal 200, last_response.status
|
21
|
+
end
|
22
|
+
|
23
|
+
it "forbids access to private path" do
|
24
|
+
get "/private-place"
|
25
|
+
assert_equal 403, last_response.status
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns OK for non filtered path if yet not reached maxretry limit" do
|
29
|
+
get "/private-place"
|
30
|
+
assert_equal 403, last_response.status
|
31
|
+
|
32
|
+
get "/"
|
33
|
+
assert_equal 200, last_response.status
|
34
|
+
end
|
35
|
+
|
36
|
+
it "forbids all access after reaching maxretry limit" do
|
37
|
+
get "/private-place"
|
38
|
+
assert_equal 403, last_response.status
|
39
|
+
|
40
|
+
get "/private-place"
|
41
|
+
assert_equal 403, last_response.status
|
42
|
+
|
43
|
+
get "/"
|
44
|
+
assert_equal 403, last_response.status
|
45
|
+
end
|
46
|
+
|
47
|
+
it "restores access after bantime elapsed" do
|
48
|
+
get "/private-place"
|
49
|
+
assert_equal 403, last_response.status
|
50
|
+
|
51
|
+
get "/private-place"
|
52
|
+
assert_equal 403, last_response.status
|
53
|
+
|
54
|
+
get "/"
|
55
|
+
assert_equal 403, last_response.status
|
56
|
+
|
57
|
+
Timecop.travel(60) do
|
58
|
+
get "/"
|
59
|
+
|
60
|
+
assert_equal 200, last_response.status
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "does not forbid all access if maxrety condition is met but not within the findtime timespan" do
|
65
|
+
get "/private-place"
|
66
|
+
assert_equal 403, last_response.status
|
67
|
+
|
68
|
+
Timecop.travel(31) do
|
69
|
+
get "/private-place"
|
70
|
+
assert_equal 403, last_response.status
|
71
|
+
|
72
|
+
get "/"
|
73
|
+
assert_equal 200, last_response.status
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "Safelist an IP" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.blocklist("admin") do |request|
|
6
|
+
request.path == "/admin"
|
7
|
+
end
|
8
|
+
|
9
|
+
Rack::Attack.safelist_ip("5.6.7.8")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "forbids request if blocklist condition is true and safelist is false" do
|
13
|
+
get "/admin", {}, "REMOTE_ADDR" => "1.2.3.4"
|
14
|
+
|
15
|
+
assert_equal 403, last_response.status
|
16
|
+
end
|
17
|
+
|
18
|
+
it "succeeds if blocklist condition is false and safelist is false" do
|
19
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
20
|
+
|
21
|
+
assert_equal 200, last_response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
it "succeeds request if blocklist condition is false and safelist is true" do
|
25
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
26
|
+
|
27
|
+
assert_equal 200, last_response.status
|
28
|
+
end
|
29
|
+
|
30
|
+
it "succeeds request if both blocklist and safelist conditions are true" do
|
31
|
+
get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
|
32
|
+
|
33
|
+
assert_equal 200, last_response.status
|
34
|
+
end
|
35
|
+
|
36
|
+
it "notifies when the request is safe" do
|
37
|
+
notification_type = nil
|
38
|
+
|
39
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
|
40
|
+
notification_type = request.env["rack.attack.match_type"]
|
41
|
+
end
|
42
|
+
|
43
|
+
get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8"
|
44
|
+
|
45
|
+
assert_equal 200, last_response.status
|
46
|
+
assert_equal :safelist, notification_type
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
@@ -34,4 +34,20 @@ describe "#safelist" do
|
|
34
34
|
|
35
35
|
assert_equal 200, last_response.status
|
36
36
|
end
|
37
|
+
|
38
|
+
it "notifies when the request is safe" do
|
39
|
+
notification_matched = nil
|
40
|
+
notification_type = nil
|
41
|
+
|
42
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
|
43
|
+
notification_matched = request.env["rack.attack.matched"]
|
44
|
+
notification_type = request.env["rack.attack.match_type"]
|
45
|
+
end
|
46
|
+
|
47
|
+
get "/safe_space", {}, "REMOTE_ADDR" => "1.2.3.4"
|
48
|
+
|
49
|
+
assert_equal 200, last_response.status
|
50
|
+
assert_equal "safe path", notification_matched
|
51
|
+
assert_equal :safelist, notification_type
|
52
|
+
end
|
37
53
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative "../spec_helper"
|
2
|
+
|
3
|
+
describe "Safelisting an IP subnet" do
|
4
|
+
before do
|
5
|
+
Rack::Attack.blocklist("admin") do |request|
|
6
|
+
request.path == "/admin"
|
7
|
+
end
|
8
|
+
|
9
|
+
Rack::Attack.safelist_ip("5.6.0.0/16")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "forbids request if blocklist condition is true and safelist is false" do
|
13
|
+
get "/admin", {}, "REMOTE_ADDR" => "5.7.0.0"
|
14
|
+
|
15
|
+
assert_equal 403, last_response.status
|
16
|
+
end
|
17
|
+
|
18
|
+
it "succeeds if blocklist condition is false and safelist is false" do
|
19
|
+
get "/", {}, "REMOTE_ADDR" => "5.7.0.0"
|
20
|
+
|
21
|
+
assert_equal 200, last_response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
it "succeeds request if blocklist condition is false and safelist is true" do
|
25
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.0.0"
|
26
|
+
|
27
|
+
assert_equal 200, last_response.status
|
28
|
+
end
|
29
|
+
|
30
|
+
it "succeeds request if both blocklist and safelist conditions are true" do
|
31
|
+
get "/admin", {}, "REMOTE_ADDR" => "5.6.255.255"
|
32
|
+
|
33
|
+
assert_equal 200, last_response.status
|
34
|
+
end
|
35
|
+
|
36
|
+
it "notifies when the request is safe" do
|
37
|
+
notification_type = nil
|
38
|
+
|
39
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
|
40
|
+
notification_type = request.env["rack.attack.match_type"]
|
41
|
+
end
|
42
|
+
|
43
|
+
get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0"
|
44
|
+
|
45
|
+
assert_equal 200, last_response.status
|
46
|
+
assert_equal :safelist, notification_type
|
47
|
+
end
|
48
|
+
end
|
@@ -2,9 +2,11 @@ require_relative "../spec_helper"
|
|
2
2
|
require "timecop"
|
3
3
|
|
4
4
|
describe "#throttle" do
|
5
|
-
|
5
|
+
before do
|
6
6
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
7
|
+
end
|
7
8
|
|
9
|
+
it "allows one request per minute by IP" do
|
8
10
|
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
9
11
|
request.ip
|
10
12
|
end
|
@@ -16,6 +18,8 @@ describe "#throttle" do
|
|
16
18
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
17
19
|
|
18
20
|
assert_equal 429, last_response.status
|
21
|
+
assert_equal "60", last_response.headers["Retry-After"]
|
22
|
+
assert_equal "Retry later\n", last_response.body
|
19
23
|
|
20
24
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
21
25
|
|
@@ -27,4 +31,129 @@ describe "#throttle" do
|
|
27
31
|
assert_equal 200, last_response.status
|
28
32
|
end
|
29
33
|
end
|
34
|
+
|
35
|
+
it "supports limit to be dynamic" do
|
36
|
+
# Could be used to have different rate limits for authorized
|
37
|
+
# vs general requests
|
38
|
+
limit_proc = lambda do |request|
|
39
|
+
if request.env["X-APIKey"] == "private-secret"
|
40
|
+
2
|
41
|
+
else
|
42
|
+
1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
Rack::Attack.throttle("by ip", limit: limit_proc, period: 60) do |request|
|
47
|
+
request.ip
|
48
|
+
end
|
49
|
+
|
50
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
51
|
+
assert_equal 200, last_response.status
|
52
|
+
|
53
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
54
|
+
assert_equal 429, last_response.status
|
55
|
+
|
56
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
|
57
|
+
assert_equal 200, last_response.status
|
58
|
+
|
59
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
|
60
|
+
assert_equal 200, last_response.status
|
61
|
+
|
62
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
|
63
|
+
assert_equal 429, last_response.status
|
64
|
+
end
|
65
|
+
|
66
|
+
it "supports period to be dynamic" do
|
67
|
+
# Could be used to have different rate limits for authorized
|
68
|
+
# vs general requests
|
69
|
+
period_proc = lambda do |request|
|
70
|
+
if request.env["X-APIKey"] == "private-secret"
|
71
|
+
10
|
72
|
+
else
|
73
|
+
30
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Rack::Attack.throttle("by ip", limit: 1, period: period_proc) do |request|
|
78
|
+
request.ip
|
79
|
+
end
|
80
|
+
|
81
|
+
# Using Time#at to align to start/end of periods exactly
|
82
|
+
# to achieve consistenty in different test runs
|
83
|
+
|
84
|
+
Timecop.travel(Time.at(0)) do
|
85
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
86
|
+
assert_equal 200, last_response.status
|
87
|
+
|
88
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
89
|
+
assert_equal 429, last_response.status
|
90
|
+
end
|
91
|
+
|
92
|
+
Timecop.travel(Time.at(10)) do
|
93
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
94
|
+
assert_equal 429, last_response.status
|
95
|
+
end
|
96
|
+
|
97
|
+
Timecop.travel(Time.at(30)) do
|
98
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
99
|
+
assert_equal 200, last_response.status
|
100
|
+
end
|
101
|
+
|
102
|
+
Timecop.travel(Time.at(0)) do
|
103
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
|
104
|
+
assert_equal 200, last_response.status
|
105
|
+
|
106
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
|
107
|
+
assert_equal 429, last_response.status
|
108
|
+
end
|
109
|
+
|
110
|
+
Timecop.travel(Time.at(10)) do
|
111
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8", "X-APIKey" => "private-secret"
|
112
|
+
assert_equal 200, last_response.status
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it "notifies when the request is throttled" do
|
117
|
+
Rack::Attack.throttle("by ip", limit: 1, period: 60) do |request|
|
118
|
+
request.ip
|
119
|
+
end
|
120
|
+
|
121
|
+
notification_matched = nil
|
122
|
+
notification_type = nil
|
123
|
+
notification_data = nil
|
124
|
+
notification_discriminator = nil
|
125
|
+
|
126
|
+
ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request|
|
127
|
+
notification_matched = request.env["rack.attack.matched"]
|
128
|
+
notification_type = request.env["rack.attack.match_type"]
|
129
|
+
notification_data = request.env['rack.attack.match_data']
|
130
|
+
notification_discriminator = request.env['rack.attack.match_discriminator']
|
131
|
+
end
|
132
|
+
|
133
|
+
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
134
|
+
|
135
|
+
assert_equal 200, last_response.status
|
136
|
+
assert_nil notification_matched
|
137
|
+
assert_nil notification_type
|
138
|
+
assert_nil notification_data
|
139
|
+
assert_nil notification_discriminator
|
140
|
+
|
141
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
142
|
+
|
143
|
+
assert_equal 200, last_response.status
|
144
|
+
assert_nil notification_matched
|
145
|
+
assert_nil notification_type
|
146
|
+
assert_nil notification_data
|
147
|
+
assert_nil notification_discriminator
|
148
|
+
|
149
|
+
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
150
|
+
|
151
|
+
assert_equal 429, last_response.status
|
152
|
+
assert_equal "by ip", notification_matched
|
153
|
+
assert_equal :throttle, notification_type
|
154
|
+
assert_equal 60, notification_data[:period]
|
155
|
+
assert_equal 1, notification_data[:limit]
|
156
|
+
assert_equal 2, notification_data[:count]
|
157
|
+
assert_equal "1.2.3.4", notification_discriminator
|
158
|
+
end
|
30
159
|
end
|