rack-attack 4.3.1 → 5.4.2

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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +230 -113
  3. data/Rakefile +11 -3
  4. data/bin/setup +8 -0
  5. data/lib/rack/attack.rb +121 -48
  6. data/lib/rack/attack/allow2ban.rb +2 -1
  7. data/lib/rack/attack/{whitelist.rb → blocklist.rb} +2 -3
  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 +3 -2
  11. data/lib/rack/attack/path_normalizer.rb +6 -11
  12. data/lib/rack/attack/request.rb +1 -1
  13. data/lib/rack/attack/{blacklist.rb → safelist.rb} +2 -4
  14. data/lib/rack/attack/store_proxy.rb +13 -12
  15. data/lib/rack/attack/store_proxy/dalli_proxy.rb +2 -3
  16. data/lib/rack/attack/store_proxy/mem_cache_proxy.rb +50 -0
  17. data/lib/rack/attack/store_proxy/mem_cache_store_proxy.rb +19 -0
  18. data/lib/rack/attack/store_proxy/redis_cache_store_proxy.rb +35 -0
  19. data/lib/rack/attack/store_proxy/redis_proxy.rb +54 -0
  20. data/lib/rack/attack/store_proxy/redis_store_proxy.rb +5 -24
  21. data/lib/rack/attack/throttle.rb +16 -12
  22. data/lib/rack/attack/track.rb +3 -3
  23. data/lib/rack/attack/version.rb +1 -1
  24. data/spec/acceptance/allow2ban_spec.rb +71 -0
  25. data/spec/acceptance/blocking_ip_spec.rb +38 -0
  26. data/spec/acceptance/blocking_spec.rb +41 -0
  27. data/spec/acceptance/blocking_subnet_spec.rb +44 -0
  28. data/spec/acceptance/cache_store_config_for_allow2ban_spec.rb +126 -0
  29. data/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +121 -0
  30. data/spec/acceptance/cache_store_config_for_throttle_spec.rb +48 -0
  31. data/spec/acceptance/cache_store_config_with_rails_spec.rb +31 -0
  32. data/spec/acceptance/customizing_blocked_response_spec.rb +41 -0
  33. data/spec/acceptance/customizing_throttled_response_spec.rb +59 -0
  34. data/spec/acceptance/extending_request_object_spec.rb +34 -0
  35. data/spec/acceptance/fail2ban_spec.rb +76 -0
  36. data/spec/acceptance/safelisting_ip_spec.rb +48 -0
  37. data/spec/acceptance/safelisting_spec.rb +53 -0
  38. data/spec/acceptance/safelisting_subnet_spec.rb +48 -0
  39. data/spec/acceptance/stores/active_support_dalli_store_spec.rb +19 -0
  40. data/spec/acceptance/stores/active_support_mem_cache_store_pooled_spec.rb +22 -0
  41. data/spec/acceptance/stores/active_support_mem_cache_store_spec.rb +18 -0
  42. data/spec/acceptance/stores/active_support_memory_store_spec.rb +16 -0
  43. data/spec/acceptance/stores/active_support_redis_cache_store_pooled_spec.rb +18 -0
  44. data/spec/acceptance/stores/active_support_redis_cache_store_spec.rb +18 -0
  45. data/spec/acceptance/stores/active_support_redis_store_spec.rb +18 -0
  46. data/spec/acceptance/stores/connection_pool_dalli_client_spec.rb +22 -0
  47. data/spec/acceptance/stores/dalli_client_spec.rb +19 -0
  48. data/spec/acceptance/stores/redis_spec.rb +20 -0
  49. data/spec/acceptance/stores/redis_store_spec.rb +18 -0
  50. data/spec/acceptance/throttling_spec.rb +159 -0
  51. data/spec/acceptance/track_spec.rb +27 -0
  52. data/spec/acceptance/track_throttle_spec.rb +53 -0
  53. data/spec/allow2ban_spec.rb +10 -9
  54. data/spec/fail2ban_spec.rb +12 -10
  55. data/spec/integration/offline_spec.rb +21 -23
  56. data/spec/rack_attack_dalli_proxy_spec.rb +0 -2
  57. data/spec/rack_attack_request_spec.rb +2 -2
  58. data/spec/rack_attack_spec.rb +53 -18
  59. data/spec/rack_attack_throttle_spec.rb +45 -13
  60. data/spec/rack_attack_track_spec.rb +11 -8
  61. data/spec/spec_helper.rb +35 -14
  62. data/spec/support/cache_store_helper.rb +82 -0
  63. metadata +161 -61
  64. data/spec/integration/rack_attack_cache_spec.rb +0 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 62911565ba358aadd130a8edf862d1f3ca57786d
4
- data.tar.gz: 55876d8afb3bed309bb09a5b1fae357c8d5cdf10
2
+ SHA256:
3
+ metadata.gz: e666812691cc414692f7125979f0b152a9111ccee075e65b811fa4a6d8770daa
4
+ data.tar.gz: 3e8caba79f7ad09d4999cce6358de9cc29b815dee3c9f9c5adbf12763c764656
5
5
  SHA512:
6
- metadata.gz: b54245dad4b5101ce8364da45654d650bfe86c2f4e3c85d48e3923b7ca8a31ad5ec61e3938745dedefba3fd117447e522a4207145b086eccf86bf14ba1696643
7
- data.tar.gz: 57063639a9c9d6e3884b1a89cbfce044fefebe7dcb4a95f1e025e260db75490a05447d4e7294060aa55531b55d567b9763101debb2119a690c8760b0b94787ec
6
+ metadata.gz: f630c0cd1a34bd588e616653a2e6795e2ec6baafc0e0df8b489e6aa451cf47fb64065447fb3ceb2b029a51d87a0393d6d44cea02e58423626ea46165531f7da3
7
+ data.tar.gz: 22efc414db06b0a1bbbf8e6d34a3e0d0ead64f1832f48dc30af7ec0a374ef215d53cacf0a8e95c842bff0af84df0939f27820e4668c739eb8db3c51ebba4088e
data/README.md CHANGED
@@ -1,198 +1,240 @@
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 *whitelisting*, *blacklisting*, *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 blacklisting & whitelisting. 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 *whitelists*, *blacklists*, *throttles*, and *tracks* that you define. There are none by default.
69
+ E.g.
61
70
 
62
- * If the request matches any **whitelist**, it is allowed.
63
- * Otherwise, if the request matches any **blacklist**, 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 whitelisted?(req)
74
- @app.call(env)
75
- elsif blacklisted?(req)
76
- self.class.blacklisted_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::Attack` 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 whitelists, blacklists, 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
- ### Whitelists
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
- # (blacklist & throttles are skipped)
105
- Rack::Attack.whitelist('allow from localhost') do |req|
105
+ # (blocklist & throttles are skipped)
106
+ Rack::Attack.safelist('allow from localhost') do |req|
106
107
  # Requests are allowed if the return value is truthy
107
108
  '127.0.0.1' == req.ip || '::1' == req.ip
108
109
  end
109
110
  ```
110
111
 
111
- ### Blacklists
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.blacklist('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
- Rack::Attack.blacklist('block bad UA logins') do |req|
150
+ Rack::Attack.blocklist('block bad UA logins') do |req|
122
151
  req.path == '/login' && req.post? && req.user_agent == 'BadUA'
123
152
  end
124
153
  ```
125
154
 
126
155
  #### Fail2Ban
127
156
 
128
- `Fail2Ban.filter` can be used within a blacklist 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
131
- how the parameters work. For multiple filters, be sure to put each filter in a separate blacklist and use a unique discriminator for each fail2ban filter.
157
+ `Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.
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
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.
161
+
162
+ Fail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).
132
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
- Rack::Attack.blacklist('fail2ban pentesters') do |req|
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
- CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
172
+ CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||
142
173
  req.path.include?('/etc/passwd') ||
143
- req.path.include?('wp-admin') ||
174
+ req.path.include?('wp-admin') ||
144
175
  req.path.include?('wp-login')
145
-
176
+
146
177
  end
147
178
  end
148
179
  ```
149
180
 
150
- Note that `Fail2Ban` filters are not automatically scoped to the blacklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. `"pentest:#{req.ip}"`.
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.
158
- Rack::Attack.blacklist('allow2ban login scrapers') do |req|
193
+ 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,35 +260,67 @@ 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 blacklisted 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
- Rack::Attack.blacklisted_response = lambda do |env|
278
+ Rack::Attack.blocklisted_response = lambda do |env|
227
279
  # Using 503 because it may make attacker think that they have successfully
228
- # DOSed the site. Rack::Attack returns 403 for blacklists by default
280
+ # DOSed the site. Rack::Attack returns 403 for blocklists by default
229
281
  [ 503, {}, ['Blocked']]
230
282
  end
231
283
 
232
284
  Rack::Attack.throttled_response = lambda do |env|
233
- # name and other data about the matched throttle
234
- body = [
235
- env['rack.attack.matched'],
236
- env['rack.attack.match_type'],
237
- env['rack.attack.match_data']
238
- ].inspect
285
+ # NB: you have access to the name and other data about the matched throttle
286
+ # env['rack.attack.matched'],
287
+ # env['rack.attack.match_type'],
288
+ # env['rack.attack.match_data'],
289
+ # env['rack.attack.match_discriminator']
239
290
 
240
291
  # Using 503 because it may make attacker think that they have successfully
241
292
  # DOSed the site. Rack::Attack returns 429 for throttling by default
242
- [ 503, {}, [body]]
293
+ [ 503, {}, ["Server Error\n"]]
294
+ end
295
+ ```
296
+
297
+ ### X-RateLimit headers for well-behaved clients
298
+
299
+ While Rack::Attack's primary focus is minimizing harm from abusive clients, it
300
+ can also be used to return rate limit data that's helpful for well-behaved clients.
301
+
302
+ Here's an example response that includes conventional `X-RateLimit-*` headers:
303
+
304
+ ```ruby
305
+ Rack::Attack.throttled_response = lambda do |env|
306
+ match_data = env['rack.attack.match_data']
307
+ now = match_data[:epoch_time]
308
+
309
+ headers = {
310
+ 'X-RateLimit-Limit' => match_data[:limit].to_s,
311
+ 'X-RateLimit-Remaining' => '0',
312
+ 'X-RateLimit-Reset' => (now + (match_data[:period] - now % match_data[:period])).to_s
313
+ }
314
+
315
+ [ 429, headers, ["Throttled\n"]]
243
316
  end
244
317
  ```
245
318
 
319
+
246
320
  For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
247
321
 
248
322
  ```ruby
249
- 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 }
250
324
  ```
251
325
 
252
326
  ## Logging & Instrumentation
@@ -261,6 +335,43 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
261
335
  end
262
336
  ```
263
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
+
264
375
  ## Testing
265
376
 
266
377
  A note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will
@@ -274,10 +385,10 @@ but it depends on how many checks you've configured, and how long they take.
274
385
  Throttles usually require a network roundtrip to your cache server(s),
275
386
  so try to keep the number of throttle checks per request low.
276
387
 
277
- If a request is blacklisted or throttled, the response is a very simple Rack response.
388
+ If a request is blocklisted or throttled, the response is a very simple Rack response.
278
389
  A single typical ruby web server thread can block several hundred requests per second.
279
390
 
280
- 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).
281
392
 
282
393
  ## Motivation
283
394
 
@@ -291,9 +402,15 @@ less on short-term, one-off hacks to block a particular attack.
291
402
 
292
403
  ## Contributing
293
404
 
294
- Pull requests and issues are greatly appreciated. This project is intended to be
295
- a safe, welcoming space for collaboration, and contributors are expected to
296
- 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).
297
414
 
298
415
  ## Mailing list
299
416
 
@@ -304,6 +421,6 @@ New releases of Rack::Attack are announced on
304
421
 
305
422
  ## License
306
423
 
307
- Copyright Kickstarter, Inc.
424
+ Copyright Kickstarter, PBC.
308
425
 
309
- Released under an [MIT License](http://opensource.org/licenses/MIT).
426
+ Released under an [MIT License](https://opensource.org/licenses/MIT).