diff --git a/Appraisals b/Appraisals index 2b843033..834d2018 100644 --- a/Appraisals +++ b/Appraisals @@ -17,24 +17,20 @@ appraise "rack_1_6" do gem "rack-test", ">= 0.6" end -appraise 'rails_6_0' do - gem 'actionpack', '~> 6.0.0' - gem 'activesupport', '~> 6.0.0' +appraise 'rails_6-0' do + gem 'railties', '~> 6.0.0' end appraise 'rails_5-2' do - gem 'actionpack', '~> 5.2.0' - gem 'activesupport', '~> 5.2.0' + gem 'railties', '~> 5.2.0' end appraise 'rails_5-1' do - gem 'actionpack', '~> 5.1.0' - gem 'activesupport', '~> 5.1.0' + gem 'railties', '~> 5.1.0' end appraise 'rails_4-2' do - gem 'actionpack', '~> 4.2.0' - gem 'activesupport', '~> 4.2.0' + gem 'railties', '~> 4.2.0' # Override rack-test version constraint by making it more loose # so it's compatible with actionpack 4.2.x diff --git a/README.md b/README.md index 538a732f..0b4a071a 100644 --- a/README.md +++ b/README.md @@ -68,14 +68,19 @@ Or install it yourself as: Then tell your ruby web application to use rack-attack as a middleware. -a) For __rails__ applications: - +a) For __rails__ applications with versions >= 5 it is used by default. For older rails versions you should enable it explicitly: ```ruby # In config/application.rb config.middleware.use Rack::Attack ``` +You can disable it permanently (like for specific environment) or temporarily (can be useful for specific test cases) by writing: + +```ruby +Rack::Attack.enabled = false +``` + b) For __rack__ applications: ```ruby diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile index eb090836..055cf9f6 100644 --- a/gemfiles/rails_4_2.gemfile +++ b/gemfiles/rails_4_2.gemfile @@ -2,8 +2,7 @@ source "https://rubygems.org" -gem "actionpack", "~> 4.2.0" -gem "activesupport", "~> 4.2.0" +gem "railties", "~> 4.2.0" gem "rack-test", ">= 0.6" gemspec path: "../" diff --git a/gemfiles/rails_5_1.gemfile b/gemfiles/rails_5_1.gemfile index d05d8e42..66a5a0b9 100644 --- a/gemfiles/rails_5_1.gemfile +++ b/gemfiles/rails_5_1.gemfile @@ -2,7 +2,6 @@ source "https://rubygems.org" -gem "actionpack", "~> 5.1.0" -gem "activesupport", "~> 5.1.0" +gem "railties", "~> 5.1.0" gemspec path: "../" diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile index d12b8c65..8b2627fc 100644 --- a/gemfiles/rails_5_2.gemfile +++ b/gemfiles/rails_5_2.gemfile @@ -2,7 +2,6 @@ source "https://rubygems.org" -gem "actionpack", "~> 5.2.0" -gem "activesupport", "~> 5.2.0" +gem "railties", "~> 5.2.0" gemspec path: "../" diff --git a/gemfiles/rails_6_0.gemfile b/gemfiles/rails_6_0.gemfile index 8e0b5587..4cd55a81 100644 --- a/gemfiles/rails_6_0.gemfile +++ b/gemfiles/rails_6_0.gemfile @@ -2,7 +2,6 @@ source "https://rubygems.org" -gem "actionpack", "~> 6.0.0" -gem "activesupport", "~> 6.0.0" +gem "railties", "~> 6.0.0" gemspec path: "../" diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index f4297a58..c8d4ad1a 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -6,6 +6,8 @@ require 'rack/attack/request' require "ipaddr" +require 'rack/attack/railtie' if defined?(::Rails) + module Rack class Attack class MisconfiguredStoreError < StandardError; end @@ -28,7 +30,8 @@ class MissingStoreError < StandardError; end autoload :Allow2Ban, 'rack/attack/allow2ban' class << self - attr_accessor :notifier, :blocklisted_response, :throttled_response, :anonymous_blocklists, :anonymous_safelists + attr_accessor :enabled, :notifier, :blocklisted_response, :throttled_response, + :anonymous_blocklists, :anonymous_safelists def safelist(name = nil, &block) safelist = Safelist.new(name, &block) @@ -134,6 +137,7 @@ def clear! end # Set defaults + @enabled = true @anonymous_blocklists = [] @anonymous_safelists = [] @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications) @@ -148,6 +152,8 @@ def initialize(app) end def call(env) + return @app.call(env) unless self.class.enabled + env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO']) request = Rack::Attack::Request.new(env) diff --git a/lib/rack/attack/railtie.rb b/lib/rack/attack/railtie.rb new file mode 100644 index 00000000..59994f1c --- /dev/null +++ b/lib/rack/attack/railtie.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Rack + class Attack + class Railtie < ::Rails::Railtie + initializer 'rack.attack.middleware', after: :load_config_initializers, before: :build_middleware_stack do |app| + if Gem::Version.new(::Rails::VERSION::STRING) >= Gem::Version.new("5") + middlewares = app.config.middleware + operations = middlewares.send(:operations) + middlewares.send(:delete_operations) + + use_middleware = operations.none? do |operation| + middleware = operation[1] + middleware.include?(Rack::Attack) + end + + middlewares.use(Rack::Attack) if use_middleware + end + end + end + end +end diff --git a/rack-attack.gemspec b/rack-attack.gemspec index 94d627e6..dfdd09d6 100644 --- a/rack-attack.gemspec +++ b/rack-attack.gemspec @@ -46,8 +46,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'byebug', '~> 11.0' end - # The following are potential runtime dependencies users may have, - # which rack-attack uses only for testing compatibility in test suite. - s.add_development_dependency 'actionpack', '~> 5.2' - s.add_development_dependency 'activesupport', '~> 5.2' + s.add_development_dependency 'railties', '>= 4.2' end diff --git a/spec/acceptance/rails_middleware_spec.rb b/spec/acceptance/rails_middleware_spec.rb new file mode 100644 index 00000000..4d27c702 --- /dev/null +++ b/spec/acceptance/rails_middleware_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +if defined?(Rails) + describe "Middleware for Rails" do + before do + @app = Class.new(Rails::Application) do + config.eager_load = false + config.logger = Logger.new(nil) # avoid creating the log/ directory automatically + config.cache_store = :null_store # avoid creating tmp/ directory for cache + end + end + + if Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new("5") + it "is used by default" do + @app.initialize! + assert_equal 1, @app.middleware.count(Rack::Attack) + end + + it "is not added when it was added explicitly" do + @app.config.middleware.use(Rack::Attack) + @app.initialize! + assert_equal 1, @app.middleware.count(Rack::Attack) + end + + it "is not added when it was explicitly deleted" do + @app.config.middleware.delete(Rack::Attack) + @app.initialize! + refute @app.middleware.include?(Rack::Attack) + end + end + + if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("5") + it "is not used by default" do + @app.initialize! + assert_equal 0, @app.middleware.count(Rack::Attack) + end + end + end +end diff --git a/spec/rack_attack_spec.rb b/spec/rack_attack_spec.rb index 51d7cf44..647a08d7 100644 --- a/spec/rack_attack_spec.rb +++ b/spec/rack_attack_spec.rb @@ -76,4 +76,27 @@ end end end + + describe 'enabled' do + it 'should be enabled by default' do + _(Rack::Attack.enabled).must_equal true + end + + it 'should directly pass request when disabled' do + bad_ip = '1.2.3.4' + Rack::Attack.blocklist("ip #{bad_ip}") { |req| req.ip == bad_ip } + + get '/', {}, 'REMOTE_ADDR' => bad_ip + _(last_response.status).must_equal 403 + + prev_enabled = Rack::Attack.enabled + begin + Rack::Attack.enabled = false + get '/', {}, 'REMOTE_ADDR' => bad_ip + _(last_response.status).must_equal 200 + ensure + Rack::Attack.enabled = prev_enabled + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 49d88731..a1728cb6 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 'active_support' -require 'action_dispatch' +require "rails" require "rack/attack" @@ -30,6 +29,7 @@ class MiniTest::Spec include Rack::Test::Methods before do + Rails.cache = nil @_original_throttled_response = Rack::Attack.throttled_response @_original_blocklisted_response = Rack::Attack.blocklisted_response end