rack-dedos 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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