rack-dedos 0.7.1 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +27 -10
- data/lib/rack/dedos/cache.rb +28 -19
- data/lib/rack/dedos/filters/base.rb +8 -5
- data/lib/rack/dedos/filters/spamhaus.rb +41 -0
- data/lib/rack/dedos/filters/user_agent.rb +3 -17
- data/lib/rack/dedos/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 947e6356b19bea810a33478eb1b71a6c15226361875e3049c838736efa80c7d7
|
|
4
|
+
data.tar.gz: 2dc040a8efdb5ebbe2efec61dc95cbafa84e2df65af1c04fdccaaf1800256466
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3262a3fae6cf1f78acabdc33677f2f9d9309f1b474204cab8f2de8e0052c77ef360ecfe0cc98bee381416550f51881647f970b93b172c7565446a843ed655860
|
|
7
|
+
data.tar.gz: 8de32babd643f4678e176ddafea65050361d3ceb1bfb2c878a7b902ed9a8f23758e0aad81c9f422f03934a00987b3b3aea772718bddd575205f3d1852612fe52
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -122,6 +122,18 @@ use Rack::Dedos,
|
|
|
122
122
|
text: "Temporary Server Error"
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### Cache
|
|
126
|
+
|
|
127
|
+
Some filters have to persist information to a key/value store. The default implementation uses a simple Ruby `Hash`, however, this is only suitable for development. If you chose to use such filters in production, you'll want to switch to Redis instead.
|
|
128
|
+
|
|
129
|
+
To do so, install the [redis gem](https://rubygems.org/gems/redis) and add the following configuration:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
use Rack::Dedos,
|
|
133
|
+
cache_url: 'redis://redis.example.com:6379/12', # db 12 on default port
|
|
134
|
+
cache_key_prefix: 'dedos', # key prefix for shared caches (default: nil)
|
|
135
|
+
```
|
|
136
|
+
|
|
125
137
|
### Log
|
|
126
138
|
|
|
127
139
|
By default, blocked request are logged as info to `$stdout` such as:
|
|
@@ -162,22 +174,19 @@ To only apply one specific filter, use the corresponding class as shown below.
|
|
|
162
174
|
|
|
163
175
|
### User Agent Filter
|
|
164
176
|
|
|
177
|
+
⚠️ This filter uses the cache!
|
|
178
|
+
|
|
165
179
|
```ruby
|
|
166
180
|
use Rack::Dedos::Filters::UserAgent,
|
|
167
|
-
cache_url: 'redis://redis.example.com:6379/12', # db 12 on default port
|
|
168
|
-
cache_key_prefix: 'dedos', # key prefix for shared caches (default: nil)
|
|
169
181
|
cache_period: 1800 # seconds (default: 900)
|
|
170
182
|
```
|
|
171
183
|
|
|
172
184
|
Requests are blocked for `cache_period` seconds in case another request has been made within `cache_period` seconds from by same IP address but with a different user agent.
|
|
173
185
|
|
|
174
|
-
The following cache backends are supported:
|
|
175
|
-
|
|
176
|
-
* `redis://...` – ⚠️ The [redis gem](https://rubygems.org/gems/redis) has to be installed.
|
|
177
|
-
* `hash` – Only for testing, don't use this in production.
|
|
178
|
-
|
|
179
186
|
### Country Filter
|
|
180
187
|
|
|
188
|
+
⚠️ This filter requires the [maxmind-db gem](https://rubygems.org/gems/maxmind-db) to be installed!
|
|
189
|
+
|
|
181
190
|
```ruby
|
|
182
191
|
use Rack::Dedos::Filters::Country,
|
|
183
192
|
maxmind_db_file: '/var/db/maxmind/GeoLite2-Country.mmdb',
|
|
@@ -187,8 +196,6 @@ use Rack::Dedos::Filters::Country,
|
|
|
187
196
|
|
|
188
197
|
Either allow or deny requests by probable country of origin. If both are set, the `denied_countries` option is ignored.
|
|
189
198
|
|
|
190
|
-
⚠️ The [maxmind-db gem](https://rubygems.org/gems/maxmind-db) has to be installed.
|
|
191
|
-
|
|
192
199
|
The MaxMind GeoLite2 database is free, however, you have to create an account on [maxmind.com](https://www.maxmind.com) and then download the country database.
|
|
193
200
|
|
|
194
201
|
For automatic updates, create a `geoipupdate.conf` file and then use the [geoipupdate tool for your arch](https://github.com/maxmind/geoipupdate/releases) to fetch the latest country database.
|
|
@@ -200,9 +207,19 @@ geoipget --help
|
|
|
200
207
|
geoipget --dir . --arch linux_amd64 /etc/geoipupdate.conf
|
|
201
208
|
```
|
|
202
209
|
|
|
210
|
+
### Spamhaus Filter
|
|
211
|
+
|
|
212
|
+
⚠️ This filter uses the cache!
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
use Rack::Dedos::Filters::Spamhaus
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Deny requests from IP addresses which are listed on the [Spamhaus ZEN Blocklist](https://www.spamhaus.org/blocklists/zen-blocklist/).
|
|
219
|
+
|
|
203
220
|
## Real Client IP
|
|
204
221
|
|
|
205
|
-
A word on how the real client IP is determined
|
|
222
|
+
A word on how the real client IP is determined: Both Rack 2 and Rack 3 (up to 3.0.7 at the time of writing) may populate the request `ip` incorrectly. Here's what a minimalistic Rack app deloyed to Render (behind Cloudflare) reports:
|
|
206
223
|
|
|
207
224
|
> request.ip = 172.71.135.17<br>
|
|
208
225
|
> request.forwarded_for = ["81.XXX.XXX.XXX", "172.71.135.17", "10.201.229.136"]
|
data/lib/rack/dedos/cache.rb
CHANGED
|
@@ -4,30 +4,27 @@ module Rack
|
|
|
4
4
|
module Dedos
|
|
5
5
|
class Cache
|
|
6
6
|
|
|
7
|
-
def initialize(url:,
|
|
8
|
-
@url, @expires_in = url, expires_in
|
|
9
|
-
@key_prefix = ("#{key_prefix}:" if key_prefix).to_s
|
|
7
|
+
def initialize(url:, key_prefix: nil, expires_in: 86400)
|
|
8
|
+
@url, @key_prefix, @expires_in = url, key_prefix, expires_in
|
|
10
9
|
type = url.split(':').first
|
|
11
10
|
extend Object.const_get("Rack::Dedos::Cache::#{type.capitalize}")
|
|
11
|
+
connect
|
|
12
12
|
rescue NameError
|
|
13
13
|
raise(ArgumentError, "type of cache for `#{@url}' not supported")
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
module Hash
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def set(key, value)
|
|
22
|
-
store[key] = [value, timestamp]
|
|
17
|
+
def set(key, value, expires_in: @expires_in)
|
|
18
|
+
expires_at = now + expires_in if expires_in
|
|
19
|
+
@store[key] = [value, expires_at]
|
|
23
20
|
end
|
|
24
21
|
|
|
25
22
|
def get(key)
|
|
26
|
-
if (value,
|
|
27
|
-
if
|
|
23
|
+
if (value, expires_at = @store[key])
|
|
24
|
+
if !expires_at || expires_at > now
|
|
28
25
|
value
|
|
29
26
|
else
|
|
30
|
-
store.delete(key)
|
|
27
|
+
@store.delete(key)
|
|
31
28
|
nil
|
|
32
29
|
end
|
|
33
30
|
end
|
|
@@ -35,7 +32,11 @@ module Rack
|
|
|
35
32
|
|
|
36
33
|
private
|
|
37
34
|
|
|
38
|
-
def
|
|
35
|
+
def connect
|
|
36
|
+
@store = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def now
|
|
39
40
|
Time.now.to_i
|
|
40
41
|
end
|
|
41
42
|
end
|
|
@@ -43,16 +44,24 @@ module Rack
|
|
|
43
44
|
module Redis
|
|
44
45
|
require 'redis'
|
|
45
46
|
|
|
46
|
-
def
|
|
47
|
-
@store
|
|
47
|
+
def set(key, value, expires_in: @expires_in)
|
|
48
|
+
@store.with { _1.set(prefixed(key), value, ex: expires_in) }
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
def
|
|
51
|
-
store.
|
|
51
|
+
def get(key)
|
|
52
|
+
@store.with { _1.get(prefixed(key)) }
|
|
52
53
|
end
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def connect
|
|
58
|
+
@store = ConnectionPool.new(size: 5, timeout: 1) do
|
|
59
|
+
::Redis.new(url: @url)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def prefixed(key)
|
|
64
|
+
@key_prefix ? "#{@key_prefix}:#{key}" : key
|
|
56
65
|
end
|
|
57
66
|
end
|
|
58
67
|
|
|
@@ -9,6 +9,8 @@ module Rack
|
|
|
9
9
|
|
|
10
10
|
DEFAULT_OPTIONS = {
|
|
11
11
|
logger: nil,
|
|
12
|
+
cache_url: 'Hash',
|
|
13
|
+
cache_key_prefix: nil,
|
|
12
14
|
only_paths: [],
|
|
13
15
|
except_paths: [],
|
|
14
16
|
status: 403,
|
|
@@ -16,13 +18,18 @@ module Rack
|
|
|
16
18
|
headers: []
|
|
17
19
|
}.freeze
|
|
18
20
|
|
|
19
|
-
attr_reader :app, :options, :details
|
|
21
|
+
attr_reader :app, :options, :cache, :logger, :details
|
|
20
22
|
|
|
21
23
|
# @param app [#call]
|
|
22
24
|
# @param options [Hash{Symbol => Object}]
|
|
23
25
|
def initialize(app, options={})
|
|
24
26
|
@app = app
|
|
25
27
|
@options = DEFAULT_OPTIONS.merge(options)
|
|
28
|
+
@cache = Cache.new(
|
|
29
|
+
url: @options[:cache_url],
|
|
30
|
+
key_prefix: @options[:cache_key_prefix]
|
|
31
|
+
)
|
|
32
|
+
@logger ||= options[:logger] || ::Logger.new($stdout, progname: 'rack-dedos')
|
|
26
33
|
@details = {}
|
|
27
34
|
end
|
|
28
35
|
|
|
@@ -46,10 +53,6 @@ module Rack
|
|
|
46
53
|
Rack::Dedos.config
|
|
47
54
|
end
|
|
48
55
|
|
|
49
|
-
def logger
|
|
50
|
-
@logger ||= options[:logger] || ::Logger.new($stdout, progname: 'rack-dedos')
|
|
51
|
-
end
|
|
52
|
-
|
|
53
56
|
def apply?(request)
|
|
54
57
|
return false if @options[:except_paths].any? { request.path.match? _1 }
|
|
55
58
|
return true if @options[:only_paths].none?
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'resolv'
|
|
4
|
+
|
|
5
|
+
module Rack
|
|
6
|
+
module Dedos
|
|
7
|
+
module Filters
|
|
8
|
+
class Spamhaus < Base
|
|
9
|
+
|
|
10
|
+
QUERY_DOMAIN = 'zen.spamhaus.org'.freeze
|
|
11
|
+
|
|
12
|
+
def name
|
|
13
|
+
:spamhaus
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(...)
|
|
17
|
+
super
|
|
18
|
+
@resolver = ConnectionPool.new(size: 5, timeout: 1) do
|
|
19
|
+
Resolv::DNS.new.tap { _1.timeouts = [1, 2] }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def allowed?(request, ip)
|
|
24
|
+
@resolver.with do |resolver|
|
|
25
|
+
resolver.getresources(domain_for(ip), Resolv::DNS::Resource::IN::A).empty?
|
|
26
|
+
end
|
|
27
|
+
rescue => error
|
|
28
|
+
logger.error("request from #{ip} allowed due to error: #{error.message}")
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def domain_for(ip)
|
|
35
|
+
ip.split('.').reverse.join('.').concat('.', QUERY_DOMAIN)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -9,28 +9,24 @@ module Rack
|
|
|
9
9
|
:user_agent
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
# @option options [String] :cache_url URL of the cache backend
|
|
13
12
|
# @option options [Integer] :cache_period how long to retain cached IP
|
|
14
13
|
# addresses in seconds (default: 900)
|
|
15
14
|
def initialize(...)
|
|
16
15
|
super
|
|
17
|
-
@cache_url = options[:cache_url] or fail "cache URL not set"
|
|
18
16
|
@cache_period = options[:cache_period] || 900
|
|
19
|
-
@cache_key_prefix = options[:cache_key_prefix]
|
|
20
|
-
cache # hit once to fail on errors at boot
|
|
21
17
|
end
|
|
22
18
|
|
|
23
19
|
def allowed?(request, ip)
|
|
24
20
|
case cache.get(ip)
|
|
25
21
|
when nil # first contact
|
|
26
|
-
cache.set(ip, request.user_agent)
|
|
22
|
+
cache.set(ip, request.user_agent, expires_in: @cache_period)
|
|
27
23
|
true
|
|
28
24
|
when 'BLOCKED' # already blocked
|
|
29
25
|
false
|
|
30
26
|
when request.user_agent # user agent hasn't changed
|
|
31
27
|
true
|
|
32
|
-
else
|
|
33
|
-
cache.set(ip, 'BLOCKED')
|
|
28
|
+
else # user agent has changed
|
|
29
|
+
cache.set(ip, 'BLOCKED', expires_in: @cache_period)
|
|
34
30
|
false
|
|
35
31
|
end
|
|
36
32
|
rescue => error
|
|
@@ -38,16 +34,6 @@ module Rack
|
|
|
38
34
|
true
|
|
39
35
|
end
|
|
40
36
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
|
-
def cache
|
|
44
|
-
config[:cache] ||= Cache.new(
|
|
45
|
-
url: @cache_url,
|
|
46
|
-
expires_in: @cache_period,
|
|
47
|
-
key_prefix: @cache_key_prefix
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
37
|
end
|
|
52
38
|
end
|
|
53
39
|
end
|
data/lib/rack/dedos/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-dedos
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sven Schwyn
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: connection_pool
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.0.0
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 3.0.0
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: rake
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -135,6 +149,7 @@ files:
|
|
|
135
149
|
- lib/rack/dedos/executables/geoipget.rb
|
|
136
150
|
- lib/rack/dedos/filters/base.rb
|
|
137
151
|
- lib/rack/dedos/filters/country.rb
|
|
152
|
+
- lib/rack/dedos/filters/spamhaus.rb
|
|
138
153
|
- lib/rack/dedos/filters/user_agent.rb
|
|
139
154
|
- lib/rack/dedos/version.rb
|
|
140
155
|
homepage: https://github.com/svoop/rack-dedos
|
|
@@ -168,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
168
183
|
- !ruby/object:Gem::Version
|
|
169
184
|
version: '0'
|
|
170
185
|
requirements: []
|
|
171
|
-
rubygems_version: 4.0.
|
|
186
|
+
rubygems_version: 4.0.10
|
|
172
187
|
specification_version: 4
|
|
173
188
|
summary: Radical filters to block denial-of-service (DoS) requests.
|
|
174
189
|
test_files: []
|