forked from rack/rack-contrib
/
deflect.rb
137 lines (115 loc) · 3.67 KB
/
deflect.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
require 'thread'
# TODO: optional stats
# TODO: performance
# TODO: clean up tests
module Rack
##
# Rack middleware for protecting against Denial-of-service attacks
# http://en.wikipedia.org/wiki/Denial-of-service_attack.
#
# This middleware is designed for small deployments, which most likely
# are not utilizing load balancing from other software or hardware. Deflect
# current supports the following functionality:
#
# * Saturation prevention (small DoS attacks, or request abuse)
# * Blacklisting of remote addresses
# * Whitelisting of remote addresses
# * Logging
#
# === Options:
#
# :log When false logging will be bypassed, otherwise pass an object responding to #puts
# :log_format Alter the logging format
# :log_date_format Alter the logging date format
# :request_threshold Number of requests allowed within the set :interval. Defaults to 100
# :interval Duration in seconds until the request counter is reset. Defaults to 5
# :block_duration Duration in seconds that a remote address will be blocked. Defaults to 900 (15 minutes)
# :whitelist Array of remote addresses which bypass Deflect. NOTE: this does not block others
# :blacklist Array of remote addresses immediately considered malicious
#
# === Examples:
#
# use Rack::Deflect, :log => $stdout, :request_threshold => 20, :interval => 2, :block_duration => 60
#
# CREDIT: TJ Holowaychuk <tj@vision-media.ca>
#
class Deflect
attr_reader :options
def initialize app, options = {}
@mutex = Mutex.new
@remote_addr_map = {}
@app, @options = app, {
:log => false,
:log_format => 'deflect(%s): %s',
:log_date_format => '%m/%d/%Y',
:request_threshold => 100,
:interval => 5,
:block_duration => 900,
:whitelist => [],
:blacklist => []
}.merge(options)
end
def call env
return deflect! if deflect? env
status, headers, body = @app.call env
[status, headers, body]
end
def deflect!
[403, { 'Content-Type' => 'text/html', 'Content-Length' => '0' }, []]
end
def deflect? env
@remote_addr = env['REMOTE_ADDR']
return false if options[:whitelist].include? @remote_addr
return true if options[:blacklist].include? @remote_addr
sync { watch }
end
def log message
return unless options[:log]
options[:log].puts(options[:log_format] % [Time.now.strftime(options[:log_date_format]), message])
end
def sync &block
@mutex.synchronize(&block)
end
def map
@remote_addr_map[@remote_addr] ||= {
:expires => Time.now + options[:interval],
:requests => 0
}
end
def watch
increment_requests
clear! if watch_expired? and not blocked?
clear! if blocked? and block_expired?
block! if watching? and exceeded_request_threshold?
blocked?
end
def block!
return if blocked?
log "blocked #{@remote_addr}"
map[:block_expires] = Time.now + options[:block_duration]
end
def blocked?
map.has_key? :block_expires
end
def block_expired?
map[:block_expires] < Time.now rescue false
end
def watching?
@remote_addr_map.has_key? @remote_addr
end
def clear!
return unless watching?
log "released #{@remote_addr}" if blocked?
@remote_addr_map.delete @remote_addr
end
def increment_requests
map[:requests] += 1
end
def exceeded_request_threshold?
map[:requests] > options[:request_threshold]
end
def watch_expired?
map[:expires] <= Time.now
end
end
end