rack-attack 5.0.1 → 5.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +190 -94
  3. data/Rakefile +11 -4
  4. data/bin/setup +8 -0
  5. data/lib/rack/attack.rb +83 -51
  6. data/lib/rack/attack/allow2ban.rb +2 -1
  7. data/lib/rack/attack/blocklist.rb +0 -1
  8. data/lib/rack/attack/cache.rb +24 -5
  9. data/lib/rack/attack/check.rb +6 -8
  10. data/lib/rack/attack/fail2ban.rb +2 -1
  11. data/lib/rack/attack/path_normalizer.rb +6 -11
  12. data/lib/rack/attack/safelist.rb +0 -1
  13. data/lib/rack/attack/store_proxy.rb +3 -12
  14. data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
  15. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +4 -5
  16. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +19 -0
  17. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +35 -0
  18. data/lib/rack/attack/store_proxy/redis_proxy.rb +54 -0
  19. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -24
  20. data/lib/rack/attack/throttle.rb +16 -12
  21. data/lib/rack/attack/track.rb +3 -3
  22. data/lib/rack/attack/version.rb +1 -1
  23. data/spec/acceptance/allow2ban_spec.rb +71 -0
  24. data/spec/acceptance/blocking_ip_spec.rb +38 -0
  25. data/spec/acceptance/blocking_spec.rb +41 -0
  26. data/spec/acceptance/blocking_subnet_spec.rb +44 -0
  27. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +126 -0
  28. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +121 -0
  29. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +48 -0
  30. data/spec/acceptance/cache_store_config_with_rails_spec.rb +31 -0
  31. data/spec/acceptance/customizing_blocked_response_spec.rb +41 -0
  32. data/spec/acceptance/customizing_throttled_response_spec.rb +59 -0
  33. data/spec/acceptance/extending_request_object_spec.rb +34 -0
  34. data/spec/acceptance/fail2ban_spec.rb +76 -0
  35. data/spec/acceptance/safelisting_ip_spec.rb +48 -0
  36. data/spec/acceptance/safelisting_spec.rb +53 -0
  37. data/spec/acceptance/safelisting_subnet_spec.rb +48 -0
  38. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +19 -0
  39. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +22 -0
  40. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +18 -0
  41. data/spec/acceptance/stores/active_support_memory_store_spec.rb +16 -0
  42. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +18 -0
  43. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +18 -0
  44. data/spec/acceptance/stores/active_support_redis_store_spec.rb +18 -0
  45. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +22 -0
  46. data/spec/acceptance/stores/dalli_client_spec.rb +19 -0
  47. data/spec/acceptance/stores/redis_spec.rb +20 -0
  48. data/spec/acceptance/stores/redis_store_spec.rb +18 -0
  49. data/spec/acceptance/throttling_spec.rb +159 -0
  50. data/spec/acceptance/track_spec.rb +27 -0
  51. data/spec/acceptance/track_throttle_spec.rb +53 -0
  52. data/spec/allow2ban_spec.rb +9 -8
  53. data/spec/fail2ban_spec.rb +11 -9
  54. data/spec/integration/offline_spec.rb +21 -23
  55. data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
  56. data/spec/rack_attack_request_spec.rb +1 -1
  57. data/spec/rack_attack_spec.rb +13 -14
  58. data/spec/rack_attack_throttle_spec.rb +28 -18
  59. data/spec/rack_attack_track_spec.rb +11 -8
  60. data/spec/spec_helper.rb +35 -14
  61. data/spec/support/cache_store_helper.rb +82 -0
  62. metadata +150 -65
  63. data/spec/integration/rack_attack_cache_spec.rb +0 -122
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0d4400f3695de545e524a11ed1eaa0ae14a5f3d2
4
- data.tar.gz: 76c98ade06cea2447a46de122ab23dbc858b8c1d
2
+ SHA256:
3
+ metadata.gz: e666812691cc414692f7125979f0b152a9111ccee075e65b811fa4a6d8770daa
4
+ data.tar.gz: 3e8caba79f7ad09d4999cce6358de9cc29b815dee3c9f9c5adbf12763c764656
5
5
  SHA512:
6
- metadata.gz: 44d33cd6011e99ec3ca5cbdd8d1bcdecb8f2463fbd55e22f0266f0b17d660416f68d7ce46ac6f318549edc25b937ea3f0f73df14e9865dcb26f0d935f43a3682
7
- data.tar.gz: 1be320077b7533ad2edb48a7799ee9681641beeb4a0232d2079f664ac73494c3cd5c50f403405656177e70a377435834b1b8af7fc922ecbd3bc72165f8659bf5
6
+ metadata.gz: f630c0cd1a34bd588e616653a2e6795e2ec6baafc0e0df8b489e6aa451cf47fb64065447fb3ceb2b029a51d87a0393d6d44cea02e58423626ea46165531f7da3
7
+ data.tar.gz: 22efc414db06b0a1bbbf8e6d34a3e0d0ead64f1832f48dc30af7ec0a374ef215d53cacf0a8e95c842bff0af84df0939f27820e4668c739eb8db3c51ebba4088e
data/README.md CHANGED
@@ -1,105 +1,106 @@
1
- # Rack::Attack!!!
2
- *Rack middleware for blocking & throttling abusive requests*
3
-
4
- Rack::Attack is a rack middleware to protect your web app from bad clients.
5
- It allows *safelisting*, *blocklisting*, *throttling*, and *tracking* based on arbitrary properties of the request.
1
+ # Rack::Attack
6
2
 
7
- Throttle and fail2ban state is stored in a configurable cache (e.g. `Rails.cache`), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
3
+ *Rack middleware for blocking & throttling abusive requests*
8
4
 
9
- See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
5
+ Protect your Rails and Rack apps from bad clients. Rack::Attack lets you easily decide when to *allow*, *block* and *throttle* based on properties of the request.
10
6
 
11
- [![Gem Version](https://badge.fury.io/rb/rack-attack.png)](http://badge.fury.io/rb/rack-attack)
12
- [![Build Status](https://travis-ci.org/kickstarter/rack-attack.png?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
13
- [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.png)](https://codeclimate.com/github/kickstarter/rack-attack)
7
+ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.
14
8
 
9
+ [![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)
10
+ [![Build Status](https://travis-ci.org/kickstarter/rack-attack.svg?branch=master)](https://travis-ci.org/kickstarter/rack-attack)
11
+ [![Code Climate](https://codeclimate.com/github/kickstarter/rack-attack.svg)](https://codeclimate.com/github/kickstarter/rack-attack)
15
12
 
16
13
  ## Getting started
17
14
 
18
- Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to your Gemfile with bundler:
15
+ ### 1. Installing
16
+
17
+ Add this line to your application's Gemfile:
19
18
 
20
19
  ```ruby
21
20
  # In your Gemfile
21
+
22
22
  gem 'rack-attack'
23
23
  ```
24
- Tell your app to use the Rack::Attack middleware.
25
- For Rails 3+ apps:
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install rack-attack
32
+
33
+ ### 2. Plugging into the application
34
+
35
+ Then tell your ruby web application to use rack-attack as a middleware.
36
+
37
+ a) For __rails__ applications:
26
38
 
27
39
  ```ruby
28
40
  # In config/application.rb
41
+
29
42
  config.middleware.use Rack::Attack
30
43
  ```
31
44
 
32
- Or for Rackup files:
45
+ b) For __rack__ applications:
33
46
 
34
47
  ```ruby
35
48
  # In config.ru
49
+
50
+ require "rack/attack"
36
51
  use Rack::Attack
37
52
  ```
38
53
 
39
- Add a `rack-attack.rb` file to `config/initializers/`:
40
- ```ruby
41
- # In config/initializers/rack-attack.rb
42
- class Rack::Attack
43
- # your custom configuration...
44
- end
45
- ```
54
+ __IMPORTANT__: By default, rack-attack won't perform any blocking or throttling, until you specifically tell it what to protect against by configuring some rules.
55
+
56
+ ## Usage
46
57
 
47
58
  *Tip:* The example in the wiki is a great way to get started:
48
59
  [Example Configuration](https://github.com/kickstarter/rack-attack/wiki/Example-Configuration)
49
60
 
50
- Optionally configure the cache store for throttling or fail2ban filtering:
61
+ Define rules by calling `Rack::Attack` public methods, in any file that runs when your application is being initialized. For rails applications this means creating a new file named `config/initializers/rack_attack.rb` and writing your rules there.
51
62
 
52
- ```ruby
53
- Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
54
- ```
63
+ ### Safelisting
55
64
 
56
- Note that `Rack::Attack.cache` is only used for throttling and fail2ban filtering; not blocklisting & safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
65
+ Safelists have the most precedence, so any request matching a safelist would be allowed despite matching any number of blocklists or throttles.
57
66
 
58
- ## How it works
67
+ #### `safelist_ip(ip_address_string)`
59
68
 
60
- The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.
69
+ E.g.
61
70
 
62
- * If the request matches any **safelist**, it is allowed.
63
- * Otherwise, if the request matches any **blocklist**, it is blocked.
64
- * Otherwise, if the request matches any **throttle**, a counter is incremented in the Rack::Attack.cache. If any throttle's limit is exceeded, the request is blocked.
65
- * Otherwise, all **tracks** are checked, and the request is allowed.
71
+ ```ruby
72
+ # config/initializers/rack_attack.rb (for rails app)
66
73
 
67
- The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
74
+ Rack::Attack.safelist_ip("5.6.7.8")
75
+ ```
76
+
77
+ #### `safelist_ip(ip_subnet_string)`
78
+
79
+ E.g.
68
80
 
69
81
  ```ruby
70
- def call(env)
71
- req = Rack::Attack::Request.new(env)
82
+ # config/initializers/rack_attack.rb (for rails app)
72
83
 
73
- if safelisted?(req)
74
- @app.call(env)
75
- elsif blocklisted?(req)
76
- self.class.blocklisted_response.call(env)
77
- elsif throttled?(req)
78
- self.class.throttled_response.call(env)
79
- else
80
- tracked?(req)
81
- @app.call(env)
82
- end
83
- end
84
+ Rack::Attack.safelist_ip("5.6.7.0/24")
84
85
  ```
85
86
 
86
- Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you
87
- can cleanly monkey patch helper methods onto the
88
- [request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
87
+ #### `safelist(name, &block)`
89
88
 
90
- ## About Tracks
89
+ Name your custom safelist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
91
90
 
92
- `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
91
+ The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
93
92
 
94
- ## Usage
93
+ E.g.
95
94
 
96
- Define safelists, blocklists, throttles, and tracks as blocks that return truthy values if matched, falsy otherwise. In a Rails app
97
- these go in an initializer in `config/initializers/`.
98
- A [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request) object is passed to the block (named 'req' in the examples).
95
+ ```ruby
96
+ # config/initializers/rack_attack.rb (for rails apps)
99
97
 
100
- ### Safelists
98
+ # Provided that trusted users use an HTTP request header named APIKey
99
+ Rack::Attack.safelist("mark any authenticated access safe") do |request|
100
+ # Requests are allowed if the return value is truthy
101
+ request.env["APIKey"] == "secret-string"
102
+ end
101
103
 
102
- ```ruby
103
104
  # Always allow requests from localhost
104
105
  # (blocklist & throttles are skipped)
105
106
  Rack::Attack.safelist('allow from localhost') do |req|
@@ -108,16 +109,44 @@ Rack::Attack.safelist('allow from localhost') do |req|
108
109
  end
109
110
  ```
110
111
 
111
- ### Blocklists
112
+ ### Blocking
113
+
114
+ #### `blocklist_ip(ip_address_string)`
115
+
116
+ E.g.
117
+
118
+ ```ruby
119
+ # config/initializers/rack_attack.rb (for rails apps)
120
+
121
+ Rack::Attack.blocklist_ip("1.2.3.4")
122
+ ```
123
+
124
+ #### `blocklist_ip(ip_subnet_string)`
125
+
126
+ E.g.
127
+
128
+ ```ruby
129
+ # config/initializers/rack_attack.rb (for rails apps)
130
+
131
+ Rack::Attack.blocklist_ip("1.2.0.0/16")
132
+ ```
133
+
134
+ #### `blocklist(name, &block)`
135
+
136
+ Name your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.
137
+
138
+ The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
139
+
140
+ E.g.
112
141
 
113
142
  ```ruby
114
- # Block requests from 1.2.3.4
115
- Rack::Attack.blocklist('block 1.2.3.4') do |req|
143
+ # config/initializers/rack_attack.rb (for rails apps)
144
+
145
+ Rack::Attack.blocklist("block all access to admin") do |request|
116
146
  # Requests are blocked if the return value is truthy
117
- '1.2.3.4' == req.ip
147
+ request.path.start_with?("/admin")
118
148
  end
119
149
 
120
- # Block logins from a bad user agent
121
150
  Rack::Attack.blocklist('block bad UA logins') do |req|
122
151
  req.path == '/login' && req.post? && req.user_agent == 'BadUA'
123
152
  end
@@ -126,17 +155,19 @@ end
126
155
  #### Fail2Ban
127
156
 
128
157
  `Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
129
- This pattern is inspired by [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page).
130
- See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
158
+ This pattern is inspired by [fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page).
159
+ See the [fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on
131
160
  how the parameters work. For multiple filters, be sure to put each filter in a separate blocklist and use a unique discriminator for each fail2ban filter.
132
161
 
162
+ Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
163
+
133
164
  ```ruby
134
165
  # Block suspicious requests for '/etc/password' or wordpress specific paths.
135
166
  # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
136
167
  Rack::Attack.blocklist('fail2ban pentesters') do |req|
137
168
  # `filter` returns truthy value if request fails, or if it's from a previously banned IP
138
169
  # so the request is blocked
139
- Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
170
+ Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do
140
171
  # The count for the IP is incremented if the return value is truthy
141
172
  CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
142
173
  req.path.include?('/etc/passwd') ||
@@ -150,8 +181,12 @@ end
150
181
  Note that `Fail2Ban` filters are not automatically scoped to the blocklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. `"pentest:#{req.ip}"`.
151
182
 
152
183
  #### Allow2Ban
184
+
153
185
  `Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
154
186
  clients until such time as they reach maxretry at which they are cut off as per normal.
187
+
188
+ Allow2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
189
+
155
190
  ```ruby
156
191
  # Lockout IP addresses that are hammering your login page.
157
192
  # After 20 requests in 1 minute, block all requests from that IP for 1 hour.
@@ -159,40 +194,47 @@ Rack::Attack.blocklist('allow2ban login scrapers') do |req|
159
194
  # `filter` returns false value if request is to your login page (but still
160
195
  # increments the count) so request below the limit are not blocked until
161
196
  # they hit the limit. At that point, filter will return true and block.
162
- Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do
197
+ Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 20, findtime: 1.minute, bantime: 1.hour) do
163
198
  # The count for the IP is incremented if the return value is truthy.
164
199
  req.path == '/login' and req.post?
165
200
  end
166
201
  end
167
202
  ```
168
203
 
204
+ ### Throttling
205
+
206
+ Throttle state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
207
+
208
+ #### `throttle(name, options, &block)`
169
209
 
170
- ### Throttles
210
+ Name your custom throttle, provide `limit` and `period` as options, and make your ruby-block argument return the __discriminator__. This discriminator is how you tell rack-attack whether you're limiting per IP address, per user email or any other.
211
+
212
+ The request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).
213
+
214
+ E.g.
171
215
 
172
216
  ```ruby
173
- # Throttle requests to 5 requests per second per ip
174
- Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
175
- # If the return value is truthy, the cache key for the return value
176
- # is incremented and compared with the limit. In this case:
177
- # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
178
- #
179
- # If falsy, the cache key is neither incremented nor checked.
180
-
181
- req.ip
217
+ # config/initializers/rack_attack.rb (for rails apps)
218
+
219
+ Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
220
+ request.ip
182
221
  end
183
222
 
184
223
  # Throttle login attempts for a given email parameter to 6 reqs/minute
185
224
  # Return the email as a discriminator on POST /login requests
186
- Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
187
- req.params['email'] if req.path == '/login' && req.post?
225
+ Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
226
+ if req.path == '/login' && req.post?
227
+ req.params['email']
228
+ end
188
229
  end
189
230
 
190
231
  # You can also set a limit and period using a proc. For instance, after
191
232
  # Rack::Auth::Basic has authenticated the user:
192
- limit_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
193
- period_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 1.second : 1.minute}
194
- Rack::Attack.throttle('req/ip', :limit => limit_proc, :period => period_proc) do |req|
195
- req.ip
233
+ limit_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 100 : 1 }
234
+ period_proc = proc { |req| req.env["REMOTE_USER"] == "admin" ? 1 : 60 }
235
+
236
+ Rack::Attack.throttle('request per ip', limit: limit_proc, period: period_proc) do |request|
237
+ request.ip
196
238
  end
197
239
  ```
198
240
 
@@ -205,7 +247,7 @@ Rack::Attack.track("special_agent") do |req|
205
247
  end
206
248
 
207
249
  # Supports optional limit and period, triggers the notification only when the limit is reached.
208
- Rack::Attack.track("special_agent", :limit => 6, :period => 60.seconds) do |req|
250
+ Rack::Attack.track("special_agent", limit: 6, period: 60) do |req|
209
251
  req.user_agent == "SpecialAgent"
210
252
  end
211
253
 
@@ -218,9 +260,19 @@ ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, r
218
260
  end
219
261
  ```
220
262
 
221
- ## Responses
263
+ ### Cache store configuration
264
+
265
+ Throttle, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).
266
+
267
+ ```ruby
268
+ Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
269
+ ```
270
+
271
+ Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
272
+
273
+ ## Customizing responses
222
274
 
223
- Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://rack.rubyforge.org/doc/SPEC.html).
275
+ Customize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://www.rubydoc.info/github/rack/rack/file/SPEC).
224
276
 
225
277
  ```ruby
226
278
  Rack::Attack.blocklisted_response = lambda do |env|
@@ -233,7 +285,8 @@ Rack::Attack.throttled_response = lambda do |env|
233
285
  # NB: you have access to the name and other data about the matched throttle
234
286
  # env['rack.attack.matched'],
235
287
  # env['rack.attack.match_type'],
236
- # env['rack.attack.match_data']
288
+ # env['rack.attack.match_data'],
289
+ # env['rack.attack.match_discriminator']
237
290
 
238
291
  # Using 503 because it may make attacker think that they have successfully
239
292
  # DOSed the site. Rack::Attack returns 429 for throttling by default
@@ -250,13 +303,13 @@ Here's an example response that includes conventional `X-RateLimit-*` headers:
250
303
 
251
304
  ```ruby
252
305
  Rack::Attack.throttled_response = lambda do |env|
253
- now = Time.now
254
306
  match_data = env['rack.attack.match_data']
307
+ now = match_data[:epoch_time]
255
308
 
256
309
  headers = {
257
310
  'X-RateLimit-Limit' => match_data[:limit].to_s,
258
311
  'X-RateLimit-Remaining' => '0',
259
- 'X-RateLimit-Reset' => (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
312
+ 'X-RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s
260
313
  }
261
314
 
262
315
  [ 429, headers, ["Throttled\n"]]
@@ -267,7 +320,7 @@ end
267
320
  For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
268
321
 
269
322
  ```ruby
270
- request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
323
+ request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l, :epoch_time => t }
271
324
  ```
272
325
 
273
326
  ## Logging & Instrumentation
@@ -282,6 +335,43 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
282
335
  end
283
336
  ```
284
337
 
338
+ ## How it works
339
+
340
+ The Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.
341
+
342
+ * If the request matches any **safelist**, it is allowed.
343
+ * Otherwise, if the request matches any **blocklist**, it is blocked.
344
+ * Otherwise, if the request matches any **throttle**, a counter is incremented in the Rack::Attack.cache. If any throttle's limit is exceeded, the request is blocked.
345
+ * Otherwise, all **tracks** are checked, and the request is allowed.
346
+
347
+ The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
348
+
349
+ ```ruby
350
+ def call(env)
351
+ req = Rack::Attack::Request.new(env)
352
+
353
+ if safelisted?(req)
354
+ @app.call(env)
355
+ elsif blocklisted?(req)
356
+ self.class.blocklisted_response.call(env)
357
+ elsif throttled?(req)
358
+ self.class.throttled_response.call(env)
359
+ else
360
+ tracked?(req)
361
+ @app.call(env)
362
+ end
363
+ end
364
+ ```
365
+
366
+ Note: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you
367
+ can cleanly monkey patch helper methods onto the
368
+ [request object](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack/request.rb).
369
+
370
+ ### About Tracks
371
+
372
+ `Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.
373
+
374
+
285
375
  ## Testing
286
376
 
287
377
  A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
@@ -298,7 +388,7 @@ so try to keep the number of throttle checks per request low.
298
388
  If a request is blocklisted or throttled, the response is a very simple Rack response.
299
389
  A single typical ruby web server thread can block several hundred requests per second.
300
390
 
301
- Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
391
+ Rack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).
302
392
 
303
393
  ## Motivation
304
394
 
@@ -312,9 +402,15 @@ less on short-term, one-off hacks to block a particular attack.
312
402
 
313
403
  ## Contributing
314
404
 
315
- Pull requests and issues are greatly appreciated. This project is intended to be
316
- a safe, welcoming space for collaboration, and contributors are expected to
317
- adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
405
+ Check out the [Contributing guide](CONTRIBUTING.md).
406
+
407
+ ## Code of Conduct
408
+
409
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).
410
+
411
+ ## Development setup
412
+
413
+ Check out the [Development guide](docs/development.md).
318
414
 
319
415
  ## Mailing list
320
416
 
@@ -327,4 +423,4 @@ New releases of Rack::Attack are announced on
327
423
 
328
424
  Copyright Kickstarter, PBC.
329
425
 
330
- Released under an [MIT License](http://opensource.org/licenses/MIT).
426
+ Released under an [MIT License](https://opensource.org/licenses/MIT).