rack-attack icon indicating copy to clipboard operation
rack-attack copied to clipboard

feature: refactor Rack::Attack#call to more easily patch in tracing

Open ggambetti opened this issue 7 months ago • 0 comments

Running Rack Attack in production systems sometimes requires debugging misbehaving rules. One of the common ways to find misbehaving application components in Ruby is by monkey-patching in tracing information (eg: https://github.com/DataDog/dd-trace-rb/blob/master/lib/datadog/tracing/contrib/). Tracing has the benefit of grouping calls to other components (eg: Redis, Valkey, PostgreSQL, MySQL, etc) in a causal way.

Refactoring Rack::Attack#call to separate out the rule evaluation component would enable consumers that want to patch in tracing to do so. Depending on how folks conceive of the short-circuit code (and it belonging in the action method) there could be a slight performance penalty if Rack Attack is not enabled for the class or has already been called (due to multiple invocations of path normalization, and allocation of Request objects).

I'm proposing (roughly):

module Rack
  class Attack
    def call(env)
      # Could be moved into action, if small performance penalty is acceptable.
      return @app.call(env) if !self.class.enabled || req["rack.attack.called"]

      env["rack.attack.called"] = true
      env['PATH_INFO'] = PathNormalizer.normalize_path(env['PATH_INFO'])
      request = Rack::Attack::Request.new(env)

      case action(request)
      when Safe, Skip
        @app.call(env)
      when Blocked
        if configuration.blocklisted_response
          configuration.blocklisted_response.call(env)
        else
          configuration.blocklisted_responder.call(request)
        end
      when Throttle
        if configuration.throttled_response
          configuration.throttled_response.call(env)
        else
          configuration.throttled_responder.call(request)
        end
      else
        configuration.tracked?(request)
        @app.call(env)
      end
    end

    # Determine which action to take for the given request.
    def action(request)
      return Skip if !self.class.enabled || req["rack.attack.called"]
      return Safe if configuration.safelisted?(req)
      return Blocked if configuration.blocklisted?(req)
      return Throttle if configuration.throttled?(req)
      nil
    end

  end
end

This allows an application to use prepends to patch the action method:

module RackAttackTracing
  def action(req)
    TracingLibrary.trace("rack.attack") do
      super
    end
  end
end
::Rack::Attack.send(:prepend, RackAttackTracing)

ggambetti avatar May 16 '25 15:05 ggambetti