properly indent code sections

Using three tick marks and double indenting is redundant. Doing both produces a readme with an odd visual flow. This change does not modify content, it only changes lefthand whitespace so the Readme on Github will be more coherent.
This commit is contained in:
Richard Schneeman 2013-10-23 00:09:43 -05:00
parent 25d8cc7c49
commit 41c449f838

214
README.md
View file

@ -18,28 +18,28 @@ See the [Backing & Hacking blog post](http://www.kickstarter.com/backing-and-hac
Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler: Install the [rack-attack](http://rubygems.org/gems/rack-attack) gem; or add it to you Gemfile with bundler:
```ruby ```ruby
# In your Gemfile # In your Gemfile
gem 'rack-attack' gem 'rack-attack'
``` ```
Tell your app to use the Rack::Attack middleware. Tell your app to use the Rack::Attack middleware.
For Rails 3+ apps: For Rails 3+ apps:
```ruby ```ruby
# In config/application.rb # In config/application.rb
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
``` ```
Or for Rackup files: Or for Rackup files:
```ruby ```ruby
# In config.ru # In config.ru
use Rack::Attack use Rack::Attack
``` ```
Optionally configure the cache store for throttling: Optionally configure the cache store for throttling:
```ruby ```ruby
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache
``` ```
Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). Note that `Rack::Attack.cache` is only used for throttling; not blacklisting & whitelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).
@ -56,20 +56,20 @@ The Rack::Attack middleware compares each request against *whitelists*, *blackli
The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb): The algorithm is actually more concise in code: See [Rack::Attack.call](https://github.com/kickstarter/rack-attack/blob/master/lib/rack/attack.rb):
```ruby ```ruby
def call(env) def call(env)
req = Rack::Request.new(env) req = Rack::Request.new(env)
if whitelisted?(req) if whitelisted?(req)
@app.call(env) @app.call(env)
elsif blacklisted?(req) elsif blacklisted?(req)
blacklisted_response[env] blacklisted_response[env]
elsif throttled?(req) elsif throttled?(req)
throttled_response[env] throttled_response[env]
else else
tracked?(req) tracked?(req)
@app.call(env) @app.call(env)
end end
end end
``` ```
## About Tracks ## About Tracks
@ -85,27 +85,27 @@ A [Rack::Request](http://rack.rubyforge.org/doc/classes/Rack/Request.html) objec
### Whitelists ### Whitelists
```ruby ```ruby
# Always allow requests from localhost # Always allow requests from localhost
# (blacklist & throttles are skipped) # (blacklist & throttles are skipped)
Rack::Attack.whitelist('allow from localhost') do |req| Rack::Attack.whitelist('allow from localhost') do |req|
# Requests are allowed if the return value is truthy # Requests are allowed if the return value is truthy
'127.0.0.1' == req.ip '127.0.0.1' == req.ip
end end
``` ```
### Blacklists ### Blacklists
```ruby ```ruby
# Block requests from 1.2.3.4 # Block requests from 1.2.3.4
Rack::Attack.blacklist('block 1.2.3.4') do |req| Rack::Attack.blacklist('block 1.2.3.4') do |req|
# Request are blocked if the return value is truthy # Request are blocked if the return value is truthy
'1.2.3.4' == req.ip '1.2.3.4' == req.ip
end end
# Block logins from a bad user agent # Block logins from a bad user agent
Rack::Attack.blacklist('block bad UA logins') do |req| Rack::Attack.blacklist('block bad UA logins') do |req|
req.path == '/login' && req.post? && req.user_agent == 'BadUA' req.path == '/login' && req.post? && req.user_agent == 'BadUA'
end end
``` ```
#### Fail2Ban #### Fail2Ban
@ -116,79 +116,79 @@ See the [fail2ban documentation](http://www.fail2ban.org/wiki/index.php/MANUAL_0
how the parameters work. how the parameters work.
```ruby ```ruby
# Block requests containing '/etc/password' in the params. # Block requests containing '/etc/password' in the params.
# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes. # After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.
Rack::Attack.blacklist('fail2ban pentesters') do |req| Rack::Attack.blacklist('fail2ban pentesters') do |req|
# `filter` returns truthy value if request fails, or if it's from a previously banned IP # `filter` returns truthy value if request fails, or if it's from a previously banned IP
# so the request is blocked # so the request is blocked
Rack::Attack::Fail2Ban.filter(req.ip, :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do Rack::Attack::Fail2Ban.filter(req.ip, :maxretry => 3, :findtime => 10.minutes, :bantime => 5.minutes) do
# The count for the IP is incremented if the return value is truthy. # The count for the IP is incremented if the return value is truthy.
CGI.unescape(req.query_string) =~ %r{/etc/passwd} CGI.unescape(req.query_string) =~ %r{/etc/passwd}
end end
end end
``` ```
#### Allow2Ban #### Allow2Ban
`Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving `Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving
clients until such time as they reach maxretry at which they are cut off as per normal. clients until such time as they reach maxretry at which they are cut off as per normal.
```ruby ```ruby
# Lockout IP addresses that are hammering your login page. # Lockout IP addresses that are hammering your login page.
# After 20 requests in 1 minute, block all requests from that IP for 1 hour. # After 20 requests in 1 minute, block all requests from that IP for 1 hour.
Rack::Attack.blacklist('allow2ban login scrapers') do |req| Rack::Attack.blacklist('allow2ban login scrapers') do |req|
# `filter` returns false value if request is to your login page (but still # `filter` returns false value if request is to your login page (but still
# increments the count) so request below the limit are not blocked until # increments the count) so request below the limit are not blocked until
# they hit the limit. At that point, filter will return true and block. # they hit the limit. At that point, filter will return true and block.
Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do Rack::Attack::Allow2Ban.filter(req.ip, :maxretry => 20, :findtime => 1.minute, :bantime => 1.hour) do
# The count for the IP is incremented if the return value is truthy. # The count for the IP is incremented if the return value is truthy.
req.path = '/login' and req.method == 'post' req.path = '/login' and req.method == 'post'
end end
end end
``` ```
### Throttles ### Throttles
```ruby ```ruby
# Throttle requests to 5 requests per second per ip # Throttle requests to 5 requests per second per ip
Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req| Rack::Attack.throttle('req/ip', :limit => 5, :period => 1.second) do |req|
# If the return value is truthy, the cache key for the return value # If the return value is truthy, the cache key for the return value
# is incremented and compared with the limit. In this case: # is incremented and compared with the limit. In this case:
# "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}" # "rack::attack:#{Time.now.to_i/1.second}:req/ip:#{req.ip}"
# #
# If falsy, the cache key is neither incremented nor checked. # If falsy, the cache key is neither incremented nor checked.
req.ip req.ip
end end
# Throttle login attempts for a given email parameter to 6 reqs/minute # Throttle login attempts for a given email parameter to 6 reqs/minute
# Return the email as a discriminator on POST /login requests # Return the email as a discriminator on POST /login requests
Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req| Rack::Attack.throttle('logins/email', :limit => 6, :period => 60.seconds) do |req|
req.params['email'] if req.path == '/login' && req.post? req.params['email'] if req.path == '/login' && req.post?
end end
# You can also set a limit using a proc instead of a number. For # You can also set a limit using a proc instead of a number. For
# instance, after Rack::Auth::Basic has authenticated the user: # instance, after Rack::Auth::Basic has authenticated the user:
limit_based_on_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1} limit_based_on_proc = proc {|req| req.env["REMOTE_USER"] == "admin" ? 100 : 1}
Rack::Attack.throttle('req/ip', :limit => limit_based_on_proc, :period => 1.second) do |req| Rack::Attack.throttle('req/ip', :limit => limit_based_on_proc, :period => 1.second) do |req|
req.ip req.ip
end end
``` ```
### Tracks ### Tracks
```ruby ```ruby
# Track requests from a special user agent # Track requests from a special user agent
Rack::Attack.track("special_agent") do |req| Rack::Attack.track("special_agent") do |req|
req.user_agent == "SpecialAgent" req.user_agent == "SpecialAgent"
end end
# Track it using ActiveSupport::Notification # Track it using ActiveSupport::Notification
ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, req| ActiveSupport::Notifications.subscribe("rack.attack") do |name, start, finish, request_id, req|
if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track if req.env['rack.attack.matched'] == "special_agent" && req.env['rack.attack.match_type'] == :track
Rails.logger.info "special_agent: #{req.path}" Rails.logger.info "special_agent: #{req.path}"
STATSD.increment("special_agent") STATSD.increment("special_agent")
end end
end end
``` ```
## Responses ## Responses
@ -196,30 +196,30 @@ clients until such time as they reach maxretry at which they are cut off as per
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). 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).
```ruby ```ruby
Rack::Attack.blacklisted_response = lambda do |env| Rack::Attack.blacklisted_response = lambda do |env|
# Using 503 because it may make attacker think that they have successfully # Using 503 because it may make attacker think that they have successfully
# DOSed the site. Rack::Attack returns 401 for blacklists by default # DOSed the site. Rack::Attack returns 401 for blacklists by default
[ 503, {}, ['Blocked']] [ 503, {}, ['Blocked']]
end end
Rack::Attack.throttled_response = lambda do |env| Rack::Attack.throttled_response = lambda do |env|
# name and other data about the matched throttle # name and other data about the matched throttle
body = [ body = [
env['rack.attack.matched'], env['rack.attack.matched'],
env['rack.attack.match_type'], env['rack.attack.match_type'],
env['rack.attack.match_data'] env['rack.attack.match_data']
].inspect ].inspect
# Using 503 because it may make attacker think that they have successfully # Using 503 because it may make attacker think that they have successfully
# DOSed the site. Rack::Attack returns 429 for throttling by default # DOSed the site. Rack::Attack returns 429 for throttling by default
[ 503, {}, [body]] [ 503, {}, [body]]
end end
``` ```
For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data: For responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:
```ruby ```ruby
request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l } request.env['rack.attack.throttle_data'][name] # => { :count => n, :period => p, :limit => l }
``` ```
## Logging & Instrumentation ## Logging & Instrumentation
@ -229,9 +229,9 @@ Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/
You can subscribe to 'rack.attack' events and log it, graph it, etc: You can subscribe to 'rack.attack' events and log it, graph it, etc:
```ruby ```ruby
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req| ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
puts req.inspect puts req.inspect
end end
``` ```
## Testing ## Testing