reputable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/README.md +512 -0
- data/lib/reputable/configuration.rb +127 -0
- data/lib/reputable/connection.rb +150 -0
- data/lib/reputable/middleware.rb +266 -0
- data/lib/reputable/rails.rb +151 -0
- data/lib/reputable/reputation.rb +306 -0
- data/lib/reputable/tracker.rb +109 -0
- data/lib/reputable/version.rb +5 -0
- data/lib/reputable.rb +154 -0
- metadata +147 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redis"
|
|
4
|
+
require "connection_pool"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
7
|
+
module Reputable
|
|
8
|
+
# Redis connection pool manager with robust error handling
|
|
9
|
+
class Connection
|
|
10
|
+
# Track if we're in a degraded state (failed recently)
|
|
11
|
+
@circuit_open = false
|
|
12
|
+
@circuit_opened_at = nil
|
|
13
|
+
@failure_count = 0
|
|
14
|
+
|
|
15
|
+
# Circuit breaker settings
|
|
16
|
+
CIRCUIT_THRESHOLD = 5 # Failures before opening circuit
|
|
17
|
+
CIRCUIT_TIMEOUT = 30 # Seconds before trying again
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :circuit_open, :circuit_opened_at, :failure_count
|
|
21
|
+
|
|
22
|
+
def pool
|
|
23
|
+
@pool ||= ConnectionPool.new(
|
|
24
|
+
size: Reputable.configuration.pool_size,
|
|
25
|
+
timeout: Reputable.configuration.pool_timeout
|
|
26
|
+
) do
|
|
27
|
+
build_redis_client
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Safe wrapper that handles all errors gracefully
|
|
32
|
+
# @param default [Object] Value to return on failure
|
|
33
|
+
# @param context [String] Description for error logging
|
|
34
|
+
# @yield Block to execute with Redis connection
|
|
35
|
+
# @return [Object] Result of block or default value
|
|
36
|
+
def safe_with(default: nil, context: "redis_operation")
|
|
37
|
+
return default unless Reputable.enabled?
|
|
38
|
+
return default if circuit_open?
|
|
39
|
+
|
|
40
|
+
result = Timeout.timeout(operation_timeout) do
|
|
41
|
+
pool.with do |redis|
|
|
42
|
+
yield redis
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
record_success
|
|
47
|
+
result
|
|
48
|
+
rescue Timeout::Error, ConnectionPool::TimeoutError => e
|
|
49
|
+
record_failure(e, context)
|
|
50
|
+
default
|
|
51
|
+
rescue Redis::BaseError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
|
|
52
|
+
Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ECONNRESET,
|
|
53
|
+
Errno::EPIPE, SocketError, IOError => e
|
|
54
|
+
record_failure(e, context)
|
|
55
|
+
default
|
|
56
|
+
rescue OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError => e
|
|
57
|
+
# Catch all SSL/TLS errors - certificate issues, handshake failures, etc.
|
|
58
|
+
record_failure(e, context)
|
|
59
|
+
default
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
# Catch-all for any unexpected errors
|
|
62
|
+
record_failure(e, context)
|
|
63
|
+
default
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Legacy method for backward compatibility
|
|
67
|
+
def with(&block)
|
|
68
|
+
safe_with(default: nil, context: "legacy_with", &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reset!
|
|
72
|
+
@pool&.shutdown(&:close)
|
|
73
|
+
@pool = nil
|
|
74
|
+
@circuit_open = false
|
|
75
|
+
@circuit_opened_at = nil
|
|
76
|
+
@failure_count = 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if circuit breaker is open (we should skip operations)
|
|
80
|
+
def circuit_open?
|
|
81
|
+
return false unless @circuit_open
|
|
82
|
+
|
|
83
|
+
# Check if enough time has passed to try again
|
|
84
|
+
if @circuit_opened_at && (Time.now - @circuit_opened_at) > CIRCUIT_TIMEOUT
|
|
85
|
+
@circuit_open = false
|
|
86
|
+
@failure_count = 0
|
|
87
|
+
Reputable.logger&.info("Reputable circuit breaker: attempting reconnection")
|
|
88
|
+
return false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def operation_timeout
|
|
97
|
+
# Total timeout for an operation (connect + read + write + pool checkout)
|
|
98
|
+
config = Reputable.configuration
|
|
99
|
+
config.connect_timeout + config.read_timeout + config.pool_timeout + 0.5
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def record_success
|
|
103
|
+
@failure_count = 0
|
|
104
|
+
if @circuit_open
|
|
105
|
+
@circuit_open = false
|
|
106
|
+
Reputable.logger&.info("Reputable circuit breaker: closed (connection restored)")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def record_failure(error, context)
|
|
111
|
+
@failure_count += 1
|
|
112
|
+
|
|
113
|
+
Reputable.logger&.warn(
|
|
114
|
+
"Reputable #{context} error (#{@failure_count}/#{CIRCUIT_THRESHOLD}): #{error.class} - #{error.message}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Notify via callback if configured
|
|
118
|
+
Reputable.configuration.on_error&.call(error, context)
|
|
119
|
+
|
|
120
|
+
# Open circuit breaker if too many failures
|
|
121
|
+
if @failure_count >= CIRCUIT_THRESHOLD && !@circuit_open
|
|
122
|
+
@circuit_open = true
|
|
123
|
+
@circuit_opened_at = Time.now
|
|
124
|
+
Reputable.logger&.warn(
|
|
125
|
+
"Reputable circuit breaker: OPEN (#{CIRCUIT_THRESHOLD} failures, will retry in #{CIRCUIT_TIMEOUT}s)"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_redis_client
|
|
131
|
+
config = Reputable.configuration
|
|
132
|
+
url = config.redis_url
|
|
133
|
+
options = config.redis_options.dup
|
|
134
|
+
|
|
135
|
+
# Set timeouts
|
|
136
|
+
options[:connect_timeout] = config.connect_timeout
|
|
137
|
+
options[:read_timeout] = config.read_timeout
|
|
138
|
+
options[:write_timeout] = config.write_timeout
|
|
139
|
+
|
|
140
|
+
# Handle TLS URLs (rediss://)
|
|
141
|
+
if config.tls?
|
|
142
|
+
options[:ssl] = true
|
|
143
|
+
options[:ssl_params] = config.effective_ssl_params
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
Redis.new(url: url, **options)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reputable
|
|
4
|
+
# Rack middleware for automatic request tracking
|
|
5
|
+
#
|
|
6
|
+
# RESILIENCE: This middleware is designed to NEVER break your application.
|
|
7
|
+
# - Disable entirely via ENV: REPUTABLE_ENABLED=false
|
|
8
|
+
# - All errors are caught and logged silently
|
|
9
|
+
# - Circuit breaker prevents repeated failures from impacting performance
|
|
10
|
+
# - Async mode (default) runs tracking in a separate thread
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage in config.ru
|
|
13
|
+
# require 'reputable'
|
|
14
|
+
# use Reputable::Middleware
|
|
15
|
+
#
|
|
16
|
+
# @example With options
|
|
17
|
+
# use Reputable::Middleware,
|
|
18
|
+
# skip_paths: ['/health', '/assets'],
|
|
19
|
+
# skip_if: ->(env) { env['HTTP_X_INTERNAL'] == 'true' },
|
|
20
|
+
# tag_builder: ->(env) { ["custom:tag"] }
|
|
21
|
+
#
|
|
22
|
+
class Middleware
|
|
23
|
+
DEFAULT_SKIP_PATHS = %w[
|
|
24
|
+
/health
|
|
25
|
+
/healthz
|
|
26
|
+
/ready
|
|
27
|
+
/readyz
|
|
28
|
+
/live
|
|
29
|
+
/livez
|
|
30
|
+
/metrics
|
|
31
|
+
/favicon.ico
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
DEFAULT_SKIP_EXTENSIONS = %w[
|
|
35
|
+
.js .css .png .jpg .jpeg .gif .svg .ico .woff .woff2 .ttf .eot .map
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(app, options = {})
|
|
39
|
+
@app = app
|
|
40
|
+
@skip_paths = options.fetch(:skip_paths, DEFAULT_SKIP_PATHS)
|
|
41
|
+
@skip_extensions = options.fetch(:skip_extensions, DEFAULT_SKIP_EXTENSIONS)
|
|
42
|
+
@skip_if = options[:skip_if]
|
|
43
|
+
@tag_builder = options[:tag_builder]
|
|
44
|
+
@async = options.fetch(:async, true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def call(env)
|
|
48
|
+
# ALWAYS process the request first - tracking must never block
|
|
49
|
+
status, headers, response = @app.call(env)
|
|
50
|
+
|
|
51
|
+
# Only attempt tracking if enabled and not skipped
|
|
52
|
+
# All tracking is wrapped in rescue to ensure it never fails
|
|
53
|
+
safe_track_request(env)
|
|
54
|
+
|
|
55
|
+
[status, headers, response]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def safe_track_request(env)
|
|
61
|
+
# Skip if disabled globally
|
|
62
|
+
return unless Reputable.enabled?
|
|
63
|
+
|
|
64
|
+
# Skip if this request should be skipped
|
|
65
|
+
return if skip_request?(env)
|
|
66
|
+
|
|
67
|
+
track_request(env)
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
# NEVER let tracking errors propagate - just log and continue
|
|
70
|
+
Reputable.logger&.debug("Reputable middleware: #{e.class} - #{e.message}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def skip_request?(env)
|
|
74
|
+
return true if @skip_if&.call(env)
|
|
75
|
+
|
|
76
|
+
path = env["PATH_INFO"] || "/"
|
|
77
|
+
|
|
78
|
+
# Skip exact path matches
|
|
79
|
+
return true if @skip_paths.include?(path)
|
|
80
|
+
|
|
81
|
+
# Skip by extension
|
|
82
|
+
ext = File.extname(path).downcase
|
|
83
|
+
return true if @skip_extensions.include?(ext)
|
|
84
|
+
|
|
85
|
+
false
|
|
86
|
+
rescue StandardError
|
|
87
|
+
# If skip logic fails, default to skipping (safer)
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def track_request(env)
|
|
92
|
+
params = build_params(env)
|
|
93
|
+
return unless params
|
|
94
|
+
|
|
95
|
+
if @async
|
|
96
|
+
Tracker.track_request_async(**params)
|
|
97
|
+
else
|
|
98
|
+
Tracker.track_request(**params)
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
Reputable.logger&.debug("Reputable track_request: #{e.class} - #{e.message}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_params(env)
|
|
105
|
+
request = Rack::Request.new(env)
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
ip: extract_ip(env),
|
|
109
|
+
path: request.path,
|
|
110
|
+
query: request.query_string.empty? ? nil : request.query_string,
|
|
111
|
+
method: request.request_method,
|
|
112
|
+
session_id: extract_session_id(env),
|
|
113
|
+
session_present: session_present?(env),
|
|
114
|
+
user_agent: env["HTTP_USER_AGENT"],
|
|
115
|
+
referer: env["HTTP_REFERER"],
|
|
116
|
+
tags: build_tags(env)
|
|
117
|
+
}.compact
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
Reputable.logger&.debug("Reputable build_params: #{e.class} - #{e.message}")
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def extract_ip(env)
|
|
124
|
+
config = Reputable.configuration
|
|
125
|
+
|
|
126
|
+
# Try each header in priority order
|
|
127
|
+
config.ip_header_priority.each do |header|
|
|
128
|
+
value = env[header]
|
|
129
|
+
next if value.nil? || value.empty?
|
|
130
|
+
|
|
131
|
+
# X-Forwarded-For and Forwarded can contain multiple IPs
|
|
132
|
+
if header == "HTTP_X_FORWARDED_FOR"
|
|
133
|
+
ip = extract_from_xff(value)
|
|
134
|
+
return ip if ip
|
|
135
|
+
elsif header == "HTTP_FORWARDED"
|
|
136
|
+
ip = extract_from_forwarded(value)
|
|
137
|
+
return ip if ip
|
|
138
|
+
else
|
|
139
|
+
# Single IP headers
|
|
140
|
+
ip = value.to_s.strip
|
|
141
|
+
return ip unless ip.empty?
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Ultimate fallback
|
|
146
|
+
"0.0.0.0"
|
|
147
|
+
rescue StandardError
|
|
148
|
+
"0.0.0.0"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Extract real client IP from X-Forwarded-For header
|
|
152
|
+
# X-Forwarded-For: client, proxy1, proxy2
|
|
153
|
+
# We want the leftmost non-private IP
|
|
154
|
+
def extract_from_xff(xff)
|
|
155
|
+
return nil if xff.nil? || xff.empty?
|
|
156
|
+
|
|
157
|
+
config = Reputable.configuration
|
|
158
|
+
ips = xff.split(",").map(&:strip)
|
|
159
|
+
|
|
160
|
+
# Find the first (leftmost) public IP
|
|
161
|
+
ips.each do |ip|
|
|
162
|
+
next if ip.empty?
|
|
163
|
+
next if config.private_ip?(ip)
|
|
164
|
+
return ip
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# If all IPs are private, return the first one
|
|
168
|
+
ips.first&.strip
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Extract client IP from RFC 7239 Forwarded header
|
|
174
|
+
# Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
|
|
175
|
+
def extract_from_forwarded(forwarded)
|
|
176
|
+
return nil if forwarded.nil? || forwarded.empty?
|
|
177
|
+
|
|
178
|
+
# Parse the "for" directive
|
|
179
|
+
forwarded.split(";").each do |directive|
|
|
180
|
+
key, value = directive.split("=", 2).map(&:strip)
|
|
181
|
+
if key&.downcase == "for" && value
|
|
182
|
+
# Remove quotes and brackets (IPv6)
|
|
183
|
+
ip = value.gsub(/["'\[\]]/, "").strip
|
|
184
|
+
return ip unless ip.empty?
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
nil
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def extract_session_id(env)
|
|
194
|
+
# Try rack.session.id first
|
|
195
|
+
return env["rack.session.id"] if env["rack.session.id"]
|
|
196
|
+
|
|
197
|
+
# Try to get from rack.session
|
|
198
|
+
session = env["rack.session"]
|
|
199
|
+
return session.id.to_s if session.respond_to?(:id)
|
|
200
|
+
|
|
201
|
+
nil
|
|
202
|
+
rescue StandardError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def session_present?(env)
|
|
207
|
+
return true if env["rack.session.id"]
|
|
208
|
+
return true if env["rack.session"]&.any?
|
|
209
|
+
return true if env["HTTP_COOKIE"]&.include?("_session")
|
|
210
|
+
|
|
211
|
+
false
|
|
212
|
+
rescue StandardError
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def build_tags(env)
|
|
217
|
+
tags = []
|
|
218
|
+
|
|
219
|
+
# Add custom tags from tag_builder
|
|
220
|
+
if @tag_builder
|
|
221
|
+
begin
|
|
222
|
+
custom_tags = @tag_builder.call(env)
|
|
223
|
+
tags.concat(Array(custom_tags))
|
|
224
|
+
rescue StandardError
|
|
225
|
+
# Ignore tag builder errors
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Add source tag based on referer
|
|
230
|
+
if (referer = env["HTTP_REFERER"])
|
|
231
|
+
tag = categorize_referer(referer)
|
|
232
|
+
tags << tag if tag
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
tags.compact
|
|
236
|
+
rescue StandardError
|
|
237
|
+
[]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def categorize_referer(referer)
|
|
241
|
+
uri = URI.parse(referer) rescue nil
|
|
242
|
+
return nil unless uri&.host
|
|
243
|
+
|
|
244
|
+
host = uri.host.downcase
|
|
245
|
+
|
|
246
|
+
case host
|
|
247
|
+
when /google\./
|
|
248
|
+
"trust:channel:search"
|
|
249
|
+
when /bing\./
|
|
250
|
+
"trust:channel:search"
|
|
251
|
+
when /facebook\./, /fb\./
|
|
252
|
+
"trust:channel:social"
|
|
253
|
+
when /twitter\./, /t\.co/
|
|
254
|
+
"trust:channel:social"
|
|
255
|
+
when /instagram\./
|
|
256
|
+
"trust:channel:social"
|
|
257
|
+
when /linkedin\./
|
|
258
|
+
"trust:channel:social"
|
|
259
|
+
else
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
rescue StandardError
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reputable
|
|
4
|
+
# Rails-specific integration
|
|
5
|
+
module Rails
|
|
6
|
+
# Railtie for automatic Rails integration
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
initializer "reputable.configure_rails_initialization" do
|
|
9
|
+
# Auto-configure from Rails credentials or ENV
|
|
10
|
+
Reputable.configure do |config|
|
|
11
|
+
# Try Rails credentials first
|
|
12
|
+
if defined?(::Rails.application.credentials)
|
|
13
|
+
creds = ::Rails.application.credentials.reputable rescue nil
|
|
14
|
+
if creds
|
|
15
|
+
config.redis_url = creds[:redis_url] if creds[:redis_url]
|
|
16
|
+
config.tenant_id = creds[:tenant_id] if creds[:tenant_id]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "reputable.insert_middleware" do |app|
|
|
23
|
+
# Only insert middleware if explicitly enabled
|
|
24
|
+
if ENV["REPUTABLE_AUTO_MIDDLEWARE"] == "true"
|
|
25
|
+
app.middleware.use Reputable::Middleware
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Helper methods for controllers
|
|
31
|
+
module ControllerHelpers
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
|
|
34
|
+
# Track the current request with optional extra tags
|
|
35
|
+
def track_reputable_request(tags: [], **options)
|
|
36
|
+
Reputable::Tracker.track_request(
|
|
37
|
+
ip: request.remote_ip,
|
|
38
|
+
path: request.path,
|
|
39
|
+
query: request.query_string,
|
|
40
|
+
method: request.method,
|
|
41
|
+
session_id: session.id.to_s,
|
|
42
|
+
session_present: session.any?,
|
|
43
|
+
user_agent: request.user_agent,
|
|
44
|
+
referer: request.referer,
|
|
45
|
+
tags: tags,
|
|
46
|
+
**options
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Trust the current user's IP (e.g., after payment)
|
|
51
|
+
def trust_current_ip(reason:, **metadata)
|
|
52
|
+
Reputable::Reputation.trust_ip(
|
|
53
|
+
request.remote_ip,
|
|
54
|
+
reason: reason,
|
|
55
|
+
**metadata
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Trust the current user
|
|
60
|
+
def trust_current_user(reason:, **metadata)
|
|
61
|
+
return false unless respond_to?(:current_user) && current_user
|
|
62
|
+
|
|
63
|
+
Reputable::Reputation.trust_user(
|
|
64
|
+
current_user.id,
|
|
65
|
+
reason: reason,
|
|
66
|
+
**metadata
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Trust the current session
|
|
71
|
+
def trust_current_session(reason:, **metadata)
|
|
72
|
+
Reputable::Reputation.trust_session(
|
|
73
|
+
session.id.to_s,
|
|
74
|
+
reason: reason,
|
|
75
|
+
**metadata
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Challenge the current IP
|
|
80
|
+
def challenge_current_ip(reason:, **metadata)
|
|
81
|
+
Reputable::Reputation.challenge_ip(
|
|
82
|
+
request.remote_ip,
|
|
83
|
+
reason: reason,
|
|
84
|
+
**metadata
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Block the current IP
|
|
89
|
+
def block_current_ip(reason:, **metadata)
|
|
90
|
+
Reputable::Reputation.block_ip(
|
|
91
|
+
request.remote_ip,
|
|
92
|
+
reason: reason,
|
|
93
|
+
**metadata
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ========================================
|
|
98
|
+
# Lookup methods (O(1) Redis lookups)
|
|
99
|
+
# ========================================
|
|
100
|
+
|
|
101
|
+
# Check if current IP is trusted
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def current_ip_trusted?
|
|
104
|
+
Reputable::Reputation.trusted_ip?(request.remote_ip)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if current IP is blocked
|
|
108
|
+
# @return [Boolean]
|
|
109
|
+
def current_ip_blocked?
|
|
110
|
+
Reputable::Reputation.blocked_ip?(request.remote_ip)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if current IP should be challenged
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def current_ip_challenged?
|
|
116
|
+
Reputable::Reputation.challenged_ip?(request.remote_ip)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Get full reputation data for current IP
|
|
120
|
+
# @return [Hash, nil]
|
|
121
|
+
def current_ip_reputation
|
|
122
|
+
Reputable::Reputation.lookup(:ip, request.remote_ip)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get reputation status for current IP
|
|
126
|
+
# @return [String, nil]
|
|
127
|
+
def current_ip_status
|
|
128
|
+
Reputable::Reputation.lookup_ip(request.remote_ip)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Check if current user is trusted
|
|
132
|
+
# @return [Boolean]
|
|
133
|
+
def current_user_trusted?
|
|
134
|
+
return false unless respond_to?(:current_user) && current_user
|
|
135
|
+
|
|
136
|
+
Reputable::Reputation.trusted_user?(current_user.id)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check if current session is trusted
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def current_session_trusted?
|
|
142
|
+
Reputable::Reputation.trusted_session?(session.id.to_s)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Auto-load Railtie if Rails is present
|
|
149
|
+
if defined?(::Rails::Railtie)
|
|
150
|
+
require_relative "rails/railtie"
|
|
151
|
+
end
|