rack-attack 6.7.0 → 6.8.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 +5 -6
- data/lib/rack/attack/base_proxy.rb +1 -0
- data/lib/rack/attack/cache.rb +1 -1
- data/lib/rack/attack/configuration.rb +7 -3
- data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +16 -10
- data/lib/rack/attack/store_proxy/redis_proxy.rb +2 -1
- data/lib/rack/attack/throttle.rb +2 -1
- data/lib/rack/attack/version.rb +1 -1
- data/lib/rack/attack.rb +3 -1
- data/spec/acceptance/blocking_ip_spec.rb +13 -8
- data/spec/acceptance/blocking_spec.rb +16 -18
- data/spec/acceptance/blocking_subnet_spec.rb +7 -8
- data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +5 -3
- data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +7 -5
- data/spec/acceptance/cache_store_config_for_throttle_spec.rb +5 -3
- data/spec/acceptance/cache_store_config_with_rails_spec.rb +6 -4
- data/spec/acceptance/extending_request_object_spec.rb +3 -7
- data/spec/acceptance/fail2ban_spec.rb +42 -0
- data/spec/acceptance/safelisting_ip_spec.rb +12 -4
- data/spec/acceptance/safelisting_spec.rb +14 -14
- data/spec/acceptance/safelisting_subnet_spec.rb +6 -4
- data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +5 -2
- data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +0 -1
- data/spec/acceptance/stores/active_support_memory_store_spec.rb +0 -2
- data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +5 -2
- data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +0 -1
- data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +0 -1
- data/spec/acceptance/stores/dalli_client_spec.rb +0 -1
- data/spec/acceptance/stores/redis_spec.rb +0 -1
- data/spec/acceptance/stores/redis_store_spec.rb +1 -3
- data/spec/acceptance/throttling_spec.rb +14 -23
- data/spec/acceptance/track_spec.rb +8 -9
- data/spec/acceptance/track_throttle_spec.rb +10 -16
- data/spec/configuration_spec.rb +33 -0
- data/spec/integration/offline_spec.rb +0 -12
- data/spec/rack_attack_instrumentation_spec.rb +24 -28
- data/spec/rack_attack_request_spec.rb +2 -4
- data/spec/rack_attack_reset_spec.rb +90 -0
- data/spec/rack_attack_spec.rb +0 -22
- data/spec/rack_attack_throttle_spec.rb +49 -28
- data/spec/rack_attack_track_spec.rb +4 -17
- data/spec/spec_helper.rb +4 -3
- data/spec/support/cache_store_helper.rb +31 -25
- data/spec/support/freeze_time_helper.rb +9 -0
- metadata +41 -15
- data/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +0 -39
- data/spec/acceptance/stores/active_support_dalli_store_spec.rb +0 -25
- data/spec/acceptance/stores/active_support_redis_store_spec.rb +0 -20
@@ -4,11 +4,14 @@ require_relative "../../spec_helper"
|
|
4
4
|
|
5
5
|
if defined?(::ConnectionPool) && defined?(::Dalli)
|
6
6
|
require_relative "../../support/cache_store_helper"
|
7
|
-
require "timecop"
|
8
7
|
|
9
8
|
describe "ActiveSupport::Cache::MemCacheStore (pooled) as a cache backend" do
|
10
9
|
before do
|
11
|
-
Rack::Attack.cache.store = ActiveSupport::
|
10
|
+
Rack::Attack.cache.store = if ActiveSupport.gem_version >= Gem::Version.new("7.2.0")
|
11
|
+
ActiveSupport::Cache::MemCacheStore.new(pool: true)
|
12
|
+
else
|
13
|
+
ActiveSupport::Cache::MemCacheStore.new(pool_size: 2)
|
14
|
+
end
|
12
15
|
end
|
13
16
|
|
14
17
|
after do
|
@@ -10,11 +10,14 @@ should_run =
|
|
10
10
|
|
11
11
|
if should_run
|
12
12
|
require_relative "../../support/cache_store_helper"
|
13
|
-
require "timecop"
|
14
13
|
|
15
14
|
describe "ActiveSupport::Cache::RedisCacheStore (pooled) as a cache backend" do
|
16
15
|
before do
|
17
|
-
Rack::Attack.cache.store = ActiveSupport::
|
16
|
+
Rack::Attack.cache.store = if ActiveSupport.gem_version >= Gem::Version.new("7.2.0")
|
17
|
+
ActiveSupport::Cache::RedisCacheStore.new(pool: true)
|
18
|
+
else
|
19
|
+
ActiveSupport::Cache::RedisCacheStore.new(pool_size: 2)
|
20
|
+
end
|
18
21
|
end
|
19
22
|
|
20
23
|
after do
|
@@ -4,9 +4,7 @@ require_relative "../../spec_helper"
|
|
4
4
|
require_relative "../../support/cache_store_helper"
|
5
5
|
|
6
6
|
if defined?(::Redis::Store)
|
7
|
-
|
8
|
-
|
9
|
-
describe "ActiveSupport::Cache::RedisStore as a cache backend" do
|
7
|
+
describe "Redis::Store as a cache backend" do
|
10
8
|
before do
|
11
9
|
Rack::Attack.cache.store = ::Redis::Store.new
|
12
10
|
end
|
@@ -4,6 +4,8 @@ require_relative "../spec_helper"
|
|
4
4
|
require "timecop"
|
5
5
|
|
6
6
|
describe "#throttle" do
|
7
|
+
let(:notifications) { [] }
|
8
|
+
|
7
9
|
before do
|
8
10
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
9
11
|
end
|
@@ -138,42 +140,31 @@ describe "#throttle" do
|
|
138
140
|
request.ip
|
139
141
|
end
|
140
142
|
|
141
|
-
notification_matched = nil
|
142
|
-
notification_type = nil
|
143
|
-
notification_data = nil
|
144
|
-
notification_discriminator = nil
|
145
|
-
|
146
143
|
ActiveSupport::Notifications.subscribe("throttle.rack_attack") do |_name, _start, _finish, _id, payload|
|
147
|
-
|
148
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
149
|
-
notification_data = payload[:request].env['rack.attack.match_data']
|
150
|
-
notification_discriminator = payload[:request].env['rack.attack.match_discriminator']
|
144
|
+
notifications.push(payload)
|
151
145
|
end
|
152
146
|
|
153
147
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
154
148
|
|
155
149
|
assert_equal 200, last_response.status
|
156
|
-
|
157
|
-
assert_nil notification_type
|
158
|
-
assert_nil notification_data
|
159
|
-
assert_nil notification_discriminator
|
150
|
+
assert notifications.empty?
|
160
151
|
|
161
152
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
162
153
|
|
163
154
|
assert_equal 200, last_response.status
|
164
|
-
|
165
|
-
assert_nil notification_type
|
166
|
-
assert_nil notification_data
|
167
|
-
assert_nil notification_discriminator
|
155
|
+
assert notifications.empty?
|
168
156
|
|
169
157
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
170
158
|
|
171
159
|
assert_equal 429, last_response.status
|
172
|
-
|
173
|
-
assert_equal
|
174
|
-
|
175
|
-
assert_equal
|
176
|
-
assert_equal
|
177
|
-
assert_equal "
|
160
|
+
|
161
|
+
assert_equal 1, notifications.size
|
162
|
+
notification = notifications.pop
|
163
|
+
assert_equal "by ip", notification[:request].env["rack.attack.matched"]
|
164
|
+
assert_equal :throttle, notification[:request].env["rack.attack.match_type"]
|
165
|
+
assert_equal 60, notification[:request].env["rack.attack.match_data"][:period]
|
166
|
+
assert_equal 1, notification[:request].env["rack.attack.match_data"][:limit]
|
167
|
+
assert_equal 2, notification[:request].env["rack.attack.match_data"][:count]
|
168
|
+
assert_equal "1.2.3.4", notification[:request].env["rack.attack.match_discriminator"]
|
178
169
|
end
|
179
170
|
end
|
@@ -3,27 +3,26 @@
|
|
3
3
|
require_relative "../spec_helper"
|
4
4
|
|
5
5
|
describe "#track" do
|
6
|
+
let(:notifications) { [] }
|
7
|
+
|
6
8
|
it "notifies when track block returns true" do
|
7
9
|
Rack::Attack.track("ip 1.2.3.4") do |request|
|
8
10
|
request.ip == "1.2.3.4"
|
9
11
|
end
|
10
12
|
|
11
|
-
notification_matched = nil
|
12
|
-
notification_type = nil
|
13
|
-
|
14
13
|
ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _id, payload|
|
15
|
-
|
16
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
14
|
+
notifications.push(payload)
|
17
15
|
end
|
18
16
|
|
19
17
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
20
18
|
|
21
|
-
|
22
|
-
assert_nil notification_type
|
19
|
+
assert notifications.empty?
|
23
20
|
|
24
21
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
25
22
|
|
26
|
-
assert_equal
|
27
|
-
|
23
|
+
assert_equal 1, notifications.size
|
24
|
+
notification = notifications.pop
|
25
|
+
assert_equal "ip 1.2.3.4", notification[:request].env["rack.attack.matched"]
|
26
|
+
assert_equal :track, notification[:request].env["rack.attack.match_type"]
|
28
27
|
end
|
29
28
|
end
|
@@ -4,6 +4,8 @@ require_relative "../spec_helper"
|
|
4
4
|
require "timecop"
|
5
5
|
|
6
6
|
describe "#track with throttle-ish options" do
|
7
|
+
let(:notifications) { [] }
|
8
|
+
|
7
9
|
it "notifies when throttle goes over the limit without actually throttling requests" do
|
8
10
|
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
9
11
|
|
@@ -11,43 +13,35 @@ describe "#track with throttle-ish options" do
|
|
11
13
|
request.ip
|
12
14
|
end
|
13
15
|
|
14
|
-
notification_matched = nil
|
15
|
-
notification_type = nil
|
16
|
-
|
17
16
|
ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, _id, payload|
|
18
|
-
|
19
|
-
notification_type = payload[:request].env["rack.attack.match_type"]
|
17
|
+
notifications.push(payload)
|
20
18
|
end
|
21
19
|
|
22
20
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
23
21
|
|
24
|
-
|
25
|
-
assert_nil notification_type
|
22
|
+
assert notifications.empty?
|
26
23
|
|
27
24
|
assert_equal 200, last_response.status
|
28
25
|
|
29
26
|
get "/", {}, "REMOTE_ADDR" => "5.6.7.8"
|
30
27
|
|
31
|
-
|
32
|
-
assert_nil notification_type
|
28
|
+
assert notifications.empty?
|
33
29
|
|
34
30
|
assert_equal 200, last_response.status
|
35
31
|
|
36
32
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
37
33
|
|
38
|
-
assert_equal
|
39
|
-
|
34
|
+
assert_equal 1, notifications.size
|
35
|
+
notification = notifications.pop
|
36
|
+
assert_equal "by ip", notification[:request].env["rack.attack.matched"]
|
37
|
+
assert_equal :track, notification[:request].env["rack.attack.match_type"]
|
40
38
|
|
41
39
|
assert_equal 200, last_response.status
|
42
40
|
|
43
41
|
Timecop.travel(60) do
|
44
|
-
notification_matched = nil
|
45
|
-
notification_type = nil
|
46
|
-
|
47
42
|
get "/", {}, "REMOTE_ADDR" => "1.2.3.4"
|
48
43
|
|
49
|
-
|
50
|
-
assert_nil notification_type
|
44
|
+
assert notifications.empty?
|
51
45
|
|
52
46
|
assert_equal 200, last_response.status
|
53
47
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spec_helper"
|
4
|
+
|
5
|
+
describe Rack::Attack::Configuration do
|
6
|
+
subject { Rack::Attack::Configuration.new }
|
7
|
+
|
8
|
+
describe 'attributes' do
|
9
|
+
it 'exposes the safelists attribute' do
|
10
|
+
_(subject.safelists).must_equal({})
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'exposes the blocklists attribute' do
|
14
|
+
_(subject.blocklists).must_equal({})
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'exposes the throttles attribute' do
|
18
|
+
_(subject.throttles).must_equal({})
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'exposes the tracks attribute' do
|
22
|
+
_(subject.tracks).must_equal({})
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'exposes the anonymous_blocklists attribute' do
|
26
|
+
_(subject.anonymous_blocklists).must_equal([])
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'exposes the anonymous_safelists attribute' do
|
30
|
+
_(subject.anonymous_safelists).must_equal([])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -21,18 +21,6 @@ OfflineExamples = Minitest::SharedExamples.new do
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
if defined?(::ActiveSupport::Cache::RedisStore)
|
25
|
-
describe 'when Redis is offline' do
|
26
|
-
include OfflineExamples
|
27
|
-
|
28
|
-
before do
|
29
|
-
@cache = Rack::Attack::Cache.new
|
30
|
-
# Use presumably unused port for Redis client
|
31
|
-
@cache.store = ActiveSupport::Cache::RedisStore.new(host: '127.0.0.1', port: 3333)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
24
|
if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4'
|
37
25
|
describe 'when Redis is offline' do
|
38
26
|
include OfflineExamples
|
@@ -2,42 +2,38 @@
|
|
2
2
|
|
3
3
|
require_relative "spec_helper"
|
4
4
|
require 'active_support'
|
5
|
+
require 'active_support/subscriber'
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
require_relative 'spec_helper'
|
9
|
-
require 'active_support/subscriber'
|
10
|
-
class CustomSubscriber < ActiveSupport::Subscriber
|
11
|
-
@notification_count = 0
|
7
|
+
class CustomSubscriber < ActiveSupport::Subscriber
|
8
|
+
@notification_count = 0
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
class << self
|
11
|
+
attr_accessor :notification_count
|
12
|
+
end
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
end
|
14
|
+
def throttle(_event)
|
15
|
+
self.class.notification_count += 1
|
20
16
|
end
|
17
|
+
end
|
21
18
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
19
|
+
describe 'Rack::Attack.instrument' do
|
20
|
+
before do
|
21
|
+
@period = 60 # Use a long period; failures due to cache key rotation less likely
|
22
|
+
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
23
|
+
Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip }
|
24
|
+
end
|
28
25
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
end
|
26
|
+
describe "with throttling" do
|
27
|
+
before do
|
28
|
+
ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do
|
29
|
+
CustomSubscriber.attach_to("rack_attack")
|
30
|
+
2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' }
|
35
31
|
end
|
32
|
+
end
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
34
|
+
it 'should instrument without error' do
|
35
|
+
_(last_response.status).must_equal 429
|
36
|
+
assert_equal 1, CustomSubscriber.notification_count
|
41
37
|
end
|
42
38
|
end
|
43
39
|
end
|
@@ -5,10 +5,8 @@ require_relative 'spec_helper'
|
|
5
5
|
describe 'Rack::Attack' do
|
6
6
|
describe 'helpers' do
|
7
7
|
before do
|
8
|
-
|
9
|
-
|
10
|
-
ip
|
11
|
-
end
|
8
|
+
Rack::Attack::Request.define_method :remote_ip do
|
9
|
+
ip
|
12
10
|
end
|
13
11
|
|
14
12
|
Rack::Attack.safelist('valid IP') do |req|
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spec_helper"
|
4
|
+
|
5
|
+
describe "Rack::Attack.reset!" do
|
6
|
+
it "raises an error when is not supported by cache store" do
|
7
|
+
Rack::Attack.cache.store = Class.new
|
8
|
+
assert_raises(Rack::Attack::IncompatibleStoreError) do
|
9
|
+
Rack::Attack.reset!
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
if defined?(Redis)
|
14
|
+
it "should delete rack attack keys" do
|
15
|
+
redis = Redis.new
|
16
|
+
redis.set("key", "value")
|
17
|
+
redis.set("#{Rack::Attack.cache.prefix}::key", "value")
|
18
|
+
Rack::Attack.cache.store = redis
|
19
|
+
Rack::Attack.reset!
|
20
|
+
|
21
|
+
_(redis.get("key")).must_equal "value"
|
22
|
+
_(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if defined?(Redis::Store)
|
27
|
+
it "should delete rack attack keys" do
|
28
|
+
redis_store = Redis::Store.new
|
29
|
+
redis_store.set("key", "value")
|
30
|
+
redis_store.set("#{Rack::Attack.cache.prefix}::key", "value")
|
31
|
+
Rack::Attack.cache.store = redis_store
|
32
|
+
Rack::Attack.reset!
|
33
|
+
|
34
|
+
_(redis_store.get("key")).must_equal "value"
|
35
|
+
_(redis_store.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore)
|
40
|
+
it "should delete rack attack keys" do
|
41
|
+
redis_cache_store = ActiveSupport::Cache::RedisCacheStore.new
|
42
|
+
redis_cache_store.write("key", "value")
|
43
|
+
redis_cache_store.write("#{Rack::Attack.cache.prefix}::key", "value")
|
44
|
+
Rack::Attack.cache.store = redis_cache_store
|
45
|
+
Rack::Attack.reset!
|
46
|
+
|
47
|
+
_(redis_cache_store.read("key")).must_equal "value"
|
48
|
+
_(redis_cache_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "with a namespaced cache" do
|
52
|
+
it "should delete rack attack keys" do
|
53
|
+
redis_cache_store = ActiveSupport::Cache::RedisCacheStore.new(namespace: "ns")
|
54
|
+
redis_cache_store.write("key", "value")
|
55
|
+
redis_cache_store.write("#{Rack::Attack.cache.prefix}::key", "value")
|
56
|
+
Rack::Attack.cache.store = redis_cache_store
|
57
|
+
Rack::Attack.reset!
|
58
|
+
|
59
|
+
_(redis_cache_store.read("key")).must_equal "value"
|
60
|
+
_(redis_cache_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if defined?(ActiveSupport::Cache::MemoryStore)
|
66
|
+
it "should delete rack attack keys" do
|
67
|
+
memory_store = ActiveSupport::Cache::MemoryStore.new
|
68
|
+
memory_store.write("key", "value")
|
69
|
+
memory_store.write("#{Rack::Attack.cache.prefix}::key", "value")
|
70
|
+
Rack::Attack.cache.store = memory_store
|
71
|
+
Rack::Attack.reset!
|
72
|
+
|
73
|
+
_(memory_store.read("key")).must_equal "value"
|
74
|
+
_(memory_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "with a namespaced cache" do
|
78
|
+
it "should delete rack attack keys" do
|
79
|
+
memory_store = ActiveSupport::Cache::MemoryStore.new(namespace: "ns")
|
80
|
+
memory_store.write("key", "value")
|
81
|
+
memory_store.write("#{Rack::Attack.cache.prefix}::key", "value")
|
82
|
+
Rack::Attack.cache.store = memory_store
|
83
|
+
Rack::Attack.reset!
|
84
|
+
|
85
|
+
_(memory_store.read("key")).must_equal "value"
|
86
|
+
_(memory_store.read("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/spec/rack_attack_spec.rb
CHANGED
@@ -103,26 +103,4 @@ describe 'Rack::Attack' do
|
|
103
103
|
end
|
104
104
|
end
|
105
105
|
end
|
106
|
-
|
107
|
-
describe 'reset!' do
|
108
|
-
it 'raises an error when is not supported by cache store' do
|
109
|
-
Rack::Attack.cache.store = Class.new
|
110
|
-
assert_raises(Rack::Attack::IncompatibleStoreError) do
|
111
|
-
Rack::Attack.reset!
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
if defined?(Redis)
|
116
|
-
it 'should delete rack attack keys' do
|
117
|
-
redis = Redis.new
|
118
|
-
redis.set('key', 'value')
|
119
|
-
redis.set("#{Rack::Attack.cache.prefix}::key", 'value')
|
120
|
-
Rack::Attack.cache.store = redis
|
121
|
-
Rack::Attack.reset!
|
122
|
-
|
123
|
-
_(redis.get('key')).must_equal 'value'
|
124
|
-
_(redis.get("#{Rack::Attack.cache.prefix}::key")).must_be_nil
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
106
|
end
|