diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7552edf5..c7950f0f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,53 +17,92 @@ jobs: strategy: matrix: ruby: - - 3.1.2 - - 3.0.4 - - 2.7.6 - - 2.6.10 - - 2.5.8 + - '3.2' + - '3.1' + - '3.0' + - '2.7' + - '2.6' + - '2.5' gemfile: + - rack_3 - rack_2 - rack_1 + - rails_7_1 - rails_7_0 - rails_6_1 - rails_6_0 - rails_5_2 - - rails_4_2 - dalli3 - dalli2 + - redis_5 - redis_4 - - redis_3 - connection_pool_dalli - - active_support_redis_cache_store - - active_support_redis_cache_store_pooled + - active_support_7_1_redis_cache_store + - active_support_7_1_redis_cache_store_pooled + - active_support_7_0_redis_cache_store + - active_support_7_0_redis_cache_store_pooled + - active_support_6_redis_cache_store + - active_support_6_redis_cache_store_pooled + - active_support_5_redis_cache_store + - active_support_5_redis_cache_store_pooled - redis_store - - active_support_redis_store exclude: - gemfile: rack_1 - ruby: 3.1.2 + ruby: '3.2' - gemfile: rails_5_2 - ruby: 3.1.2 - - gemfile: rails_4_2 - ruby: 3.1.2 + ruby: '3.2' + - gemfile: active_support_5_redis_cache_store + ruby: '3.2' + - gemfile: active_support_5_redis_cache_store_pooled + ruby: '3.2' - gemfile: dalli2 - ruby: 3.1.2 + ruby: '3.2' - gemfile: rack_1 - ruby: 3.0.4 + ruby: '3.1' - gemfile: rails_5_2 - ruby: 3.0.4 - - gemfile: rails_4_2 - ruby: 3.0.4 + ruby: '3.1' + - gemfile: active_support_5_redis_cache_store + ruby: '3.1' + - gemfile: active_support_5_redis_cache_store_pooled + ruby: '3.1' - gemfile: dalli2 - ruby: 3.0.4 + ruby: '3.1' - gemfile: rack_1 - ruby: 2.7.6 - - gemfile: rails_4_2 - ruby: 2.7.6 + ruby: '3.0' + - gemfile: rails_5_2 + ruby: '3.0' + - gemfile: active_support_5_redis_cache_store + ruby: '3.0' + - gemfile: active_support_5_redis_cache_store_pooled + ruby: '3.0' + - gemfile: dalli2 + ruby: '3.0' + - gemfile: rack_1 + ruby: '2.7' - gemfile: rails_7_0 - ruby: 2.6.10 + ruby: '2.6' - gemfile: rails_7_0 - ruby: 2.5.8 + ruby: '2.5' + - gemfile: active_support_7_0_redis_cache_store + ruby: '2.6' + - gemfile: active_support_7_0_redis_cache_store + ruby: '2.5' + - gemfile: active_support_7_0_redis_cache_store_pooled + ruby: '2.6' + - gemfile: active_support_7_0_redis_cache_store_pooled + ruby: '2.5' + - gemfile: rails_7_1 + ruby: '2.6' + - gemfile: rails_7_1 + ruby: '2.5' + - gemfile: active_support_7_1_redis_cache_store + ruby: '2.6' + - gemfile: active_support_7_1_redis_cache_store + ruby: '2.5' + - gemfile: active_support_7_1_redis_cache_store_pooled + ruby: '2.6' + - gemfile: active_support_7_1_redis_cache_store_pooled + ruby: '2.5' env: BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile steps: diff --git a/.rubocop.yml b/.rubocop.yml index 745bb907..865fea87 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,7 @@ require: + - rubocop-minitest - rubocop-performance + - rubocop-rake inherit_mode: merge: @@ -56,7 +58,6 @@ Security: Style/BlockDelimiters: Enabled: true - IgnoredMethods: [] # Workaround rubocop bug: https://github.com/rubocop-hq/rubocop/issues/6179 Style/ClassAndModuleChildren: Enabled: true diff --git a/Appraisals b/Appraisals index 9035597e..f68e8b7f 100644 --- a/Appraisals +++ b/Appraisals @@ -1,5 +1,9 @@ # frozen_string_literal: true +appraise "rack_3" do + gem "rack", "~> 3.0" +end + appraise "rack_2" do gem "rack", "~> 2.0" end @@ -17,6 +21,10 @@ appraise "rack_1" do gem "rack-test", ">= 0.6" end +appraise 'rails_7-1' do + gem 'railties', '~> 7.1.0' +end + appraise 'rails_7-0' do gem 'railties', '~> 7.0.0' end @@ -33,14 +41,6 @@ appraise 'rails_5-2' do gem 'railties', '~> 5.2.0' end -appraise 'rails_4-2' do - gem 'railties', '~> 4.2.0' - - # Override rack-test version constraint by making it more loose - # so it's compatible with actionpack 4.2.x - gem "rack-test", ">= 0.6" -end - appraise 'dalli2' do gem 'dalli', '~> 2.0' end @@ -49,12 +49,12 @@ appraise 'dalli3' do gem 'dalli', '~> 3.0' end -appraise 'redis_4' do - gem 'redis', '~> 4.0' +appraise 'redis_5' do + gem 'redis', '~> 5.0' end -appraise 'redis_3' do - gem 'redis', '~> 3.3' +appraise 'redis_4' do + gem 'redis', '~> 4.0' end appraise "connection_pool_dalli" do @@ -62,21 +62,50 @@ appraise "connection_pool_dalli" do gem "dalli", "~> 3.0" end -appraise "active_support_redis_cache_store" do +appraise "active_support_7-1_redis_cache_store" do + gem "activesupport", "~> 7.1.0" + gem "redis", "~> 5.0" +end + +appraise "active_support_7-1_redis_cache_store_pooled" do + gem "activesupport", "~> 7.1.0" + gem "connection_pool", "~> 2.2" + gem "redis", "~> 5.0" +end + +appraise "active_support_7-0_redis_cache_store" do + gem "activesupport", "~> 7.0.0" + gem "redis", "~> 5.0" +end + +appraise "active_support_7-0_redis_cache_store_pooled" do + gem "activesupport", "~> 7.0.0" + gem "connection_pool", "~> 2.2" + gem "redis", "~> 5.0" +end + +appraise "active_support_6_redis_cache_store" do gem "activesupport", "~> 6.1.0" - gem "redis", "~> 4.0" + gem "redis", "~> 5.0" end -appraise "active_support_redis_cache_store_pooled" do +appraise "active_support_6_redis_cache_store_pooled" do gem "activesupport", "~> 6.1.0" gem "connection_pool", "~> 2.2" - gem "redis", "~> 4.0" + gem "redis", "~> 5.0" end -appraise "redis_store" do - gem "redis-store", "~> 1.5" +appraise "active_support_5_redis_cache_store" do + gem "activesupport", "~> 5.2.0" + gem "redis", "~> 5.0" end -appraise "active_support_redis_store" do - gem "redis-activesupport", "~> 5.0" +appraise "active_support_5_redis_cache_store_pooled" do + gem "activesupport", "~> 5.2.0" + gem "connection_pool", "~> 2.2" + gem "redis", "~> 5.0" +end + +appraise "redis_store" do + gem "redis-store", "~> 1.5" end diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c76d142..35f2a686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog -All notable changes to this project will be documented in this file. +This file will no longer be updated - all changes after v6.7.0 will only be documented in the relevant release note. + +## [6.7.0] - 2023-07-26 + +- Replace git.io URL by @kyoshidajp in #579 +- test: update rack-test to v2 from v1 by @grzuy in #587 +- Update example description to not suggest using a deprecated method by @MaksimAbramchuk in #589 +- Add note about cache stores and in-memory caches. by @nateberkopec in #604 +- ci: tests against redis gem v5 by @grzuy in #612 +- Support rack 3 by @ioquatix in #586 +- Gem release management. by @ioquatix in #614 ## [6.x.x] = 2022-xx-xx @@ -277,6 +287,7 @@ so your custom code is less prone to race conditions ([#282](https://github.com/ - Extract mandatory options to constants +[6.7.0]: https://github.com/rack/rack-attack/compare/v6.6.1...v6.7.0/ [6.6.1]: https://github.com/rack/rack-attack/compare/v6.6.0...v6.6.1/ [6.6.0]: https://github.com/rack/rack-attack/compare/v6.5.0...v6.6.0/ [6.5.0]: https://github.com/rack/rack-attack/compare/v6.4.0...v6.5.0/ diff --git a/Gemfile b/Gemfile index 7f4f5e95..69753c94 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,8 @@ source 'https://rubygems.org' gemspec + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end diff --git a/README.md b/README.md index ed19ca8c..aa7fdf1c 100644 --- a/README.md +++ b/README.md @@ -305,10 +305,15 @@ end Throttle, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)). ```ruby -Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new # defaults to Rails.cache +# This is the default +Rack::Attack.cache.store = Rails.cache +# It is recommended to use a separate database for throttling/allow2ban/fail2ban. +Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: "...") ``` -Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). +Most applications should use a new, separate database used only for `rack-attack`. During an actual attack or periods of heavy load, this database will come under heavy load. Keeping it on a separate database instance will give you additional resilience and make sure that other functions (like caching for your application) don't go down. + +Note that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). This means that other cache stores which inherit from ActiveSupport::Cache::Store are also compatible. In-memory stores which are not backed by an external database, such as `ActiveSupport::Cache::MemoryStore.new`, will be mostly ineffective because each Ruby process in your deployment will have it's own state, effectively multiplying the number of requests each client can make by the number of Ruby processes you have deployed. ## Customizing responses diff --git a/docs/example_configuration.md b/docs/example_configuration.md index b30a4ab0..cfe77581 100644 --- a/docs/example_configuration.md +++ b/docs/example_configuration.md @@ -75,7 +75,7 @@ class Rack::Attack # If you want to return 503 so that the attacker might be fooled into # believing that they've successfully broken your app (or you just want to # customize the response), then uncomment these lines. - # self.throttled_response = lambda do |env| + # self.throttled_responder = lambda do |env| # [ 503, # status # {}, # headers # ['']] # body diff --git a/gemfiles/active_support_5_redis_cache_store.gemfile b/gemfiles/active_support_5_redis_cache_store.gemfile new file mode 100644 index 00000000..0b800a0f --- /dev/null +++ b/gemfiles/active_support_5_redis_cache_store.gemfile @@ -0,0 +1,13 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activesupport", "~> 5.2.0" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/active_support_5_redis_cache_store_pooled.gemfile b/gemfiles/active_support_5_redis_cache_store_pooled.gemfile new file mode 100644 index 00000000..9127da50 --- /dev/null +++ b/gemfiles/active_support_5_redis_cache_store_pooled.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activesupport", "~> 5.2.0" +gem "connection_pool", "~> 2.2" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/active_support_redis_cache_store.gemfile b/gemfiles/active_support_6_redis_cache_store.gemfile similarity index 56% rename from gemfiles/active_support_redis_cache_store.gemfile rename to gemfiles/active_support_6_redis_cache_store.gemfile index 8acbbe13..72fb5b1d 100644 --- a/gemfiles/active_support_redis_cache_store.gemfile +++ b/gemfiles/active_support_6_redis_cache_store.gemfile @@ -3,6 +3,11 @@ source "https://rubygems.org" gem "activesupport", "~> 6.1.0" -gem "redis", "~> 4.0" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end gemspec path: "../" diff --git a/gemfiles/active_support_redis_cache_store_pooled.gemfile b/gemfiles/active_support_6_redis_cache_store_pooled.gemfile similarity index 62% rename from gemfiles/active_support_redis_cache_store_pooled.gemfile rename to gemfiles/active_support_6_redis_cache_store_pooled.gemfile index cf1e92e2..36a40f57 100644 --- a/gemfiles/active_support_redis_cache_store_pooled.gemfile +++ b/gemfiles/active_support_6_redis_cache_store_pooled.gemfile @@ -4,6 +4,11 @@ source "https://rubygems.org" gem "activesupport", "~> 6.1.0" gem "connection_pool", "~> 2.2" -gem "redis", "~> 4.0" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end gemspec path: "../" diff --git a/gemfiles/active_support_7_0_redis_cache_store.gemfile b/gemfiles/active_support_7_0_redis_cache_store.gemfile new file mode 100644 index 00000000..a94cfe88 --- /dev/null +++ b/gemfiles/active_support_7_0_redis_cache_store.gemfile @@ -0,0 +1,13 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activesupport", "~> 7.0.0" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/active_support_7_0_redis_cache_store_pooled.gemfile b/gemfiles/active_support_7_0_redis_cache_store_pooled.gemfile new file mode 100644 index 00000000..bd2a6e71 --- /dev/null +++ b/gemfiles/active_support_7_0_redis_cache_store_pooled.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activesupport", "~> 7.0.0" +gem "connection_pool", "~> 2.2" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/active_support_7_1_redis_cache_store.gemfile b/gemfiles/active_support_7_1_redis_cache_store.gemfile new file mode 100644 index 00000000..a0602ba5 --- /dev/null +++ b/gemfiles/active_support_7_1_redis_cache_store.gemfile @@ -0,0 +1,13 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activesupport", "~> 7.1.0" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/active_support_7_1_redis_cache_store_pooled.gemfile b/gemfiles/active_support_7_1_redis_cache_store_pooled.gemfile new file mode 100644 index 00000000..ae2d6d96 --- /dev/null +++ b/gemfiles/active_support_7_1_redis_cache_store_pooled.gemfile @@ -0,0 +1,14 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activesupport", "~> 7.1.0" +gem "connection_pool", "~> 2.2" +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/active_support_redis_store.gemfile b/gemfiles/active_support_redis_store.gemfile deleted file mode 100644 index 517c70f9..00000000 --- a/gemfiles/active_support_redis_store.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "redis-activesupport", "~> 5.0" - -gemspec path: "../" diff --git a/gemfiles/connection_pool_dalli.gemfile b/gemfiles/connection_pool_dalli.gemfile index 67c69a59..f84eb52e 100644 --- a/gemfiles/connection_pool_dalli.gemfile +++ b/gemfiles/connection_pool_dalli.gemfile @@ -5,4 +5,9 @@ source "https://rubygems.org" gem "connection_pool", "~> 2.2" gem "dalli", "~> 3.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/dalli2.gemfile b/gemfiles/dalli2.gemfile index c47d5afa..eb7e4acb 100644 --- a/gemfiles/dalli2.gemfile +++ b/gemfiles/dalli2.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "dalli", "~> 2.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/dalli3.gemfile b/gemfiles/dalli3.gemfile index 5f4d073b..3873dedf 100644 --- a/gemfiles/dalli3.gemfile +++ b/gemfiles/dalli3.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "dalli", "~> 3.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rack_1.gemfile b/gemfiles/rack_1.gemfile index 2390b859..36b2f91b 100644 --- a/gemfiles/rack_1.gemfile +++ b/gemfiles/rack_1.gemfile @@ -7,4 +7,9 @@ gem "activesupport", ">= 4.2" gem "rack", "~> 1.6" gem "rack-test", ">= 0.6" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rack_2.gemfile b/gemfiles/rack_2.gemfile index 964e087d..246c981a 100644 --- a/gemfiles/rack_2.gemfile +++ b/gemfiles/rack_2.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "rack", "~> 2.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rack_3.gemfile b/gemfiles/rack_3.gemfile new file mode 100644 index 00000000..f0735014 --- /dev/null +++ b/gemfiles/rack_3.gemfile @@ -0,0 +1,12 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rack", "~> 3.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile deleted file mode 100644 index 055cf9f6..00000000 --- a/gemfiles/rails_4_2.gemfile +++ /dev/null @@ -1,8 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "railties", "~> 4.2.0" -gem "rack-test", ">= 0.6" - -gemspec path: "../" diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index 8b2627fc..161bb698 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "railties", "~> 5.2.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 4cd55a81..679fea78 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "railties", "~> 6.0.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rails_6_1.gemfile b/gemfiles/rails_6_1.gemfile index 4c467fec..b1e5c039 100644 --- a/gemfiles/rails_6_1.gemfile +++ b/gemfiles/rails_6_1.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "railties", "~> 6.1.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 495c72d0..6f490fff 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "railties", "~> 7.0.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile new file mode 100644 index 00000000..fdfb546f --- /dev/null +++ b/gemfiles/rails_7_1.gemfile @@ -0,0 +1,12 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "railties", "~> 7.1.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/redis_3.gemfile b/gemfiles/redis_3.gemfile deleted file mode 100644 index 403482c1..00000000 --- a/gemfiles/redis_3.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "redis", "~> 3.3" - -gemspec path: "../" diff --git a/gemfiles/redis_4.gemfile b/gemfiles/redis_4.gemfile index 701e936c..e8b82f16 100644 --- a/gemfiles/redis_4.gemfile +++ b/gemfiles/redis_4.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "redis", "~> 4.0" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/gemfiles/redis_5.gemfile b/gemfiles/redis_5.gemfile new file mode 100644 index 00000000..fc9b4655 --- /dev/null +++ b/gemfiles/redis_5.gemfile @@ -0,0 +1,12 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "redis", "~> 5.0" + +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + +gemspec path: "../" diff --git a/gemfiles/redis_store.gemfile b/gemfiles/redis_store.gemfile index 8aafc6d1..e32d1e9e 100644 --- a/gemfiles/redis_store.gemfile +++ b/gemfiles/redis_store.gemfile @@ -4,4 +4,9 @@ source "https://rubygems.org" gem "redis-store", "~> 1.5" +group :maintenance, optional: true do + gem "bake" + gem "bake-gem" +end + gemspec path: "../" diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index 9b134165..c9094b21 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -11,15 +11,17 @@ require 'rack/attack/store_proxy/redis_proxy' require 'rack/attack/store_proxy/redis_store_proxy' require 'rack/attack/store_proxy/redis_cache_store_proxy' -require 'rack/attack/store_proxy/active_support_redis_store_proxy' require 'rack/attack/railtie' if defined?(::Rails) module Rack class Attack class Error < StandardError; end + class MisconfiguredStoreError < Error; end + class MissingStoreError < Error; end + class IncompatibleStoreError < Error; end autoload :Check, 'rack/attack/check' diff --git a/lib/rack/attack/base_proxy.rb b/lib/rack/attack/base_proxy.rb index 3e3c28af..f10af3d4 100644 --- a/lib/rack/attack/base_proxy.rb +++ b/lib/rack/attack/base_proxy.rb @@ -11,6 +11,7 @@ def proxies end def inherited(klass) + super proxies << klass end diff --git a/lib/rack/attack/cache.rb b/lib/rack/attack/cache.rb index 0c709460..ce8c10d1 100644 --- a/lib/rack/attack/cache.rb +++ b/lib/rack/attack/cache.rb @@ -9,8 +9,14 @@ class Cache attr_reader :last_epoch_time attr_reader :last_retry_after_time - def initialize - self.store = ::Rails.cache if defined?(::Rails.cache) + def self.default_store + if Object.const_defined?(:Rails) && Rails.respond_to?(:cache) + ::Rails.cache + end + end + + def initialize(store: self.class.default_store) + self.store = store @prefix = 'rack::attack' end diff --git a/lib/rack/attack/configuration.rb b/lib/rack/attack/configuration.rb index bcbafc49..ada5d261 100644 --- a/lib/rack/attack/configuration.rb +++ b/lib/rack/attack/configuration.rb @@ -61,11 +61,15 @@ def blocklist(name = nil, &block) end def blocklist_ip(ip_address) - @anonymous_blocklists << Blocklist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) } + @anonymous_blocklists << Blocklist.new do |request| + request.ip && !request.ip.empty? && IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) + end end def safelist_ip(ip_address) - @anonymous_safelists << Safelist.new { |request| IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) } + @anonymous_safelists << Safelist.new do |request| + request.ip && !request.ip.empty? && IPAddr.new(ip_address).include?(IPAddr.new(request.ip)) + end end def throttle(name, options, &block) diff --git a/lib/rack/attack/railtie.rb b/lib/rack/attack/railtie.rb index 234e1267..9521493b 100644 --- a/lib/rack/attack/railtie.rb +++ b/lib/rack/attack/railtie.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +begin + require 'rails/railtie' +rescue LoadError + return +end + module Rack class Attack class Railtie < ::Rails::Railtie diff --git a/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb b/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb deleted file mode 100644 index d2c0e3b2..00000000 --- a/lib/rack/attack/store_proxy/active_support_redis_store_proxy.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'rack/attack/base_proxy' - -module Rack - class Attack - module StoreProxy - class ActiveSupportRedisStoreProxy < BaseProxy - def self.handle?(store) - defined?(::Redis) && - defined?(::ActiveSupport::Cache::RedisStore) && - store.is_a?(::ActiveSupport::Cache::RedisStore) - end - - def increment(name, amount = 1, options = {}) - # #increment ignores options[:expires_in]. - # - # So in order to workaround this we use #write (which sets expiration) to initialize - # the counter. After that we continue using the original #increment. - if options[:expires_in] && !read(name) - write(name, amount, options) - - amount - else - super - end - end - - def read(name, options = {}) - super(name, options.merge!(raw: true)) - end - - def write(name, value, options = {}) - super(name, value, options.merge!(raw: true)) - end - end - end - end -end diff --git a/lib/rack/attack/throttle.rb b/lib/rack/attack/throttle.rb index 3a8644ca..51f83481 100644 --- a/lib/rack/attack/throttle.rb +++ b/lib/rack/attack/throttle.rb @@ -39,8 +39,9 @@ def matched_by?(request, use_offset = false) retry_after: cache.last_retry_after_time } + annotate_request_with_throttle_data(request, data) + (count > current_limit).tap do |throttled| - annotate_request_with_throttle_data(request, data) if throttled annotate_request_with_matched_data(request, data) Rack::Attack.instrument(request) diff --git a/lib/rack/attack/version.rb b/lib/rack/attack/version.rb index fe7ae8db..754fe57b 100644 --- a/lib/rack/attack/version.rb +++ b/lib/rack/attack/version.rb @@ -2,6 +2,6 @@ module Rack class Attack - VERSION = '6.6.1' + VERSION = '6.7.0' end end diff --git a/rack-attack.gemspec b/rack-attack.gemspec index cf7db71f..41cc7a8f 100644 --- a/rack-attack.gemspec +++ b/rack-attack.gemspec @@ -1,9 +1,6 @@ # frozen_string_literal: true -lib = File.expand_path('lib', __dir__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) - -require 'rack/attack/version' +require_relative 'lib/rack/attack/version' Gem::Specification.new do |s| s.name = 'rack-attack' @@ -29,16 +26,18 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.4' - s.add_runtime_dependency 'rack', ">= 1.0", "< 3" + s.add_runtime_dependency 'rack', ">= 1.0", "< 4" s.add_development_dependency 'appraisal', '~> 2.2' s.add_development_dependency "bundler", ">= 1.17", "< 3.0" s.add_development_dependency 'minitest', "~> 5.11" s.add_development_dependency "minitest-stub-const", "~> 0.6" - s.add_development_dependency 'rack-test', "~> 1.0" + s.add_development_dependency 'rack-test', "~> 2.0" s.add_development_dependency 'rake', "~> 13.0" - s.add_development_dependency "rubocop", "0.89.1" - s.add_development_dependency "rubocop-performance", "~> 1.5.0" + s.add_development_dependency "rubocop", "1.12.1" + s.add_development_dependency "rubocop-minitest", "~> 0.11.1" + s.add_development_dependency "rubocop-performance", "~> 1.10.2" + s.add_development_dependency "rubocop-rake", "~> 0.5.1" s.add_development_dependency "timecop", "~> 0.9.1" # byebug only works with MRI @@ -46,5 +45,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'byebug', '~> 11.0' end - s.add_development_dependency 'railties', '>= 4.2', '< 7.1' + s.add_development_dependency "activesupport" end diff --git a/spec/acceptance/blocking_ip_spec.rb b/spec/acceptance/blocking_ip_spec.rb index 102a8fce..4d08042f 100644 --- a/spec/acceptance/blocking_ip_spec.rb +++ b/spec/acceptance/blocking_ip_spec.rb @@ -19,6 +19,12 @@ assert_equal 200, last_response.status end + it "succeeds if IP is missing" do + get "/", {}, "REMOTE_ADDR" => "" + + assert_equal 200, last_response.status + end + it "notifies when the request is blocked" do notified = false notification_type = nil diff --git a/spec/acceptance/cache_store_config_for_fail2ban_spec.rb b/spec/acceptance/cache_store_config_for_fail2ban_spec.rb index 6f330eee..6fd79807 100644 --- a/spec/acceptance/cache_store_config_for_fail2ban_spec.rb +++ b/spec/acceptance/cache_store_config_for_fail2ban_spec.rb @@ -79,7 +79,7 @@ def write(key, value); end end it "works with any object that responds to #read, #write and #increment" do - FakeStore = Class.new do + fake_store_class = Class.new do attr_accessor :backend def initialize @@ -100,7 +100,7 @@ def increment(key, _count, _options = {}) end end - Rack::Attack.cache.store = FakeStore.new + Rack::Attack.cache.store = fake_store_class.new get "/" assert_equal 200, last_response.status diff --git a/spec/acceptance/extending_request_object_spec.rb b/spec/acceptance/extending_request_object_spec.rb index a4ea1a62..5449b90c 100644 --- a/spec/acceptance/extending_request_object_spec.rb +++ b/spec/acceptance/extending_request_object_spec.rb @@ -4,10 +4,8 @@ describe "Extending the request object" do before do - class Rack::Attack::Request - def authorized? - env["APIKey"] == "private-secret" - end + Rack::Attack::Request.define_method :authorized? do + env["APIKey"] == "private-secret" end Rack::Attack.blocklist("unauthorized requests") do |request| @@ -17,9 +15,7 @@ def authorized? # We don't want the extension to leak to other test cases after do - class Rack::Attack::Request - remove_method :authorized? - end + Rack::Attack::Request.undef_method :authorized? end it "forbids request if blocklist condition is true" do diff --git a/spec/acceptance/rails_middleware_spec.rb b/spec/acceptance/rails_middleware_spec.rb index 0e14e895..31dc6209 100644 --- a/spec/acceptance/rails_middleware_spec.rb +++ b/spec/acceptance/rails_middleware_spec.rb @@ -2,7 +2,7 @@ require_relative "../spec_helper" -if defined?(Rails) +if defined?(Rails::Application) describe "Middleware for Rails" do before do @app = Class.new(Rails::Application) do diff --git a/spec/acceptance/safelisting_ip_spec.rb b/spec/acceptance/safelisting_ip_spec.rb index c0b8faf5..fd80dc1b 100644 --- a/spec/acceptance/safelisting_ip_spec.rb +++ b/spec/acceptance/safelisting_ip_spec.rb @@ -17,6 +17,12 @@ assert_equal 403, last_response.status end + it "forbids request if blocklist condition is true and safelist is false (missing IP)" do + get "/admin", {}, "REMOTE_ADDR" => "" + + assert_equal 403, last_response.status + end + it "succeeds if blocklist condition is false and safelist is false" do get "/", {}, "REMOTE_ADDR" => "1.2.3.4" diff --git a/spec/acceptance/stores/active_support_redis_store_spec.rb b/spec/acceptance/stores/active_support_redis_store_spec.rb deleted file mode 100644 index 75e4d68d..00000000 --- a/spec/acceptance/stores/active_support_redis_store_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require_relative "../../spec_helper" - -if defined?(::ActiveSupport::Cache::RedisStore) - require_relative "../../support/cache_store_helper" - require "timecop" - - describe "ActiveSupport::Cache::RedisStore as a cache backend" do - before do - Rack::Attack.cache.store = ActiveSupport::Cache::RedisStore.new - end - - after do - Rack::Attack.cache.store.clear - end - - it_works_for_cache_backed_features(fetch_from_store: ->(key) { Rack::Attack.cache.store.read(key) }) - end -end diff --git a/spec/acceptance/stores/redis_store_spec.rb b/spec/acceptance/stores/redis_store_spec.rb index d7e8e115..dee35bcf 100644 --- a/spec/acceptance/stores/redis_store_spec.rb +++ b/spec/acceptance/stores/redis_store_spec.rb @@ -6,7 +6,7 @@ if defined?(::Redis::Store) require "timecop" - describe "ActiveSupport::Cache::RedisStore as a cache backend" do + describe "Redis::Store as a cache backend" do before do Rack::Attack.cache.store = ::Redis::Store.new end diff --git a/spec/integration/offline_spec.rb b/spec/integration/offline_spec.rb index d4960633..85429a42 100644 --- a/spec/integration/offline_spec.rb +++ b/spec/integration/offline_spec.rb @@ -21,18 +21,6 @@ end end -if defined?(::ActiveSupport::Cache::RedisStore) - describe 'when Redis is offline' do - include OfflineExamples - - before do - @cache = Rack::Attack::Cache.new - # Use presumably unused port for Redis client - @cache.store = ActiveSupport::Cache::RedisStore.new(host: '127.0.0.1', port: 3333) - end - end -end - if defined?(Redis) && defined?(ActiveSupport::Cache::RedisCacheStore) && Redis::VERSION >= '4' describe 'when Redis is offline' do include OfflineExamples diff --git a/spec/rack_attack_instrumentation_spec.rb b/spec/rack_attack_instrumentation_spec.rb index a8b4527f..d2291f77 100644 --- a/spec/rack_attack_instrumentation_spec.rb +++ b/spec/rack_attack_instrumentation_spec.rb @@ -1,42 +1,39 @@ # frozen_string_literal: true require_relative "spec_helper" +require 'active_support' +require 'active_support/subscriber' -# ActiveSupport::Subscribers added in ~> 4.0.2.0 -if ActiveSupport::VERSION::MAJOR > 3 - require_relative 'spec_helper' - require 'active_support/subscriber' - class CustomSubscriber < ActiveSupport::Subscriber - @notification_count = 0 +class CustomSubscriber < ActiveSupport::Subscriber + @notification_count = 0 - class << self - attr_accessor :notification_count - end + class << self + attr_accessor :notification_count + end - def throttle(_event) - self.class.notification_count += 1 - end + def throttle(_event) + self.class.notification_count += 1 end +end - describe 'Rack::Attack.instrument' do - before do - @period = 60 # Use a long period; failures due to cache key rotation less likely - Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new - Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip } - end +describe 'Rack::Attack.instrument' do + before do + @period = 60 # Use a long period; failures due to cache key rotation less likely + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + Rack::Attack.throttle('ip/sec', limit: 1, period: @period) { |req| req.ip } + end - describe "with throttling" do - before do - ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do - CustomSubscriber.attach_to("rack_attack") - 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' } - end + describe "with throttling" do + before do + ActiveSupport::Notifications.stub(:notifier, ActiveSupport::Notifications::Fanout.new) do + CustomSubscriber.attach_to("rack_attack") + 2.times { get '/', {}, 'REMOTE_ADDR' => '1.2.3.4' } end + end - it 'should instrument without error' do - _(last_response.status).must_equal 429 - assert_equal 1, CustomSubscriber.notification_count - end + it 'should instrument without error' do + _(last_response.status).must_equal 429 + assert_equal 1, CustomSubscriber.notification_count end end end diff --git a/spec/rack_attack_request_spec.rb b/spec/rack_attack_request_spec.rb index 8d4d27fc..8f27301a 100644 --- a/spec/rack_attack_request_spec.rb +++ b/spec/rack_attack_request_spec.rb @@ -5,10 +5,8 @@ describe 'Rack::Attack' do describe 'helpers' do before do - class Rack::Attack::Request - def remote_ip - ip - end + Rack::Attack::Request.define_method :remote_ip do + ip end Rack::Attack.safelist('valid IP') do |req| diff --git a/spec/rack_attack_spec.rb b/spec/rack_attack_spec.rb index c8358352..40936017 100644 --- a/spec/rack_attack_spec.rb +++ b/spec/rack_attack_spec.rb @@ -11,6 +11,10 @@ end it 'blocks requests with trailing slash' do + if Rack::Attack::PathNormalizer == Rack::Attack::FallbackPathNormalizer + skip "Normalization is only present on Rails" + end + get '/foo/' _(last_response.status).must_equal 403 end diff --git a/spec/rack_attack_track_spec.rb b/spec/rack_attack_track_spec.rb index 0db66e47..d8c53b8a 100644 --- a/spec/rack_attack_track_spec.rb +++ b/spec/rack_attack_track_spec.rb @@ -3,17 +3,19 @@ require_relative 'spec_helper' describe 'Rack::Attack.track' do - class Counter - def self.incr - @counter += 1 - end + let(:counter_class) do + Class.new do + def self.incr + @counter += 1 + end - def self.reset - @counter = 0 - end + def self.reset + @counter = 0 + end - def self.check - @counter + def self.check + @counter + end end end @@ -32,19 +34,19 @@ def self.check describe "with a notification subscriber and two tracks" do before do - Counter.reset + counter_class.reset # A second track Rack::Attack.track("homepage") { |req| req.path == "/" } ActiveSupport::Notifications.subscribe("track.rack_attack") do |*_args| - Counter.incr + counter_class.incr end get "/" end it "should notify twice" do - _(Counter.check).must_equal 2 + _(counter_class.check).must_equal 2 end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 10f856bf..f529e6a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,8 +5,7 @@ require "minitest/autorun" require "minitest/pride" require "rack/test" -require "rails" - +require "active_support" require "rack/attack" if RUBY_ENGINE == "ruby" @@ -22,14 +21,15 @@ def safe_require(name) safe_require "connection_pool" safe_require "dalli" safe_require "redis" -safe_require "redis-activesupport" safe_require "redis-store" -class MiniTest::Spec +class Minitest::Spec include Rack::Test::Methods before do - Rails.cache = nil + if Object.const_defined?(:Rails) && Rails.respond_to?(:cache) + Rails.cache.clear + end end after do