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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9198afaa909fd5893e05e59d4e107303aedb6d9cba9a99fc26025d4785fb0f1
4
- data.tar.gz: 573d19c93954eb75ad4f4db7b9ea92003162c1648e9b576a8e4279aa2efb6dbc
3
+ metadata.gz: 947e6356b19bea810a33478eb1b71a6c15226361875e3049c838736efa80c7d7
4
+ data.tar.gz: 2dc040a8efdb5ebbe2efec61dc95cbafa84e2df65af1c04fdccaaf1800256466
5
5
  SHA512:
6
- metadata.gz: 95d555d680506742c46c9de899c2ec17a9e4cb4d5423326777cf3b0b4b8131e85798b2f9bccbb3c9e4c7f6be44e0e2a86442ace63aab11e02cf2f21cc76a3924
7
- data.tar.gz: 2e633200a5563c497354bba636abc42a829f4d24297fd9d8174f767c2ca789edd1e7758e83ca1eaea4788c0acb084eb9378fe9ce85c3a82f4d34d880d3f3e9b3
6
+ metadata.gz: 3262a3fae6cf1f78acabdc33677f2f9d9309f1b474204cab8f2de8e0052c77ef360ecfe0cc98bee381416550f51881647f970b93b172c7565446a843ed655860
7
+ data.tar.gz: 8de32babd643f4678e176ddafea65050361d3ceb1bfb2c878a7b902ed9a8f23758e0aad81c9f422f03934a00987b3b3aea772718bddd575205f3d1852612fe52
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Nothing so far
4
4
 
5
+ ## 0.7.2
6
+
7
+ ### Additions
8
+ * Spamhaus filter
9
+
5
10
  ## 0.7.1
6
11
 
7
12
  ### Improvements
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. 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:
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"]
@@ -4,30 +4,27 @@ module Rack
4
4
  module Dedos
5
5
  class Cache
6
6
 
7
- def initialize(url:, expires_in: nil, key_prefix: nil)
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 store
18
- @store ||= {}
19
- end
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, created_at = store[key])
27
- if !@expires_in || @expires_in >= timestamp - created_at
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 timestamp
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 store
47
- @store ||= ::Redis.new(url: @url)
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 set(key, value)
51
- store.set(@key_prefix + key, value, ex: @expires_in)
51
+ def get(key)
52
+ @store.with { _1.get(prefixed(key)) }
52
53
  end
53
54
 
54
- def get(key)
55
- store.get(@key_prefix + key)
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 # user agent has changed
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  module Dedos
5
- VERSION = "0.7.1"
5
+ VERSION = "0.7.2"
6
6
  end
7
7
  end
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.1
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.6
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: []