rack-attack 5.0.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 (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).