rack-dedos 0.7.1 → 0.7.3

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: 1876d028a80926559978d4afff1d011227355de81868bc6a2bf78377199ca7e3
4
+ data.tar.gz: 915be1922ba19ea3c4f64e4710994eee3e74a1a8ae383554dccd72ef4213775f
5
5
  SHA512:
6
- metadata.gz: 95d555d680506742c46c9de899c2ec17a9e4cb4d5423326777cf3b0b4b8131e85798b2f9bccbb3c9e4c7f6be44e0e2a86442ace63aab11e02cf2f21cc76a3924
7
- data.tar.gz: 2e633200a5563c497354bba636abc42a829f4d24297fd9d8174f767c2ca789edd1e7758e83ca1eaea4788c0acb084eb9378fe9ce85c3a82f4d34d880d3f3e9b3
6
+ metadata.gz: f7452453fd2b4a36a02398fc32563e386f4ab42e35a50e082db295b2584a947b7bf36e8216ac331f8ac54a9d0ac5fe9c7d4e2e59192237142a535357abfb344c
7
+ data.tar.gz: e8c5f2118f9eb86dba94ef777a1e6f6339367467051ace6740addb23152b4715b3d6ed59c624fe83ec4912d2fd6a80253b1112578ebc0d72ed85c76b193afd2b
data/CHANGELOG.md CHANGED
@@ -2,107 +2,147 @@
2
2
 
3
3
  Nothing so far
4
4
 
5
- ## 0.7.1
5
+ # Changelog
6
6
 
7
- ### Improvements
8
- * Enforce MFA for future gem releases
7
+ All notable changes to this project will be documented in this file.
9
8
 
10
- ## 0.7.0
9
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
10
+ and this project adheres to [Break Versioning](https://www.taoensso.com/break-versioning).
11
11
 
12
- ### Breaking changes
13
- * Change format of country code details in log
12
+ ## [Unreleased]
14
13
 
15
- ### Fixes
16
- * Drop keyword arguments for Rack compatibility
17
- * Fix collision with Rack::Logger
14
+ Nothing so far
15
+
16
+ ## [0.7.3] - 2026-06-26
17
+
18
+ ### Changed
19
+
20
+ - Relaxed dependency on connection_pool for better legacy support
21
+
22
+ ## [0.7.2] - 2026-06-26
23
+
24
+ ### Added
25
+
26
+ - Spamhaus filter
27
+
28
+ ## [0.7.1] - 2026-04-21
29
+
30
+ ### Added
31
+
32
+ - Enforce MFA for future gem releases
33
+
34
+ ## [0.7.0] - 2026-03-11
35
+
36
+ ### Changed
37
+
38
+ - **Breaking:** Changed format of country code details in log
39
+
40
+ ### Fixed
41
+
42
+ - Dropped keyword arguments for Rack compatibility
43
+ - Fixed collision with Rack::Logger
44
+
45
+ ## [0.6.0] - 2026-03-11
46
+
47
+ ### Changed
48
+
49
+ - **Breaking:** Log to STDOUT (instead of STDERR) by default
50
+
51
+ ### Added
52
+
53
+ - Suppport custom loggers
54
+ - Optionally log request headers
55
+
56
+ ## [0.5.1] - 2026-01-08
57
+
58
+ ### Fixed
59
+
60
+ - Fixed getcwd errors due to unlinked tmpdir
61
+
62
+ ## [0.5.0] - 2026-01-06
63
+
64
+ ### Changed
65
+
66
+ - Updated to Ruby 4.0
67
+ - Require Minitest >= 6
68
+
69
+ ## [0.4.2] - 2025-12-01
70
+
71
+ ### Changed
72
+
73
+ - Adhere to [BreakVer](https://www.taoensso.com/break-versioning) from this point forward
18
74
 
19
- ## 0.6.0 (yanked)
75
+ ### Added
20
76
 
21
- ### Breaking changes
22
- * Log to STDOUT (instead of STDERR) by default
77
+ - Include the requested URL in warnings
78
+ - Added `only_paths` and `except_paths` options
23
79
 
24
- ### Additions
25
- * Suppport custom loggers
26
- * Optionally log request headers
80
+ ## [0.4.1] - 2025-11-04
27
81
 
28
- ## 0.5.1
82
+ ### Fixed
29
83
 
30
- ### Fixes
31
- * Fix getcwd errors due to unlinked tmpdir
84
+ - Correctly include details in warnings (i.e. country code)
32
85
 
33
- ## 0.5.0
86
+ ## [0.4.0] - 2025-07-21
34
87
 
35
- ### Changes
36
- * Update to Ruby 4.0
37
- * Require Minitest >= 6
88
+ ### Changed
38
89
 
39
- ## 0.4.2
90
+ - Dropped certs
91
+ - Addded action for trusted release
40
92
 
41
- ### Changes
42
- * Use [BreakVer](https://www.taoensso.com/break-versioning) from this point forward
93
+ ## [0.3.2] - 2025-01-16
43
94
 
44
- ### Additions
45
- * Include the requested URL in warnings
46
- * Add `only_paths` and `except_paths` options
95
+ ### Changed
47
96
 
48
- ## 0.4.1
97
+ - Resolve all paths to prevent problems with relative paths
49
98
 
50
- ### Fixes
51
- * Correctly include details in warnings (i.e. country code)
99
+ ## [0.3.1] - 2025-01-16
52
100
 
53
- ## 0.4.0
101
+ ### Fixed
54
102
 
55
- ### Changes
56
- * Drop certs
57
- * Add action for trusted release
103
+ - Root `File` operations to prevent clashes with Rack
58
104
 
59
- ## 0.3.2
105
+ ## [0.3.0] - 2025-01-16
60
106
 
61
- ### Changes
62
- * Resolve all paths to prevent problems with relative paths
107
+ ### Changed
63
108
 
64
- ## 0.3.1
109
+ - Converted `geoipget` from Bash to Ruby
65
110
 
66
- ### Changes
67
- * Root `File` operations to prevent clashes with Rack
111
+ ## [0.2.4] - 2025-01-15
68
112
 
69
- ## 0.3.0
113
+ ### Changed
70
114
 
71
- ### Changes
72
- * Convert `geoipget` from Bash to Ruby
115
+ - Use Bash for `geoipget` to prevent problems with `/bin/sh` diversity
73
116
 
74
- ## 0.2.4
117
+ ## [0.2.3] - 2025-01-15
75
118
 
76
- ### Changes
77
- * Use Bash for `geoipget` to prevent problems with `/bin/sh` diversity
119
+ ### Added
78
120
 
79
- ## 0.2.3
121
+ - `geoipget` shell script
80
122
 
81
- ### Additions
82
- * `geoipget` shell script
123
+ ## [0.2.2] - 2024-12-25
83
124
 
84
- ## 0.2.2
125
+ ### Changed
85
126
 
86
- ### Changes
87
- * Update to Ruby 3.4
127
+ - Updated to Ruby 3.4
88
128
 
89
- ## 0.2.1
129
+ ## [0.2.1] - 2024-11-20
90
130
 
91
- ### Fixes
131
+ ### Fixed
92
132
 
93
- * Fix paths on conditional requires
94
- * Renew certificate
133
+ - Fixed paths on conditional requires
134
+ - Renewed certificate
95
135
 
96
- ## 0.2.0
136
+ ## [0.2.0] - 2023-05-16
97
137
 
98
- ### Changes
138
+ ### Changed
99
139
 
100
- * Determine real client IP
101
- * Drop autoload and put filters in proper namespace
140
+ - Determine real client IP
141
+ - Dropped autoload and put filters in proper namespace
102
142
 
103
- ## 0.1.0
143
+ ## [0.1.0] - 2023-02-03
104
144
 
105
- ### Initial implementation
145
+ ### Added
106
146
 
107
- * UserAgent filter
108
- * Country filter
147
+ - UserAgent filter
148
+ - Country filter
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.3"
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.3
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: 2.5.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.5.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: []