reputable 0.1.21 → 0.1.22

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbabd6630a3e0d348391e2795ea58cd8a858944101b0ac8a0dfaa04277b16035
4
- data.tar.gz: 3939cf81a472b76d4084d6236a39242d7a693e9f2434df576ac73bb153e8c5d0
3
+ metadata.gz: 594ed67d80dc9ae5bd493bbe7c3322c4ed79f1d4e661b7e36ddc57d46d350c71
4
+ data.tar.gz: e9b6093d3cc1a23fe9f881d292984c92e7a32c86ccb52c1581f60a12eae94078
5
5
  SHA512:
6
- metadata.gz: 4e868fd7037ffbecfda2be3103486940210bae975619d51d9c9f68521c0a9d7ea36bf7ebbea7ae5da8c90e19158fc11bbe82762b65c00d216d03d1342eb78d2a
7
- data.tar.gz: f664f98a97054be9f2f9ddc89611f2fb68028b641eb47057534633dec2291939f770a792bc0b0506d7ce2297f0c5648e36d885315490810b338fe6dee1c6ca34
6
+ metadata.gz: c921b3e37a852a8226551cd43c4787760a3c41230dbfaf7535b93c62599185b7e9438c4421cf7e03533325036b184b4a6c817cedee69606f057dec27abb93b79
7
+ data.tar.gz: 6da6fc589dd71010685ef7e02d68b5c0b6c4a97fd457187f1c432fc9a5c992a8821d49bda29cd2f169ed95262e28c63a0f4be08b41d811555f166c5a14717c92
data/README.md CHANGED
@@ -29,10 +29,13 @@ bundle install
29
29
  Reputable.configure do |config|
30
30
  # Required: Redis/Dragonfly URL (TLS supported via rediss://)
31
31
  config.redis_url = ENV['REPUTABLE_REDIS_URL']
32
-
33
- # Optional: Enable logging (logs at debug level)
34
- Reputable.logger = Rails.logger
32
+
33
+ # Optional: Verbose logging (logs every middleware decision)
34
+ # config.verbose = true
35
35
  end
36
+
37
+ # Optional: Enable logging
38
+ Reputable.logger = Rails.logger
36
39
  ```
37
40
 
38
41
  ### 2. Add Middleware
@@ -100,6 +103,9 @@ REPUTABLE_WRITE_TIMEOUT=0.5 # Redis write timeout (seconds)
100
103
  REPUTABLE_POOL_SIZE=5 # Connection pool size
101
104
  REPUTABLE_POOL_TIMEOUT=1.0 # Pool checkout timeout (seconds)
102
105
 
106
+ # Optional: Verbose logging (logs every middleware decision at info level)
107
+ REPUTABLE_VERBOSE=true
108
+
103
109
  # Optional: SSL (for custom certificates)
104
110
  REPUTABLE_SSL_VERIFY=false # Disable SSL verification (NOT recommended for production)
105
111
  ```
@@ -155,6 +161,10 @@ Reputable.configure do |config|
155
161
  config.support_email = ENV['REPUTABLE_SUPPORT_EMAIL']
156
162
  config.support_url = ENV['REPUTABLE_SUPPORT_URL']
157
163
 
164
+ # Verbose logging — logs every middleware decision at info level
165
+ # Also available via REPUTABLE_VERBOSE=true
166
+ config.verbose = true
167
+
158
168
  # Error callback (optional)
159
169
  config.on_error = ->(error, context) {
160
170
  # Report to your error tracking service
@@ -162,7 +172,7 @@ Reputable.configure do |config|
162
172
  }
163
173
  end
164
174
 
165
- # Enable logging
175
+ # Enable logging (required for verbose output)
166
176
  Reputable.logger = Rails.logger
167
177
  ```
168
178
 
@@ -719,6 +729,42 @@ config.on_error = ->(error, context) {
719
729
 
720
730
  ---
721
731
 
732
+ ## Verbose Logging / Debugging
733
+
734
+ When troubleshooting middleware behavior (e.g., unexpected redirects or blocked requests), enable verbose mode to see every decision the gem makes:
735
+
736
+ ```ruby
737
+ # config/initializers/reputable.rb
738
+ Reputable.configure do |config|
739
+ config.verbose = true
740
+ # ...
741
+ end
742
+ Reputable.logger = Rails.logger
743
+ ```
744
+
745
+ Or via environment variable:
746
+
747
+ ```bash
748
+ REPUTABLE_VERBOSE=true
749
+ ```
750
+
751
+ When enabled, the middleware logs at `info` level with a `[Reputable]` prefix. Example output for a challenged request:
752
+
753
+ ```
754
+ [Reputable] request: GET / ip=68.6.118.12 rid=abc-123
755
+ [Reputable] state: enabled=true operational=true gate=true track=false
756
+ [Reputable] extract_ip: 68.6.118.12 (from HTTP_X_FORWARDED_FOR: 68.6.118.12, 10.0.0.1)
757
+ [Reputable] lookup: reputation:ip:68.6.118.12 → status=untrusted_challenge reason=inherited:asn:7922 source=inherited
758
+ [Reputable] gate: ip=68.6.118.12 status="untrusted_challenge"
759
+ [Reputable] verified_session?: false (reputable_verified_at= reputable_verified=)
760
+ [Reputable] gate: CHALLENGING ip=68.6.118.12 action=verify
761
+ [Reputable] challenge: REDIRECTING 302 → https://api.reputable.click/_reputable/verify?token=...
762
+ ```
763
+
764
+ When verbose is `false` (the default), none of these logs are emitted.
765
+
766
+ ---
767
+
722
768
  ## Tags for Classification
723
769
 
724
770
  Use tags to classify requests for behavioral analysis:
@@ -15,7 +15,8 @@ module Reputable
15
15
  :connect_timeout, :read_timeout, :write_timeout,
16
16
  :ssl_params, :trusted_proxies, :ip_header_priority,
17
17
  :on_error, :trusted_keys, :base_url,
18
- :site_name, :support_email, :support_url
18
+ :site_name, :support_email, :support_url,
19
+ :verbose
19
20
 
20
21
  # Alias for backward compatibility
21
22
  alias_method :verification_base_url, :base_url
@@ -82,6 +83,7 @@ module Reputable
82
83
  @site_name = ENV["REPUTABLE_SITE_NAME"]
83
84
  @support_email = ENV["REPUTABLE_SUPPORT_EMAIL"]
84
85
  @support_url = ENV["REPUTABLE_SUPPORT_URL"]
86
+ @verbose = %w[1 true yes on].include?(ENV.fetch("REPUTABLE_VERBOSE", "").downcase)
85
87
  end
86
88
 
87
89
  # Alias for backward compatibility
@@ -67,6 +67,15 @@ module Reputable
67
67
  # This ID is exposed to views so it can be included in the JS snippet
68
68
  env['reputable.request_id'] = SecureRandom.uuid
69
69
 
70
+ ip = extract_ip(env)
71
+ env["reputable.ip"] = ip
72
+ path = env["PATH_INFO"] || "/"
73
+
74
+ if Reputable.verbose?
75
+ Reputable.logger&.info("[Reputable] request: #{env['REQUEST_METHOD']} #{path} ip=#{ip} rid=#{env['reputable.request_id']}")
76
+ Reputable.logger&.info("[Reputable] state: enabled=#{Reputable.enabled?} operational=#{Reputable.operational?} gate=#{@reputation_gate} track=#{track_request_enabled?}")
77
+ end
78
+
70
79
  # Check for verification return parameters and verify signature if present
71
80
  handle_verification_return(env)
72
81
 
@@ -96,29 +105,53 @@ module Reputable
96
105
  private
97
106
 
98
107
  def safe_reputation_gate(env)
99
- return nil unless @reputation_gate
100
- return nil unless Reputable.enabled?
101
- return nil unless Reputable.operational?
102
- return nil if skip_request?(env)
108
+ unless @reputation_gate
109
+ Reputable.logger&.info("[Reputable] gate: skipped (reputation_gate=false)") if Reputable.verbose?
110
+ return nil
111
+ end
112
+ unless Reputable.enabled?
113
+ Reputable.logger&.info("[Reputable] gate: skipped (disabled)") if Reputable.verbose?
114
+ return nil
115
+ end
116
+ unless Reputable.operational?
117
+ Reputable.logger&.info("[Reputable] gate: skipped (not operational, circuit_breaker=#{Connection.circuit_open? ? 'open' : 'closed'} failures=#{Connection.failure_count})") if Reputable.verbose?
118
+ return nil
119
+ end
120
+ if skip_request?(env)
121
+ Reputable.logger&.info("[Reputable] gate: skipped (path excluded)") if Reputable.verbose?
122
+ return nil
123
+ end
103
124
 
104
125
  enforce_reputation_gate(env)
105
126
  rescue StandardError => e
106
- Reputable.logger&.debug("Reputable reputation gate: #{e.class} - #{e.message}")
127
+ Reputable.logger&.warn("[Reputable] gate: exception #{e.class} - #{e.message}")
128
+ Reputable.logger&.debug(e.backtrace&.first(5)&.join("\n")) if Reputable.verbose?
107
129
  nil
108
130
  end
109
131
 
110
132
  def enforce_reputation_gate(env)
111
133
  status = reputation_status(env)
112
- return nil if status.nil?
113
134
  ip = env["reputable.ip"] || extract_ip(env)
114
135
 
136
+ if Reputable.verbose?
137
+ Reputable.logger&.info("[Reputable] gate: ip=#{ip} status=#{status.inspect}")
138
+ end
139
+
140
+ return nil if status.nil?
141
+
115
142
  case status
116
143
  when "untrusted_block"
144
+ Reputable.logger&.info("[Reputable] gate: BLOCKING ip=#{ip} action=#{@block_action}") if Reputable.verbose?
117
145
  handle_block_action(env, ip)
118
146
  when "untrusted_challenge"
119
- return nil if verified_session?(env)
147
+ if verified_session?(env)
148
+ Reputable.logger&.info("[Reputable] gate: ip=#{ip} has untrusted_challenge but session is verified, passing through") if Reputable.verbose?
149
+ return nil
150
+ end
151
+ Reputable.logger&.info("[Reputable] gate: CHALLENGING ip=#{ip} action=#{@challenge_action}") if Reputable.verbose?
120
152
  handle_challenge_action(env)
121
153
  else
154
+ Reputable.logger&.info("[Reputable] gate: ip=#{ip} status=#{status} — allowing") if Reputable.verbose?
122
155
  nil
123
156
  end
124
157
  end
@@ -156,19 +189,29 @@ module Reputable
156
189
  query = request.query_string
157
190
  return unless query.include?("reputable_r") || query.include?("reputable_status")
158
191
 
192
+ Reputable.logger&.info("[Reputable] verification return detected in query string") if Reputable.verbose?
193
+
159
194
  params = request.params
160
195
 
161
196
  # Determine status from new format or legacy format
162
197
  status = if params["reputable_r"]
163
198
  decoded = Reputable.decode_reputable_response(params)
199
+ Reputable.logger&.info("[Reputable] verification: new format decoded=#{decoded.inspect}") if Reputable.verbose?
164
200
  decoded&.dig("status")
165
201
  else
202
+ Reputable.logger&.info("[Reputable] verification: legacy format status=#{params['reputable_status']}") if Reputable.verbose?
166
203
  params["reputable_status"]
167
204
  end
168
205
 
169
- return unless status == "pass"
206
+ unless status == "pass"
207
+ Reputable.logger&.info("[Reputable] verification: status=#{status.inspect} (not 'pass'), ignoring") if Reputable.verbose?
208
+ return
209
+ end
170
210
 
171
- if Reputable.verify_redirect_return(params)
211
+ sig_valid = Reputable.verify_redirect_return(params)
212
+ Reputable.logger&.info("[Reputable] verification: signature_valid=#{sig_valid}") if Reputable.verbose?
213
+
214
+ if sig_valid
172
215
  env["reputable.verified"] = true
173
216
 
174
217
  # Extract ignore_analytics from new format or legacy format
@@ -182,15 +225,21 @@ module Reputable
182
225
  unless ignore_analytics.nil?
183
226
  env["reputable.ignore_analytics"] = ignore_analytics == true || ignore_analytics.to_s == "true"
184
227
  end
185
-
228
+
186
229
  # Store in session if available
187
230
  if env["rack.session"]
188
231
  env["rack.session"]["reputable_verified"] = true
189
232
  env["rack.session"]["reputable_verified_at"] = Time.now.to_i
233
+ Reputable.logger&.info("[Reputable] verification: stored in session, verified=true") if Reputable.verbose?
234
+ else
235
+ Reputable.logger&.info("[Reputable] verification: no rack.session available, cannot persist") if Reputable.verbose?
190
236
  end
237
+ else
238
+ Reputable.logger&.warn("[Reputable] verification: INVALID signature, rejecting")
191
239
  end
192
240
  rescue StandardError => e
193
- Reputable.logger&.debug("Reputable verification error: #{e.message}")
241
+ Reputable.logger&.warn("[Reputable] verification: exception #{e.class} - #{e.message}")
242
+ Reputable.logger&.debug(e.backtrace&.first(5)&.join("\n")) if Reputable.verbose?
194
243
  end
195
244
 
196
245
  def handle_blocked_page_request(env)
@@ -209,15 +258,20 @@ module Reputable
209
258
  def handle_block_action(env, ip)
210
259
  case @block_action
211
260
  when :blocked_page
261
+ Reputable.logger&.info("[Reputable] block: rendering inline blocked page for ip=#{ip}") if Reputable.verbose?
212
262
  build_blocked_page_response(ip)
213
263
  when :blocked_page_remote
214
264
  url = resolve_blocked_redirect_url(env) || Reputable.blocked_page_url
265
+ Reputable.logger&.info("[Reputable] block: REDIRECTING #{@blocked_redirect_status} → #{url}") if Reputable.verbose?
215
266
  redirect_response(url, @blocked_redirect_status)
216
267
  when :forbidden
268
+ Reputable.logger&.info("[Reputable] block: returning 403 for ip=#{ip}") if Reputable.verbose?
217
269
  [403, { "Content-Type" => "text/plain; charset=utf-8" }, ["Forbidden"]]
218
270
  when Proc
271
+ Reputable.logger&.info("[Reputable] block: calling custom proc for ip=#{ip}") if Reputable.verbose?
219
272
  @block_action.call(env, ip)
220
273
  else
274
+ Reputable.logger&.info("[Reputable] block: rendering default blocked page for ip=#{ip}") if Reputable.verbose?
221
275
  build_blocked_page_response(ip)
222
276
  end
223
277
  end
@@ -226,13 +280,20 @@ module Reputable
226
280
  case @challenge_action
227
281
  when :verify
228
282
  keys = Reputable.configuration.trusted_keys
229
- return nil if keys.nil? || keys.empty?
283
+ if keys.nil? || keys.empty?
284
+ Reputable.logger&.info("[Reputable] challenge: no trusted_keys configured, cannot redirect to verification") if Reputable.verbose?
285
+ return nil
286
+ end
230
287
 
231
288
  request = Rack::Request.new(env)
232
289
  return_url = request.url
233
290
  failure_url = build_verification_failure_url(request)
234
291
  session_id = resolve_session_id(env)
235
292
 
293
+ if Reputable.verbose?
294
+ Reputable.logger&.info("[Reputable] challenge: building verification URL return_url=#{return_url} failure_url=#{failure_url} session_id=#{session_id}")
295
+ end
296
+
236
297
  redirect_url = Reputable.verification_url(
237
298
  return_url: return_url,
238
299
  failure_url: failure_url,
@@ -240,10 +301,15 @@ module Reputable
240
301
  force_challenge: @verification_force_challenge
241
302
  )
242
303
 
243
- return nil if redirect_url == return_url
304
+ if redirect_url == return_url
305
+ Reputable.logger&.info("[Reputable] challenge: verification_url == return_url, skipping redirect") if Reputable.verbose?
306
+ return nil
307
+ end
244
308
 
309
+ Reputable.logger&.info("[Reputable] challenge: REDIRECTING #{@challenge_redirect_status} → #{redirect_url}") if Reputable.verbose?
245
310
  redirect_response(redirect_url, @challenge_redirect_status)
246
311
  when Proc
312
+ Reputable.logger&.info("[Reputable] challenge: calling custom proc") if Reputable.verbose?
247
313
  @challenge_action.call(env)
248
314
  else
249
315
  nil
@@ -287,14 +353,27 @@ module Reputable
287
353
  end
288
354
 
289
355
  def verified_session?(env)
290
- return true if env["reputable.verified"]
356
+ if env["reputable.verified"]
357
+ Reputable.logger&.info("[Reputable] verified_session?: true (env flag set this request)") if Reputable.verbose?
358
+ return true
359
+ end
291
360
 
292
361
  session = env["rack.session"]
293
- return false unless session
362
+ unless session
363
+ Reputable.logger&.info("[Reputable] verified_session?: false (no rack.session)") if Reputable.verbose?
364
+ return false
365
+ end
294
366
 
295
- @verified_session_keys.any? do |key|
367
+ found = @verified_session_keys.any? do |key|
296
368
  session[key] || session[key.to_s]
297
369
  end
370
+
371
+ if Reputable.verbose?
372
+ keys_state = @verified_session_keys.map { |k| "#{k}=#{session[k] || session[k.to_s]}" }.join(" ")
373
+ Reputable.logger&.info("[Reputable] verified_session?: #{found} (#{keys_state})")
374
+ end
375
+
376
+ found
298
377
  rescue StandardError
299
378
  false
300
379
  end
@@ -408,27 +487,36 @@ module Reputable
408
487
 
409
488
  def extract_ip(env)
410
489
  config = Reputable.configuration
411
-
490
+
412
491
  # Try each header in priority order
413
492
  config.ip_header_priority.each do |header|
414
493
  value = env[header]
415
494
  next if value.nil? || value.empty?
416
-
495
+
417
496
  # X-Forwarded-For and Forwarded can contain multiple IPs
418
497
  if header == "HTTP_X_FORWARDED_FOR"
419
498
  ip = extract_from_xff(value)
420
- return ip if ip
499
+ if ip
500
+ Reputable.logger&.info("[Reputable] extract_ip: #{ip} (from #{header}: #{value})") if Reputable.verbose?
501
+ return ip
502
+ end
421
503
  elsif header == "HTTP_FORWARDED"
422
504
  ip = extract_from_forwarded(value)
423
- return ip if ip
505
+ if ip
506
+ Reputable.logger&.info("[Reputable] extract_ip: #{ip} (from #{header}: #{value})") if Reputable.verbose?
507
+ return ip
508
+ end
424
509
  else
425
510
  # Single IP headers
426
511
  ip = value.to_s.strip
427
- return ip unless ip.empty?
512
+ unless ip.empty?
513
+ Reputable.logger&.info("[Reputable] extract_ip: #{ip} (from #{header})") if Reputable.verbose?
514
+ return ip
515
+ end
428
516
  end
429
517
  end
430
518
 
431
- # Ultimate fallback
519
+ Reputable.logger&.info("[Reputable] extract_ip: 0.0.0.0 (no headers matched)") if Reputable.verbose?
432
520
  "0.0.0.0"
433
521
  rescue StandardError
434
522
  "0.0.0.0"
@@ -133,20 +133,25 @@ module Reputable
133
133
  return nil unless valid_entity_type?(entity_type)
134
134
 
135
135
  key = "reputation:#{entity_type}:#{entity_id}"
136
+ Reputable.logger&.info("[Reputable] lookup: key=#{key}") if Reputable.verbose?
136
137
 
137
138
  data = Connection.safe_with(default: nil, context: "reputation_lookup") do |redis|
138
139
  redis.hgetall(key)
139
140
  end
140
141
 
141
- return nil if data.nil? || data.empty?
142
+ if data.nil? || data.empty?
143
+ Reputable.logger&.info("[Reputable] lookup: #{key} → (none)") if Reputable.verbose?
144
+ return nil
145
+ end
142
146
 
143
147
  # Check if expired (Redis TTL should handle this, but double-check)
144
148
  expires_at = data["expires_at"].to_i
145
149
  if expires_at > 0 && expires_at < (Time.now.to_f * 1000).to_i
150
+ Reputable.logger&.info("[Reputable] lookup: #{key} → expired (expires_at=#{expires_at})") if Reputable.verbose?
146
151
  return nil
147
152
  end
148
153
 
149
- {
154
+ result = {
150
155
  status: data["status"],
151
156
  reason: data["reason"],
152
157
  source: data["source"],
@@ -154,8 +159,11 @@ module Reputable
154
159
  expires_at: expires_at,
155
160
  metadata: safe_parse_json(data["metadata"])
156
161
  }
162
+
163
+ Reputable.logger&.info("[Reputable] lookup: #{key} → status=#{result[:status]} reason=#{result[:reason]} source=#{result[:source]}") if Reputable.verbose?
164
+ result
157
165
  rescue StandardError => e
158
- Reputable.logger&.debug("Reputable lookup error: #{e.class} - #{e.message}")
166
+ Reputable.logger&.warn("[Reputable] lookup: exception #{e.class} - #{e.message}")
159
167
  nil
160
168
  end
161
169
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reputable
4
- VERSION = "0.1.21"
4
+ VERSION = "0.1.22"
5
5
  end
data/lib/reputable.rb CHANGED
@@ -69,6 +69,12 @@ module Reputable
69
69
  configuration.enabled?
70
70
  end
71
71
 
72
+ # Check if verbose logging is enabled
73
+ # @return [Boolean]
74
+ def verbose?
75
+ configuration.verbose
76
+ end
77
+
72
78
  # Check if Reputable is disabled
73
79
  # @return [Boolean]
74
80
  def disabled?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reputable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.21
4
+ version: 0.1.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reputable
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-07 00:00:00.000000000 Z
11
+ date: 2026-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis