Class: Reputable::Middleware

Inherits:
Object
  • Object
show all
Defined in:
lib/reputable/middleware.rb

Overview

Rack middleware for automatic request tracking

RESILIENCE: This middleware is designed to NEVER break your application.

  • Disable entirely via ENV: REPUTABLE_ENABLED=false
  • All errors are caught and logged silently
  • Circuit breaker prevents repeated failures from impacting performance
  • Async mode (default) runs tracking in a separate thread

Examples:

Basic usage in config.ru

require 'reputable'
use Reputable::Middleware

With options

use Reputable::Middleware,
  skip_paths: ['/health', '/assets'],
  skip_if: ->(env) { env['HTTP_X_INTERNAL'] == 'true' },
  tag_builder: ->(env) { ["custom:tag"] }

Constant Summary collapse

DEFAULT_SKIP_PATHS =
%w[
  /health
  /healthz
  /ready
  /readyz
  /live
  /livez
  /metrics
  /favicon.ico
].freeze
DEFAULT_SKIP_EXTENSIONS =
%w[
  .js .css .png .jpg .jpeg .gif .svg .ico .woff .woff2 .ttf .eot .map
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Middleware

Returns a new instance of Middleware.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/reputable/middleware.rb', line 41

def initialize(app, options = {})
  @app = app
  @skip_paths = options.fetch(:skip_paths, DEFAULT_SKIP_PATHS)
  @skip_path_prefixes = Array(options[:skip_path_prefixes])
  @skip_extensions = options.fetch(:skip_extensions, DEFAULT_SKIP_EXTENSIONS)
  @skip_if = options[:skip_if]
  @skip_ips = Array(options[:skip_ips])
  @skip_user_agents = Array(options[:skip_user_agents]).map(&:downcase)
  @skip_query_params = Array(options[:skip_query_params]).map(&:downcase)
  @tag_builder = options[:tag_builder]
  @async = options.fetch(:async, true)
  @track_request = options.key?(:track_request) ? options[:track_request] : nil
  @reputation_gate = options.fetch(:reputation_gate, false)
  @expose_reputation = options.fetch(:expose_reputation, true)
  @challenge_action = options.fetch(:challenge_action, :verify)
  @block_action = options.fetch(:block_action, :blocked_page_remote)
  @challenge_redirect_status = options.fetch(:challenge_redirect_status, 302)
  @blocked_redirect_status = options.fetch(:blocked_redirect_status, 302)
  @blocked_redirect_url = options[:blocked_redirect_url]
  @verification_force_challenge = options.fetch(:verification_force_challenge, false)
  @verification_failure_url = options[:verification_failure_url]
  @verification_session_id = options[:verification_session_id]
  @verified_session_keys = Array(options.fetch(:verified_session_keys, [:reputable_verified_at, :reputable_verified]))
  @blocked_page_options = options.fetch(:blocked_page, {})
  @blocked_page_path = options[:blocked_page_path]
  @ignore_xhr = options.fetch(:ignore_xhr, false)
end

Instance Method Details

#call(env) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/reputable/middleware.rb', line 69

def call(env)
  # Generate a unique request ID for reconciliation with JS tracking
  # This ID is exposed to views so it can be included in the JS snippet
  env['reputable.request_id'] = SecureRandom.uuid

  ip = extract_ip(env)
  env["reputable.ip"] = ip
  path = env["PATH_INFO"] || "/"

  # Check for verification return parameters and verify signature if present
  handle_verification_return(env)

  # Optional: render blocked page path directly
  if (blocked_response = handle_blocked_page_request(env))
    return blocked_response
  end

  # Optional: enforce reputation gate before app
  if (gate_response = safe_reputation_gate(env))
    return gate_response
  end

  # Optional: expose reputation context for views/controllers
  safe_apply_reputation_context(env) if @expose_reputation

  # ALWAYS process the request first - tracking must never block
  status, headers, response = @app.call(env)

  # Only attempt tracking if enabled and not skipped
  # All tracking is wrapped in rescue to ensure it never fails
  safe_track_request(env)

  # Single summary line for requests that passed through without intervention
  if Reputable.verbose?
    rep_status = env["reputable.reputation_status"]
    Reputable.logger&.info("[Reputable] #{env['REQUEST_METHOD']} #{path} ip=#{ip} status=#{rep_status || 'unknown'} gate=#{@reputation_gate} result=pass")
  end

  [status, headers, response]
end