reputable 0.1.18 → 0.1.20

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: 40be87edb5494144f499f263572e00d98331cd07913239fca32b5ce71450df26
4
- data.tar.gz: 63cfe644b94396a6d72b05a75a45076e8bc41196f6c4970307992e6260d6f86f
3
+ metadata.gz: 6060e2e9fd11f70f0722c0f9683c1fe334f75aced2791dfde4a6336b5f08b429
4
+ data.tar.gz: 0c92fbbcc06ec6473e4af1c64b87003c92555c2137643d021b149aa3416a0acc
5
5
  SHA512:
6
- metadata.gz: ed8aee9a76c98611718621fde0c1dada4aff7449092c9fc6cd1990d545b91ee1641c169d841eea12b2b0eeb56edc399e2f8db612041565a2d90dbf76bad34d92
7
- data.tar.gz: 5b2eeb4fb8ddbf12beeb989b0a33aa5f42352c6a131ca1211ad5475b19978ba8244dbf52504555256aa0d79eb58935ab23be05cd503405d11d35b835bca506c0
6
+ metadata.gz: 1fee3bf3e197dbad2377296119ef87d43657f37a008982af4c14b15f39fcff0c59c13054a8d132b5c568cd31f695183e58b54ab34affac94a342893de7ace262
7
+ data.tar.gz: 43a20b7d26fcc4a0464e0c29c15be6cf25874335ea841c4b77c704dca2f4b4fdf94d42cd60ec2638ea29c3cbfdefef1bdf5bd161b22c907275853866e83cf703
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reputable (0.1.18)
4
+ reputable (0.1.20)
5
5
  connection_pool (~> 2.2)
6
6
  redis (>= 4.0, < 6.0)
7
7
 
data/README.md CHANGED
@@ -305,44 +305,6 @@ Notes:
305
305
  - Use `blocked_page_path` only for local blocked pages (or to build a custom `failure_url`).
306
306
  - Override `challenge_redirect_status` (default `302`) or `verification_force_challenge` if needed.
307
307
 
308
- ### ASN Fallback
309
-
310
- When an IP has no reputation, the middleware can fall back to checking ASN reputation. This is useful for blocking/challenging entire ASNs (e.g., datacenter ASNs known for abuse).
311
-
312
- **Enable via environment variable:**
313
- ```bash
314
- REPUTABLE_ASN_FALLBACK=true
315
- REPUTABLE_ASN_HEADER=HTTP_X_ASN # Optional, defaults to HTTP_X_ASN
316
- ```
317
-
318
- **Enable via configuration:**
319
- ```ruby
320
- Reputable.configure do |config|
321
- config.asn_fallback = true
322
- config.asn_header = "HTTP_X_ASN" # Or HTTP_CF_ASN for Cloudflare, etc.
323
- end
324
- ```
325
-
326
- **Enable via middleware option:**
327
- ```ruby
328
- config.middleware.use Reputable::Middleware,
329
- reputation_gate: true,
330
- asn_fallback: true
331
- ```
332
-
333
- **How it works:**
334
- 1. Middleware looks up IP reputation first
335
- 2. If IP has no reputation and ASN fallback is enabled, it extracts ASN from the configured header
336
- 3. If ASN has a reputation (blocked, challenged), that decision is applied
337
- 4. The `env['reputable.reputation_source']` is set to `'asn'` when using ASN-based decision
338
-
339
- **Providing ASN from your app:**
340
- If your app has its own GeoIP lookup, set the ASN directly:
341
- ```ruby
342
- # In a before_action or middleware
343
- request.env['reputable.asn'] = lookup_asn_for_ip(request.remote_ip)
344
- ```
345
-
346
308
  ### Server/JS Request Reconciliation
347
309
 
348
310
  When using both server-side tracking (Rack middleware) and client-side JavaScript tracking, requests can be double-counted. The reconciliation system prevents this by correlating requests using a unique `request_id`.
@@ -566,6 +528,58 @@ Reputable.lookup_reputation(:asn, "15169")
566
528
 
567
529
  **Note:** ASNs are normalized automatically - both `"15169"` and `"AS15169"` work.
568
530
 
531
+ ### Enforcing ASN Reputation in Controllers
532
+
533
+ If you want to challenge or block requests based on ASN reputation (independent of the middleware's IP-based gate), add a controller filter:
534
+
535
+ ```ruby
536
+ # app/controllers/application_controller.rb
537
+ class ApplicationController < ActionController::Base
538
+ before_action :enforce_asn_reputation
539
+
540
+ private
541
+
542
+ def enforce_asn_reputation
543
+ return if reputable_verified?
544
+
545
+ asn = request_asn
546
+ return unless asn
547
+
548
+ if Reputable.challenged_asn?(asn)
549
+ redirect_to Reputable.verification_url(
550
+ return_url: request.original_url,
551
+ session_id: session.id.to_s
552
+ ), status: 302, allow_other_host: true
553
+ elsif Reputable.blocked_asn?(asn)
554
+ redirect_to Reputable.blocked_page_url, status: 302, allow_other_host: true
555
+ end
556
+ end
557
+
558
+ def reputable_verified?
559
+ session[:reputable_verified] || session[:reputable_verified_at].present?
560
+ end
561
+
562
+ def request_asn
563
+ # Option 1: From a header (if HAProxy/Cloudflare provides it)
564
+ # request.headers["X-ASN"]&.sub(/^AS/i, "")
565
+
566
+ # Option 2: From your own GeoIP lookup
567
+ # GeoIP.lookup(request.remote_ip)&.asn
568
+
569
+ # Option 3: Stored in request.env from earlier middleware
570
+ # request.env["app.asn"]
571
+ end
572
+ end
573
+ ```
574
+
575
+ To apply only to specific controllers or actions:
576
+
577
+ ```ruby
578
+ class CheckoutController < ApplicationController
579
+ before_action :enforce_asn_reputation, only: [:create]
580
+ end
581
+ ```
582
+
569
583
  ---
570
584
 
571
585
  ## User Verification & Trust Flow
@@ -15,8 +15,7 @@ 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,
19
- :asn_fallback, :asn_header
18
+ :site_name, :support_email, :support_url
20
19
 
21
20
  # Alias for backward compatibility
22
21
  alias_method :verification_base_url, :base_url
@@ -83,10 +82,6 @@ module Reputable
83
82
  @site_name = ENV["REPUTABLE_SITE_NAME"]
84
83
  @support_email = ENV["REPUTABLE_SUPPORT_EMAIL"]
85
84
  @support_url = ENV["REPUTABLE_SUPPORT_URL"]
86
-
87
- # ASN fallback: when IP has no reputation, check ASN reputation
88
- @asn_fallback = env_truthy?("REPUTABLE_ASN_FALLBACK")
89
- @asn_header = ENV.fetch("REPUTABLE_ASN_HEADER", "HTTP_X_ASN")
90
85
  end
91
86
 
92
87
  # Alias for backward compatibility
@@ -163,20 +158,5 @@ module Reputable
163
158
  rescue IPAddr::InvalidAddressError
164
159
  false
165
160
  end
166
-
167
- # Check if ASN fallback is enabled
168
- def asn_fallback?
169
- @asn_fallback
170
- end
171
-
172
- private
173
-
174
- # Helper to check if an environment variable is truthy
175
- def env_truthy?(name)
176
- value = ENV[name]
177
- return false if value.nil?
178
-
179
- %w[1 true yes on enabled].include?(value.to_s.downcase)
180
- end
181
161
  end
182
162
  end
@@ -60,7 +60,6 @@ module Reputable
60
60
  @blocked_page_options = options.fetch(:blocked_page, {})
61
61
  @blocked_page_path = options[:blocked_page_path]
62
62
  @ignore_xhr = options.fetch(:ignore_xhr, false)
63
- @asn_fallback = options.key?(:asn_fallback) ? options[:asn_fallback] : nil
64
63
  end
65
64
 
66
65
  def call(env)
@@ -99,6 +98,7 @@ module Reputable
99
98
  def safe_reputation_gate(env)
100
99
  return nil unless @reputation_gate
101
100
  return nil unless Reputable.enabled?
101
+ return nil unless Reputable.operational?
102
102
  return nil if skip_request?(env)
103
103
 
104
104
  enforce_reputation_gate(env)
@@ -319,17 +319,6 @@ module Reputable
319
319
  ip = extract_ip(env)
320
320
  env["reputable.ip"] = ip
321
321
  status = Reputable::Reputation.lookup_ip(ip)
322
-
323
- # Fallback to ASN reputation if IP has no status and ASN fallback is enabled
324
- if status.nil? && asn_fallback_enabled?
325
- asn = extract_asn(env)
326
- if asn
327
- env["reputable.asn"] = asn
328
- status = Reputable::Reputation.lookup_asn(asn)
329
- env["reputable.reputation_source"] = "asn" if status
330
- end
331
- end
332
-
333
322
  env["reputable.reputation_status"] = status
334
323
  env["reputable.ignore_analytics"] = status.to_s.start_with?("untrusted")
335
324
  status
@@ -339,28 +328,6 @@ module Reputable
339
328
  nil
340
329
  end
341
330
 
342
- def asn_fallback_enabled?
343
- # Middleware option takes precedence, then config
344
- return @asn_fallback unless @asn_fallback.nil?
345
-
346
- Reputable.configuration.asn_fallback?
347
- end
348
-
349
- def extract_asn(env)
350
- # First check if app explicitly set it
351
- return env["reputable.asn"] if env["reputable.asn"]
352
-
353
- # Then check the configured header
354
- header = Reputable.configuration.asn_header
355
- value = env[header]
356
- return nil if value.nil? || value.empty?
357
-
358
- # Normalize: strip "AS" prefix if present
359
- value.to_s.strip.sub(/^AS/i, "")
360
- rescue StandardError
361
- nil
362
- end
363
-
364
331
  def blocked_page_options
365
332
  config = Reputable.configuration
366
333
  defaults = {
@@ -159,6 +159,9 @@ module Reputable
159
159
  )
160
160
  return if reputable_verified?(session_key: session_key)
161
161
 
162
+ # Skip redirect when circuit is open — the Reputable server is likely unreachable
163
+ return unless Reputable.operational?
164
+
162
165
  # Check for new format (reputable_s) or legacy format (reputable_signature)
163
166
  if params[:reputable_s] || params[:reputable_signature]
164
167
  if Reputable.verify_redirect_return(params)
@@ -207,6 +210,9 @@ module Reputable
207
210
  blocked_message: nil,
208
211
  blocked_show_ip: true
209
212
  )
213
+ # Skip all gating when circuit is open — the Reputable server is likely unreachable
214
+ return unless Reputable.operational?
215
+
210
216
  if current_ip_blocked?
211
217
  render_reputable_blocked_page(
212
218
  status: blocked_status,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reputable
4
- VERSION = "0.1.18"
4
+ VERSION = "0.1.20"
5
5
  end
data/lib/reputable.rb CHANGED
@@ -75,6 +75,28 @@ module Reputable
75
75
  !enabled?
76
76
  end
77
77
 
78
+ # Check if Reputable is operational (enabled AND circuit breaker is closed)
79
+ # Use this to guard redirect/gating decisions — when the circuit is open,
80
+ # the Reputable server is likely unreachable and redirects would fail.
81
+ # @return [Boolean] true if Reputable can be used, false otherwise
82
+ def operational?
83
+ return false unless enabled?
84
+ return false if Connection.circuit_open?
85
+ true
86
+ end
87
+
88
+ # Get a detailed status report for monitoring/debugging
89
+ # @return [Hash] status information
90
+ def status
91
+ {
92
+ enabled: enabled?,
93
+ operational: operational?,
94
+ circuit_breaker: Connection.circuit_open? ? "open" : "closed",
95
+ failure_count: Connection.failure_count,
96
+ circuit_opened_at: Connection.circuit_opened_at
97
+ }
98
+ end
99
+
78
100
  # Delegate tracking methods to Tracker
79
101
  def track_request(ip:, path:, **options)
80
102
  Tracker.track_request(ip: ip, path: path, **options)
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.18
4
+ version: 0.1.20
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-03 00:00:00.000000000 Z
11
+ date: 2026-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -137,7 +137,6 @@ files:
137
137
  - lib/reputable/reputation.rb
138
138
  - lib/reputable/tracker.rb
139
139
  - lib/reputable/version.rb
140
- - reputable.gemspec
141
140
  homepage: https://github.com/reputable-click/reputable-rb
142
141
  licenses:
143
142
  - MIT
data/reputable.gemspec DELETED
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/reputable/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "reputable"
7
- spec.version = Reputable::VERSION
8
- spec.authors = ["Reputable"]
9
- spec.email = ["support@reputable.click"]
10
-
11
- spec.summary = "Ruby client for Reputable - bot detection and reputation scoring"
12
- spec.description = "Track requests and manage IP reputation through Redis/Dragonfly integration with Reputable"
13
- spec.homepage = "https://github.com/reputable-click/reputable-rb"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.7.0"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
-
21
- spec.files = Dir.chdir(__dir__) do
22
- `git ls-files -z`.split("\x0").reject do |f|
23
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) || f.end_with?(".gem")
24
- end
25
- end
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
- spec.require_paths = ["lib"]
29
-
30
- spec.add_dependency "redis", ">= 4.0", "< 6.0"
31
- spec.add_dependency "connection_pool", "~> 2.2"
32
-
33
- spec.add_development_dependency "bundler", "~> 2.0"
34
- spec.add_development_dependency "rake", "~> 13.0"
35
- spec.add_development_dependency "rspec", "~> 3.12"
36
- spec.add_development_dependency "rack", "~> 2.0"
37
- spec.add_development_dependency "rubocop", "~> 1.0"
38
- end