Skip to content

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

@ggambetti

Description

@ggambetti

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions