rack-dedos 0.1.0 → 0.2.1

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: 69f8976f9ee4fc15e9c0b3915020919fa72014acb8577933a5bdcd02b674570c
4
- data.tar.gz: 31bbb973f18eee8e22ed576a12846f6443d1ab2106fa3b8a1cdf6b52cc374464
3
+ metadata.gz: 44d58ef342f579aa4245e7e3f8dd41b505f576331c6bb5f65d4c7b6e5e07e74f
4
+ data.tar.gz: 6b63cf01106e9f09d2fea0daee7691b0d263fcdc48e4c7b44bcf04144c4468b4
5
5
  SHA512:
6
- metadata.gz: f506c9e93a4eb859a79c74c2053e493455aa13ebe8487fc42650a59bdb3e65da862b02cfab1cbfc54cb2186e340e014547bf6df4caf4f0afe2eae7233f3c9c1a
7
- data.tar.gz: 9a18dfe754ea786cbef049ec91a11108fb36c272ce954190331612089412957f3056f0635f6bf520a60ff0ab3373678067f6fcc5a7cf4e05799b170a6139a26f
6
+ metadata.gz: cb872c5c6d1a339bbf62220fdd1c179349ed28d266d291fdca4013527039e5290539a5e60adee88c570e87de0b04a93ac808a99103f5faeda4d5616acaf88995
7
+ data.tar.gz: eedf962195f3eacea34330bfc4b158263ee15d57684a18f6bd48aa26a78e49e92fa73fcb96ec043e18e1b96dfcfbd586c5117cd805b8c3b96e2f3b52d14982e0
checksums.yaml.gz.sig CHANGED
@@ -1,2 +1,2 @@
1
- �n
2
- �.�D�j��v����=0��91[�⅑E��"g�({[�n��:�s�fqSc���+����c�Ș��58j���`�j�~ur��sn�-�m�P@���#D�.�p�!%`W�{3qw���$|�3�Dݴ��nJR�ڥW�ߦ��CΘ�2��k9>|����c�e@�K6��I뒢�y�,��*C���il�G�:I�zk��X��G�� HP�VK��$�_��s�}8b0 �L�JX9����4@��R]:
1
+ ^细�-*��T:F�����t�+
2
+ A+��� ��
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  Nothing so far
4
4
 
5
+ ## 0.2.1
6
+
7
+ #### Fixes
8
+
9
+ * Fix paths on conditional requires
10
+ * Renew certificate
11
+
12
+ ## 0.2.0
13
+
14
+ #### Changes
15
+
16
+ * Determine real client IP
17
+ * Drop autoload and put filters in proper namespace
18
+
5
19
  ## 0.1.0
6
20
 
7
21
  #### Initial implementation
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  [![Version](https://img.shields.io/gem/v/rack-dedos.svg?style=flat)](https://rubygems.org/gems/rack-dedos)
2
2
  [![Tests](https://img.shields.io/github/actions/workflow/status/svoop/rack-dedos/test.yml?style=flat&label=tests)](https://github.com/svoop/rack-dedos/actions?workflow=Test)
3
3
  [![Code Climate](https://img.shields.io/codeclimate/maintainability/svoop/rack-dedos.svg?style=flat)](https://codeclimate.com/github/svoop/rack-dedos/)
4
- [![Donorbox](https://img.shields.io/badge/donate-on_donorbox-yellow.svg)](https://donorbox.org/bitcetera)
4
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/svoop.svg)](https://github.com/sponsors/svoop)
5
5
 
6
6
  <img src="https://github.com/svoop/rack-dedos/raw/main/doc/chop-chop.png" alt="chop-chop" align="right">
7
7
 
@@ -15,6 +15,8 @@ The filters have been proven to work against certain DoS attacks, however, they
15
15
  * [API](https://www.rubydoc.info/gems/rack-dedos)
16
16
  * Author: [Sven Schwyn - Bitcetera](https://bitcetera.com)
17
17
 
18
+ Thank you for supporting free and open-source software by sponsoring on [GitHub](https://github.com/sponsors/svoop) or on [Donorbox](https://donorbox.com/bitcetera). Any gesture is appreciated, from a single Euro for a ☕️ cup of coffee to 🍹 early retirement.
19
+
18
20
  ## Install
19
21
 
20
22
  ### Security
@@ -83,11 +85,11 @@ use Rack::Dedos,
83
85
 
84
86
  ## Filters
85
87
 
86
- By default, all filters described below are applied. You can exclude exclude certain filters:
88
+ By default, all filters described below are applied. You can exclude certain filters:
87
89
 
88
90
  ```ruby
89
91
  use Rack::Dedos,
90
- exclude: [:user_agent]
92
+ except: [:user_agent]
91
93
  ```
92
94
 
93
95
  To only apply one specific filter, use the corresponding class as shown below.
@@ -95,7 +97,7 @@ To only apply one specific filter, use the corresponding class as shown below.
95
97
  ### User Agent Filter
96
98
 
97
99
  ```ruby
98
- use Rack::Dedos::UserAgent,
100
+ use Rack::Dedos::Filters::UserAgent,
99
101
  cache_url: 'redis://redis.example.com:6379/12', # db 12 on default port
100
102
  cache_key_prefix: 'dedos', # key prefix for shared caches (default: nil)
101
103
  cache_period: 1800 # seconds (default: 900)
@@ -111,7 +113,7 @@ The following cache backends are supported:
111
113
  ### Country Filter
112
114
 
113
115
  ```ruby
114
- use Rack::Dedos::Country,
116
+ use Rack::Dedos::Filters::Country,
115
117
  maxmind_db_file: '/var/db/maxmind/GeoLite2-Country.mmdb',
116
118
  allowed_countries: %i(AT CH DE),
117
119
  denied_countries: %i(RU)
@@ -137,6 +139,21 @@ tar -xz -C /tmp -f /tmp/geoipupdate.tgz
137
139
  /tmp/geoipupdate_${version}_${arch}/geoipupdate -f "${conf}" -d "${dir}"
138
140
  ```
139
141
 
142
+ ## Real Client IP
143
+
144
+ 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:
145
+
146
+ > request.ip = 172.71.135.17<br>
147
+ > request.forwarded_for = ["81.XXX.XXX.XXX", "172.71.135.17", "10.201.229.136"]
148
+
149
+ Obviously, the reported IP 172.71.135.17 is not the real client IP, the correct one is the (redacted) 81.XXX.XXX.XXX.
150
+
151
+ Due to this flaw, Rack::Dedos determines the real client IP as follows in order of priority:
152
+
153
+ 1. [`Cf-Connecting-Ip` header](https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/#cf-connecting-ip)
154
+ 2. First entry of the [`X-Forwarded-For` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
155
+ 3. [`ip` reported by Rack](https://github.com/rack/rack/blob/main/lib/rack/request.rb)
156
+
140
157
  ## Development
141
158
 
142
159
  For all required test fixtures to be present, you have to check out the repo
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module Dedos
5
+ module Filters
6
+ class Base
7
+
8
+ DEFAULT_OPTIONS = {
9
+ status: 403,
10
+ text: 'Forbidden (Temporarily Blocked by Rules)'
11
+ }
12
+
13
+ attr_reader :app
14
+ attr_reader :options
15
+
16
+ # @param app [#call]
17
+ # @param options [Hash{Symbol => Object}]
18
+ def initialize(app, options = {})
19
+ @app = app
20
+ @options = DEFAULT_OPTIONS.merge(options)
21
+ end
22
+
23
+ def call(env)
24
+ request = Rack::Request.new(env)
25
+ ip = real_ip(request)
26
+ if allowed?(request, ip)
27
+ app.call(env)
28
+ else
29
+ warn("rack-dedos: request from #{ip} blocked by #{self.class} `#{@country_code.inspect}'")
30
+ [options[:status], { 'Content-Type' => 'text/plain' }, [options[:text]]]
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def config
37
+ Rack::Dedos.config
38
+ end
39
+
40
+ # Get the real IP of the client
41
+ #
42
+ # If containers and/or proxies such as Cloudflare are in the mix, the
43
+ # client IP reported by Rack may be wrong. Therefore, we determine the
44
+ # real client IP using the following priorities:
45
+ #
46
+ # 1. Cf-Connecting-Ip header
47
+ # 2. X-Forwarded-For header (also remove port number)
48
+ # 3. IP reported by Rack
49
+ #
50
+ # @param request [Rack::Request]
51
+ # @return [String] real client IP
52
+ def real_ip(request)
53
+ case
54
+ when ip = request.get_header('HTTP_CF_CONNECTING_IP')
55
+ ip
56
+ when forwarded_for = request.forwarded_for
57
+ forwarded_for.split(/\s*,\s*/).first&.sub(/:\d+$/, '')
58
+ else
59
+ request.ip
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/db'
4
+
5
+ module Rack
6
+ module Dedos
7
+ module Filters
8
+ class Country < Base
9
+
10
+ # @option options [String] :maxmind_db_file MaxMind database file
11
+ # @option options [Symbol, Array<Symbol>] :allowed_countries ISO 3166-1 alpha 2
12
+ # @option options [Symbol, Array<Symbol>] :denied_countries ISO 3166-1 alpha 2
13
+ def initialize(*)
14
+ super
15
+ @maxmind_db_file = options[:maxmind_db_file] or fail "MaxMind database file not set"
16
+ @allowed = case
17
+ when @countries = options[:allowed_countries] then true
18
+ when @countries = options[:denied_countries] then false
19
+ else fail "neither allowed nor denied countries set"
20
+ end
21
+ maxmind_db # hit once to fail on errors at boot
22
+ end
23
+
24
+ def allowed?(request, ip)
25
+ if country = maxmind_db.get(ip)
26
+ country_code = country.dig('country', 'iso_code').to_sym
27
+ @countries.include?(country_code) ? @allowed : !@allowed
28
+ else # not found in database
29
+ true
30
+ end
31
+ rescue => error
32
+ warn("rack-dedos: request from #{ip} allowed due to error: #{error.message}")
33
+ true
34
+ end
35
+
36
+ private
37
+
38
+ def maxmind_db
39
+ config[:maxmind_db] ||= MaxMind::DB.new(
40
+ @maxmind_db_file,
41
+ mode: MaxMind::DB::MODE_FILE
42
+ )
43
+ end
44
+
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module Dedos
5
+ module Filters
6
+ class UserAgent < Base
7
+
8
+ # @option options [String] :cache_url URL of the cache backend
9
+ # @option options [Integer] :cache_period how long to retain cached IP
10
+ # addresses in seconds (default: 900)
11
+ def initialize(*)
12
+ super
13
+ @cache_url = options[:cache_url] or fail "cache URL not set"
14
+ @cache_period = options[:cache_period] || 900
15
+ @cache_key_prefix = options[:cache_key_prefix]
16
+ cache # hit once to fail on errors at boot
17
+ end
18
+
19
+ def allowed?(request, ip)
20
+ case cache.get(ip)
21
+ when nil # first contact
22
+ cache.set(ip, request.user_agent)
23
+ true
24
+ when 'BLOCKED' # already blocked
25
+ false
26
+ when request.user_agent # user agent hasn't changed
27
+ true
28
+ else # user agent has changed
29
+ cache.set(ip, 'BLOCKED')
30
+ false
31
+ end
32
+ rescue => error
33
+ warn("rack-dedos: request from #{ip} allowed due to error: #{error.message}")
34
+ true
35
+ end
36
+
37
+ private
38
+
39
+ def cache
40
+ config[:cache] ||= Cache.new(
41
+ url: @cache_url,
42
+ expires_in: @cache_period,
43
+ key_prefix: @cache_key_prefix
44
+ )
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  module Dedos
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
data/lib/rack/dedos.rb CHANGED
@@ -6,11 +6,9 @@ require_relative 'dedos/version'
6
6
 
7
7
  module Rack
8
8
  module Dedos
9
- lib_dir = ::File.expand_path(::File.dirname(__FILE__))
10
- autoload :Cache, lib_dir + '/dedos/cache'
11
- autoload :Base, lib_dir + '/dedos/base'
12
- autoload :UserAgent, lib_dir + '/dedos/user_agent'
13
- autoload :Country, lib_dir + '/dedos/country'
9
+
10
+ require_relative 'dedos/cache'
11
+ require_relative 'dedos/filters/base'
14
12
 
15
13
  class << self
16
14
  def config
@@ -21,8 +19,14 @@ module Rack
21
19
  except = Array options[:except]
22
20
 
23
21
  Rack::Builder.new do
24
- use(::Rack::Dedos::UserAgent, options) unless except.include? :user_agent
25
- use(::Rack::Dedos::Country, options) unless except.include? :country
22
+ unless except.include? :user_agent
23
+ require_relative 'dedos/filters/user_agent'
24
+ use(::Rack::Dedos::Filters::UserAgent, options)
25
+ end
26
+ unless except.include? :country
27
+ require_relative 'dedos/filters/country'
28
+ use(::Rack::Dedos::Filters::Country, options)
29
+ end
26
30
  run app
27
31
  end.to_app
28
32
  end
data.tar.gz.sig CHANGED
Binary file
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.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Schwyn
@@ -11,8 +11,8 @@ cert_chain:
11
11
  - |
12
12
  -----BEGIN CERTIFICATE-----
13
13
  MIIDODCCAiCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhydWJ5
14
- L0RDPWJpdGNldGVyYS9EQz1jb20wHhcNMjIxMTA2MTIzNjUwWhcNMjMxMTA2MTIz
15
- NjUwWjAjMSEwHwYDVQQDDBhydWJ5L0RDPWJpdGNldGVyYS9EQz1jb20wggEiMA0G
14
+ L0RDPWJpdGNldGVyYS9EQz1jb20wHhcNMjQxMTIwMjExMDIwWhcNMjUxMTIwMjEx
15
+ MDIwWjAjMSEwHwYDVQQDDBhydWJ5L0RDPWJpdGNldGVyYS9EQz1jb20wggEiMA0G
16
16
  CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDcLg+IHjXYaUlTSU7R235lQKD8ZhEe
17
17
  KMhoGlSUonZ/zo1OT3KXcqTCP1iMX743xYs6upEGALCWWwq+nxvlDdnWRjF3AAv7
18
18
  ikC+Z2BEowjyeCCT/0gvn4ohKcR0JOzzRaIlFUVInlGSAHx2QHZ2N8ntf54lu7nd
@@ -21,15 +21,15 @@ cert_chain:
21
21
  PVa0i729A4IhroNnFNmw4wOC93ARNbM1+LW36PLMmKjKudf5Exg8VmDVAgMBAAGj
22
22
  dzB1MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBSfK8MtR62mQ6oN
23
23
  yoX/VKJzFjLSVDAdBgNVHREEFjAUgRJydWJ5QGJpdGNldGVyYS5jb20wHQYDVR0S
24
- BBYwFIEScnVieUBiaXRjZXRlcmEuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAYG2na
25
- ye8OE2DANQIFM/xDos/E4DaPWCJjX5xvFKNKHMCeQYPeZvLICCwyw2paE7Otwk6p
26
- uvbg2Ks5ykXsbk5i6vxDoeeOLvmxCqI6m+tHb8v7VZtmwRJm8so0eSX0WvTaKnIf
27
- CAn1bVUggczVdNoBXw9WAILKyw9bvh3Ft740XZrR74sd+m2pGwjCaM8hzLvrVbGP
28
- DyYhlBeRWyQKQ0WDIsiTSRhzK8HwSTUWjvPwx7SEdIU/HZgyrk0ETObKPakVu6bH
29
- kAyiRqgxF4dJviwtqI7mZIomWL63+kXLgjOjMe1SHxfIPo/0ji6+r1p4KYa7o41v
30
- fwIwU1MKlFBdsjkd
24
+ BBYwFIEScnVieUBiaXRjZXRlcmEuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQDSeB1x
25
+ 8QK8F/ML37isgvwGiQxovDUqu6Sq14cQ1qE9y5prUBmL2AsDuCBpXXctcvamFqNC
26
+ PgfJtj7ZZcXmY0SfKCog7T1btkr6zYxPXpxwUqB45n0I6v5qc0UCNvMEfBzxlak5
27
+ VW7UMNlKD9qukeN55hxuLF2F/sLldMcHUo/ATgdV4zk1t3sK6A9+02wz5K5qfWdM
28
+ Mi+XWXmGd57uojk3RcIXNwBRRP4DTKcKgVXhuyHb7q1vjTXrS6bw1Ortu0KmWOIk
29
+ jTyRsT1gymASS2KHe+BaCTwD74GqO8q4woYLZgXnJ/PvgcFgY2FEi2Kn/sXLp4JE
30
+ boIgxQCMT+nxBHCD
31
31
  -----END CERTIFICATE-----
32
- date: 2023-02-03 00:00:00.000000000 Z
32
+ date: 2024-11-20 00:00:00.000000000 Z
33
33
  dependencies:
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: rack
@@ -116,7 +116,7 @@ dependencies:
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  - !ruby/object:Gem::Dependency
119
- name: minitest-sound
119
+ name: minitest-flash
120
120
  requirement: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - ">="
@@ -207,10 +207,10 @@ files:
207
207
  - LICENSE.txt
208
208
  - README.md
209
209
  - lib/rack/dedos.rb
210
- - lib/rack/dedos/base.rb
211
210
  - lib/rack/dedos/cache.rb
212
- - lib/rack/dedos/country.rb
213
- - lib/rack/dedos/user_agent.rb
211
+ - lib/rack/dedos/filters/base.rb
212
+ - lib/rack/dedos/filters/country.rb
213
+ - lib/rack/dedos/filters/user_agent.rb
214
214
  - lib/rack/dedos/version.rb
215
215
  homepage: https://github.com/svoop/rack-dedos
216
216
  licenses:
@@ -243,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
243
  - !ruby/object:Gem::Version
244
244
  version: '0'
245
245
  requirements: []
246
- rubygems_version: 3.4.6
246
+ rubygems_version: 3.5.23
247
247
  signing_key:
248
248
  specification_version: 4
249
249
  summary: Radical filters to block denial-of-service (DoS) requests.
metadata.gz.sig CHANGED
Binary file
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rack
4
- module Dedos
5
- class Base
6
-
7
- DEFAULT_OPTIONS = {
8
- status: 403,
9
- text: 'Forbidden (Temporarily Blocked by Rules)'
10
- }
11
-
12
- attr_reader :app
13
- attr_reader :options
14
-
15
- # @param app [#call]
16
- # @param options [Hash{Symbol => Object}]
17
- def initialize(app, options = {})
18
- @app = app
19
- @options = DEFAULT_OPTIONS.merge(options)
20
- end
21
-
22
- def call(env)
23
- request = Rack::Request.new(env)
24
- if allowed?(request)
25
- app.call(env)
26
- else
27
- warn("rack-dedos: request from #{request.ip} blocked by #{self.class}")
28
- [options[:status], { 'Content-Type' => 'text/plain' }, [options[:text]]]
29
- end
30
- end
31
-
32
- private
33
-
34
- def config
35
- Rack::Dedos.config
36
- end
37
-
38
- end
39
- end
40
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'maxmind/db'
4
-
5
- module Rack
6
- module Dedos
7
- class Country < Base
8
-
9
- # @option options [String] :maxmind_db_file MaxMind database file
10
- # @option options [Symbol, Array<Symbol>] :allowed_countries ISO 3166-1 alpha 2
11
- # @option options [Symbol, Array<Symbol>] :denied_countries ISO 3166-1 alpha 2
12
- def initialize(*)
13
- super
14
- @maxmind_db_file = options[:maxmind_db_file] or fail "MaxMind database file not set"
15
- @allowed = case
16
- when @countries = options[:allowed_countries] then true
17
- when @countries = options[:denied_countries] then false
18
- else fail "neither allowed nor denied countries set"
19
- end
20
- maxmind_db # hit once to fail on errors at boot
21
- end
22
-
23
- def allowed?(request)
24
- if country = maxmind_db.get(request.ip)
25
- country_code = country.dig('country', 'iso_code').to_sym
26
- @countries.include?(country_code) ? @allowed : !@allowed
27
- else # not found in database
28
- true
29
- end
30
- rescue => error
31
- warn("rack-dedos: request from #{request.ip} allowed due to error: #{error.message}")
32
- true
33
- end
34
-
35
- private
36
-
37
- def maxmind_db
38
- config[:maxmind_db] ||= MaxMind::DB.new(
39
- @maxmind_db_file,
40
- mode: MaxMind::DB::MODE_FILE
41
- )
42
- end
43
-
44
- end
45
- end
46
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Rack
4
- module Dedos
5
- class UserAgent < Base
6
-
7
- # @option options [String] :cache_url URL of the cache backend
8
- # @option options [Integer] :cache_period how long to retain cached IP
9
- # addresses in seconds (default: 900)
10
- def initialize(*)
11
- super
12
- @cache_url = options[:cache_url] or fail "cache URL not set"
13
- @cache_period = options[:cache_period] || 900
14
- @cache_key_prefix = options[:cache_key_prefix]
15
- cache # hit once to fail on errors at boot
16
- end
17
-
18
- def allowed?(request)
19
- case cache.get(request.ip)
20
- when nil # first contact
21
- cache.set(request.ip, request.user_agent)
22
- true
23
- when 'BLOCKED' # already blocked
24
- false
25
- when request.user_agent # user agent hasn't changed
26
- true
27
- else # user agent has changed
28
- cache.set(request.ip, 'BLOCKED')
29
- false
30
- end
31
- rescue => error
32
- warn("rack-dedos: request from #{request.ip} allowed due to error: #{error.message}")
33
- true
34
- end
35
-
36
- private
37
-
38
- def cache
39
- config[:cache] ||= Cache.new(
40
- url: @cache_url,
41
- expires_in: @cache_period,
42
- key_prefix: @cache_key_prefix
43
- )
44
- end
45
-
46
- end
47
- end
48
- end