reputable 0.1.8 → 0.1.10
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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +70 -3
- data/RELEASING.md +26 -0
- data/lib/reputable/blocked_page.rb +171 -0
- data/lib/reputable/configuration.rb +6 -2
- data/lib/reputable/middleware.rb +190 -0
- data/lib/reputable/rails.rb +69 -2
- data/lib/reputable/reputation.rb +6 -4
- data/lib/reputable/version.rb +1 -1
- data/lib/reputable.rb +13 -4
- data/reputable.gemspec +38 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10ace4d8c54f4fe4bcf4a1a2bbba66015dbff719b353ee9331e9f3ad56400c69
|
|
4
|
+
data.tar.gz: d3044684df204b097e1a739f29425947306ed9c5533d4a20c66b1064dafc8892
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3e578a48821efe6610ee60d0560c366fa2136e4fcbc93274822abda5cfb776e0aa924fe7877ff3c2164b177328fe630734fbf5d972103d741d5fc5f8db3f65b
|
|
7
|
+
data.tar.gz: 077b28a5af24557650e4e82759f63656532da2586f200b68a1dee2ebb6315c238efa44bf8dbb5a58634eb3cecb1819649772abeb65f78efcd064339453d842e3
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Ruby gem for integrating with Reputable - bot detection and reputation scoring f
|
|
|
4
4
|
|
|
5
5
|
**Resilience First**: This gem is designed to never break your application. All operations fail silently with safe defaults.
|
|
6
6
|
|
|
7
|
+
Release notes and version bumps: see `clients/ruby/RELEASING.md`.
|
|
8
|
+
|
|
7
9
|
## Installation
|
|
8
10
|
|
|
9
11
|
Add to your Gemfile:
|
|
@@ -83,6 +85,11 @@ REPUTABLE_REDIS_URL=rediss://user:password@your-dragonfly.example.com:6379
|
|
|
83
85
|
# Optional: Base URL for verification and API endpoints (domain only)
|
|
84
86
|
REPUTABLE_BASE_URL=https://api.reputable.click
|
|
85
87
|
|
|
88
|
+
# Optional: Blocked page branding/support info
|
|
89
|
+
REPUTABLE_SITE_NAME="Example Store"
|
|
90
|
+
REPUTABLE_SUPPORT_EMAIL=support@example.com
|
|
91
|
+
REPUTABLE_SUPPORT_URL=https://example.com/support
|
|
92
|
+
|
|
86
93
|
# Optional: Disable entirely (useful for test environments)
|
|
87
94
|
REPUTABLE_ENABLED=false
|
|
88
95
|
|
|
@@ -122,7 +129,7 @@ Reputable.configure do |config|
|
|
|
122
129
|
# Customize TTLs (in seconds, 0 = forever)
|
|
123
130
|
config.default_ttls = {
|
|
124
131
|
trusted_verified: 0, # Forever
|
|
125
|
-
trusted_behavior:
|
|
132
|
+
trusted_behavior: 365 * 24 * 3600, # 1 year
|
|
126
133
|
untrusted_challenge: 7 * 24 * 3600,
|
|
127
134
|
untrusted_block: 7 * 24 * 3600,
|
|
128
135
|
untrusted_ignore: 7 * 24 * 3600
|
|
@@ -142,6 +149,11 @@ Reputable.configure do |config|
|
|
|
142
149
|
# Supports comma-separated list in REPUTABLE_TRUSTED_KEYS or single key in REPUTABLE_TRUSTED_KEY
|
|
143
150
|
config.trusted_keys = ENV['REPUTABLE_TRUSTED_KEYS']&.split(',') || ENV['REPUTABLE_TRUSTED_KEY']
|
|
144
151
|
config.base_url = ENV['REPUTABLE_BASE_URL'] # Domain only
|
|
152
|
+
|
|
153
|
+
# Optional: blocked page branding/support info
|
|
154
|
+
config.site_name = ENV['REPUTABLE_SITE_NAME']
|
|
155
|
+
config.support_email = ENV['REPUTABLE_SUPPORT_EMAIL']
|
|
156
|
+
config.support_url = ENV['REPUTABLE_SUPPORT_URL']
|
|
145
157
|
|
|
146
158
|
# Error callback (optional)
|
|
147
159
|
config.on_error = ->(error, context) {
|
|
@@ -260,6 +272,27 @@ config.middleware.use Reputable::Middleware,
|
|
|
260
272
|
async: true
|
|
261
273
|
```
|
|
262
274
|
|
|
275
|
+
### Optional Reputation Gate
|
|
276
|
+
|
|
277
|
+
If you want the middleware to enforce IP reputation decisions, enable the gate:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
config.middleware.use Reputable::Middleware,
|
|
281
|
+
reputation_gate: true,
|
|
282
|
+
challenge_action: :verify, # Redirect to verification for untrusted_challenge
|
|
283
|
+
block_action: :blocked_page_remote, # Redirect to hosted blocked page (uses app UI settings)
|
|
284
|
+
blocked_page_path: "/_reputable/blocked" # Only used for local blocked page
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Notes:
|
|
288
|
+
- For `untrusted_challenge`, the middleware redirects to the Reputable verification URL.
|
|
289
|
+
- For `untrusted_block`, the default is to redirect to the hosted blocked page (`/_reputable/verify/blocked`).
|
|
290
|
+
- The hosted blocked page uses the same app UI settings as the verify/failure pages (`siteName`, `supportEmail`).
|
|
291
|
+
- To render a local blocked page instead, set `block_action: :blocked_page` and pass `blocked_page` options.
|
|
292
|
+
- To use a custom hosted page, set `blocked_redirect_url: "https://example.com/blocked"`.
|
|
293
|
+
- Use `blocked_page_path` only for local blocked pages (or to build a custom `failure_url`).
|
|
294
|
+
- Override `challenge_redirect_status` (default `302`) or `verification_force_challenge` if needed.
|
|
295
|
+
|
|
263
296
|
### Default Skipped Paths
|
|
264
297
|
|
|
265
298
|
The middleware automatically skips:
|
|
@@ -330,6 +363,31 @@ reputable_verified?
|
|
|
330
363
|
clear_reputable_verification!
|
|
331
364
|
```
|
|
332
365
|
|
|
366
|
+
### Reputation Gate Helpers
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
class SessionsController < ApplicationController
|
|
370
|
+
def new
|
|
371
|
+
require_reputable_reputation!
|
|
372
|
+
# If not blocked/challenged, continue
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`require_reputable_reputation!` will:
|
|
378
|
+
- Render a blocked page for `untrusted_block`
|
|
379
|
+
- Run verification flow for `untrusted_challenge`
|
|
380
|
+
|
|
381
|
+
You can also render the blocked page directly:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
render_reputable_blocked_page(
|
|
385
|
+
site_name: "Example Store",
|
|
386
|
+
support_email: "support@example.com",
|
|
387
|
+
support_url: "https://example.com/support"
|
|
388
|
+
)
|
|
389
|
+
```
|
|
390
|
+
|
|
333
391
|
---
|
|
334
392
|
|
|
335
393
|
## Manual API Usage
|
|
@@ -358,8 +416,17 @@ Reputable.track_request_async(
|
|
|
358
416
|
### Reputation Management
|
|
359
417
|
|
|
360
418
|
```ruby
|
|
361
|
-
# Trust IP
|
|
362
|
-
Reputable.trust_ip(request.ip, reason: '
|
|
419
|
+
# Trust IP (behavioral by default, uses default TTL)
|
|
420
|
+
Reputable.trust_ip(request.ip, reason: 'behavior_trust', order_id: order.id)
|
|
421
|
+
|
|
422
|
+
# Trust IP as verified (forever, explicitly)
|
|
423
|
+
Reputable.trust_ip(
|
|
424
|
+
request.ip,
|
|
425
|
+
reason: 'payment_completed',
|
|
426
|
+
status: :trusted_verified,
|
|
427
|
+
ttl: 0,
|
|
428
|
+
order_id: order.id
|
|
429
|
+
)
|
|
363
430
|
|
|
364
431
|
# Challenge (require CAPTCHA, etc.)
|
|
365
432
|
Reputable.challenge_ip(request.ip, reason: 'unusual_activity')
|
data/RELEASING.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Releasing the Ruby Gem
|
|
2
|
+
|
|
3
|
+
## Update version + changelog
|
|
4
|
+
|
|
5
|
+
1. Bump the version in `clients/ruby/lib/reputable/version.rb`.
|
|
6
|
+
2. Add a matching entry in `CHANGELOG.md` (repo root).
|
|
7
|
+
|
|
8
|
+
## Build the gem (optional local check)
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd clients/ruby
|
|
12
|
+
bundle exec rake build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Build + push from repo root (npm script)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm run release:gem
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Publish (if applicable)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd clients/ruby
|
|
25
|
+
gem push pkg/reputable-<version>.gem
|
|
26
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reputable
|
|
4
|
+
# Simple HTML blocked page renderer for Rack/Rails usage.
|
|
5
|
+
module BlockedPage
|
|
6
|
+
class << self
|
|
7
|
+
def response(
|
|
8
|
+
site_name: nil,
|
|
9
|
+
support_email: nil,
|
|
10
|
+
support_url: nil,
|
|
11
|
+
client_ip: nil,
|
|
12
|
+
heading: "Access blocked",
|
|
13
|
+
message: nil,
|
|
14
|
+
status: 403,
|
|
15
|
+
show_ip: true
|
|
16
|
+
)
|
|
17
|
+
html = build_html(
|
|
18
|
+
site_name: site_name,
|
|
19
|
+
support_email: support_email,
|
|
20
|
+
support_url: support_url,
|
|
21
|
+
client_ip: client_ip,
|
|
22
|
+
heading: heading,
|
|
23
|
+
message: message,
|
|
24
|
+
show_ip: show_ip
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
headers = {
|
|
28
|
+
"Content-Type" => "text/html; charset=utf-8",
|
|
29
|
+
"Cache-Control" => "no-store"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[status, headers, [html]]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def html(**options)
|
|
36
|
+
build_html(**options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_html(site_name:, support_email:, support_url:, client_ip:, heading:, message:, show_ip:)
|
|
42
|
+
display_name = escape_html(site_name || "this site")
|
|
43
|
+
safe_support_email = support_email ? escape_html(support_email) : nil
|
|
44
|
+
safe_support_url = support_url ? escape_html(support_url) : nil
|
|
45
|
+
safe_heading = escape_html(heading || "Access blocked")
|
|
46
|
+
safe_message = escape_html(
|
|
47
|
+
message || "We cannot allow this request for #{display_name}. Your connection was blocked by a security policy."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
contact_line = if safe_support_email || safe_support_url
|
|
51
|
+
parts = []
|
|
52
|
+
parts << if safe_support_email
|
|
53
|
+
%(at <a href="mailto:#{safe_support_email}">#{safe_support_email}</a>)
|
|
54
|
+
else
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
parts << if safe_support_url
|
|
58
|
+
%(via <a href="#{safe_support_url}">support</a>)
|
|
59
|
+
else
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
parts.compact.join(" or ")
|
|
63
|
+
else
|
|
64
|
+
"by contacting the site owner"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ip_line = if show_ip && client_ip && !client_ip.to_s.empty?
|
|
68
|
+
%(<div class="ip-text">Your IP: #{escape_html(client_ip.to_s)}</div>)
|
|
69
|
+
else
|
|
70
|
+
""
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
<<~HTML
|
|
74
|
+
<!DOCTYPE html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="UTF-8">
|
|
78
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
79
|
+
<title>Access blocked</title>
|
|
80
|
+
<style>
|
|
81
|
+
:root {
|
|
82
|
+
--bg-color: #fafafa;
|
|
83
|
+
--card-bg: #ffffff;
|
|
84
|
+
--text-color: #333333;
|
|
85
|
+
--text-muted: #6b7280;
|
|
86
|
+
--border-color: #e5e7eb;
|
|
87
|
+
--link-color: #2563eb;
|
|
88
|
+
}
|
|
89
|
+
@media (prefers-color-scheme: dark) {
|
|
90
|
+
:root {
|
|
91
|
+
--bg-color: #0f0f0f;
|
|
92
|
+
--card-bg: #1a1a1a;
|
|
93
|
+
--text-color: #f3f4f6;
|
|
94
|
+
--text-muted: #9ca3af;
|
|
95
|
+
--border-color: #374151;
|
|
96
|
+
--link-color: #60a5fa;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
* { box-sizing: border-box; }
|
|
100
|
+
body {
|
|
101
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
|
102
|
+
background: var(--bg-color);
|
|
103
|
+
color: var(--text-color);
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
min-height: 100vh;
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 20px;
|
|
110
|
+
}
|
|
111
|
+
.card {
|
|
112
|
+
max-width: 520px;
|
|
113
|
+
width: 100%;
|
|
114
|
+
padding: 40px;
|
|
115
|
+
background: var(--card-bg);
|
|
116
|
+
border: 1px solid var(--border-color);
|
|
117
|
+
border-radius: 12px;
|
|
118
|
+
text-align: center;
|
|
119
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
120
|
+
}
|
|
121
|
+
h1 { margin: 0 0 12px; font-size: 22px; font-weight: 600; }
|
|
122
|
+
p { margin: 0 0 20px; line-height: 1.5; color: var(--text-muted); }
|
|
123
|
+
a { color: var(--link-color); text-decoration: none; }
|
|
124
|
+
a:hover { text-decoration: underline; }
|
|
125
|
+
.muted { font-size: 13px; color: var(--text-muted); }
|
|
126
|
+
.branding {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
margin-top: 24px;
|
|
132
|
+
padding-top: 20px;
|
|
133
|
+
border-top: 1px solid var(--border-color);
|
|
134
|
+
}
|
|
135
|
+
.branding-text {
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
color: var(--text-muted);
|
|
138
|
+
}
|
|
139
|
+
.ip-text {
|
|
140
|
+
margin-top: 12px;
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
color: var(--text-muted);
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<div class="card">
|
|
148
|
+
<h1>#{safe_heading}</h1>
|
|
149
|
+
<p>#{safe_message}</p>
|
|
150
|
+
<p class="muted">If you believe this is a mistake, you can reach #{display_name} #{contact_line}.</p>
|
|
151
|
+
<div class="branding">
|
|
152
|
+
<span class="branding-text">Protected by Reputable</span>
|
|
153
|
+
</div>
|
|
154
|
+
#{ip_line}
|
|
155
|
+
</div>
|
|
156
|
+
</body>
|
|
157
|
+
</html>
|
|
158
|
+
HTML
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def escape_html(text)
|
|
162
|
+
text.to_s
|
|
163
|
+
.gsub("&", "&")
|
|
164
|
+
.gsub("<", "<")
|
|
165
|
+
.gsub(">", ">")
|
|
166
|
+
.gsub('"', """)
|
|
167
|
+
.gsub("'", "'")
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -14,7 +14,8 @@ module Reputable
|
|
|
14
14
|
:default_ttls, :pool_size, :pool_timeout,
|
|
15
15
|
:connect_timeout, :read_timeout, :write_timeout,
|
|
16
16
|
:ssl_params, :trusted_proxies, :ip_header_priority,
|
|
17
|
-
:on_error, :trusted_keys, :base_url
|
|
17
|
+
:on_error, :trusted_keys, :base_url,
|
|
18
|
+
:site_name, :support_email, :support_url
|
|
18
19
|
|
|
19
20
|
# Alias for backward compatibility
|
|
20
21
|
alias_method :verification_base_url, :base_url
|
|
@@ -23,7 +24,7 @@ module Reputable
|
|
|
23
24
|
# Default TTLs in seconds (0 = forever)
|
|
24
25
|
DEFAULT_TTLS = {
|
|
25
26
|
trusted_verified: 0, # Forever
|
|
26
|
-
trusted_behavior:
|
|
27
|
+
trusted_behavior: 365 * 24 * 3600, # 1 year
|
|
27
28
|
untrusted_challenge: 7 * 24 * 3600, # 7 days
|
|
28
29
|
untrusted_block: 7 * 24 * 3600, # 7 days
|
|
29
30
|
untrusted_ignore: 7 * 24 * 3600 # 7 days
|
|
@@ -78,6 +79,9 @@ module Reputable
|
|
|
78
79
|
@trusted_keys = [ENV["REPUTABLE_SECRET_KEY"]]
|
|
79
80
|
end
|
|
80
81
|
@base_url = ENV.fetch("REPUTABLE_BASE_URL", "https://api.reputable.click")
|
|
82
|
+
@site_name = ENV["REPUTABLE_SITE_NAME"]
|
|
83
|
+
@support_email = ENV["REPUTABLE_SUPPORT_EMAIL"]
|
|
84
|
+
@support_url = ENV["REPUTABLE_SUPPORT_URL"]
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
# Alias for backward compatibility
|
data/lib/reputable/middleware.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "blocked_page"
|
|
4
|
+
|
|
3
5
|
module Reputable
|
|
4
6
|
# Rack middleware for automatic request tracking
|
|
5
7
|
#
|
|
@@ -42,12 +44,34 @@ module Reputable
|
|
|
42
44
|
@skip_if = options[:skip_if]
|
|
43
45
|
@tag_builder = options[:tag_builder]
|
|
44
46
|
@async = options.fetch(:async, true)
|
|
47
|
+
@reputation_gate = options.fetch(:reputation_gate, false)
|
|
48
|
+
@challenge_action = options.fetch(:challenge_action, :verify)
|
|
49
|
+
@block_action = options.fetch(:block_action, :blocked_page_remote)
|
|
50
|
+
@challenge_redirect_status = options.fetch(:challenge_redirect_status, 302)
|
|
51
|
+
@blocked_redirect_status = options.fetch(:blocked_redirect_status, 302)
|
|
52
|
+
@blocked_redirect_url = options[:blocked_redirect_url]
|
|
53
|
+
@verification_force_challenge = options.fetch(:verification_force_challenge, false)
|
|
54
|
+
@verification_failure_url = options[:verification_failure_url]
|
|
55
|
+
@verification_session_id = options[:verification_session_id]
|
|
56
|
+
@verified_session_keys = Array(options.fetch(:verified_session_keys, [:reputable_verified_at, :reputable_verified]))
|
|
57
|
+
@blocked_page_options = options.fetch(:blocked_page, {})
|
|
58
|
+
@blocked_page_path = options[:blocked_page_path]
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
def call(env)
|
|
48
62
|
# Check for verification return parameters and verify signature if present
|
|
49
63
|
handle_verification_return(env)
|
|
50
64
|
|
|
65
|
+
# Optional: render blocked page path directly
|
|
66
|
+
if (blocked_response = handle_blocked_page_request(env))
|
|
67
|
+
return blocked_response
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Optional: enforce reputation gate before app
|
|
71
|
+
if (gate_response = safe_reputation_gate(env))
|
|
72
|
+
return gate_response
|
|
73
|
+
end
|
|
74
|
+
|
|
51
75
|
# ALWAYS process the request first - tracking must never block
|
|
52
76
|
status, headers, response = @app.call(env)
|
|
53
77
|
|
|
@@ -60,6 +84,33 @@ module Reputable
|
|
|
60
84
|
|
|
61
85
|
private
|
|
62
86
|
|
|
87
|
+
def safe_reputation_gate(env)
|
|
88
|
+
return nil unless @reputation_gate
|
|
89
|
+
return nil unless Reputable.enabled?
|
|
90
|
+
return nil if skip_request?(env)
|
|
91
|
+
|
|
92
|
+
enforce_reputation_gate(env)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
Reputable.logger&.debug("Reputable reputation gate: #{e.class} - #{e.message}")
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def enforce_reputation_gate(env)
|
|
99
|
+
ip = extract_ip(env)
|
|
100
|
+
status = Reputable::Reputation.lookup_ip(ip)
|
|
101
|
+
return nil if status.nil?
|
|
102
|
+
|
|
103
|
+
case status
|
|
104
|
+
when "untrusted_block"
|
|
105
|
+
handle_block_action(env, ip)
|
|
106
|
+
when "untrusted_challenge"
|
|
107
|
+
return nil if verified_session?(env)
|
|
108
|
+
handle_challenge_action(env)
|
|
109
|
+
else
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
63
114
|
def safe_track_request(env)
|
|
64
115
|
# Skip if disabled globally
|
|
65
116
|
return unless Reputable.enabled?
|
|
@@ -93,6 +144,145 @@ module Reputable
|
|
|
93
144
|
Reputable.logger&.debug("Reputable verification error: #{e.message}")
|
|
94
145
|
end
|
|
95
146
|
|
|
147
|
+
def handle_blocked_page_request(env)
|
|
148
|
+
return nil unless @blocked_page_path
|
|
149
|
+
|
|
150
|
+
path = env["PATH_INFO"] || "/"
|
|
151
|
+
return nil unless path == @blocked_page_path
|
|
152
|
+
|
|
153
|
+
ip = extract_ip(env)
|
|
154
|
+
build_blocked_page_response(ip)
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
Reputable.logger&.debug("Reputable blocked page: #{e.class} - #{e.message}")
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def handle_block_action(env, ip)
|
|
161
|
+
case @block_action
|
|
162
|
+
when :blocked_page
|
|
163
|
+
build_blocked_page_response(ip)
|
|
164
|
+
when :blocked_page_remote
|
|
165
|
+
url = resolve_blocked_redirect_url(env) || Reputable.blocked_page_url
|
|
166
|
+
redirect_response(url, @blocked_redirect_status)
|
|
167
|
+
when :forbidden
|
|
168
|
+
[403, { "Content-Type" => "text/plain; charset=utf-8" }, ["Forbidden"]]
|
|
169
|
+
when Proc
|
|
170
|
+
@block_action.call(env, ip)
|
|
171
|
+
else
|
|
172
|
+
build_blocked_page_response(ip)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_challenge_action(env)
|
|
177
|
+
case @challenge_action
|
|
178
|
+
when :verify
|
|
179
|
+
keys = Reputable.configuration.trusted_keys
|
|
180
|
+
return nil if keys.nil? || keys.empty?
|
|
181
|
+
|
|
182
|
+
request = Rack::Request.new(env)
|
|
183
|
+
return_url = request.url
|
|
184
|
+
failure_url = build_verification_failure_url(request)
|
|
185
|
+
session_id = resolve_session_id(env)
|
|
186
|
+
|
|
187
|
+
redirect_url = Reputable.verification_url(
|
|
188
|
+
return_url: return_url,
|
|
189
|
+
failure_url: failure_url,
|
|
190
|
+
session_id: session_id,
|
|
191
|
+
force_challenge: @verification_force_challenge
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return nil if redirect_url == return_url
|
|
195
|
+
|
|
196
|
+
redirect_response(redirect_url, @challenge_redirect_status)
|
|
197
|
+
when Proc
|
|
198
|
+
@challenge_action.call(env)
|
|
199
|
+
else
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def build_verification_failure_url(request)
|
|
205
|
+
if @verification_failure_url.respond_to?(:call)
|
|
206
|
+
@verification_failure_url.call(request)
|
|
207
|
+
elsif @verification_failure_url
|
|
208
|
+
@verification_failure_url
|
|
209
|
+
elsif @blocked_page_path
|
|
210
|
+
base = request.base_url
|
|
211
|
+
path = @blocked_page_path.start_with?("/") ? @blocked_page_path : "/#{@blocked_page_path}"
|
|
212
|
+
"#{base}#{path}"
|
|
213
|
+
end
|
|
214
|
+
rescue StandardError
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def resolve_session_id(env)
|
|
219
|
+
return @verification_session_id.call(env) if @verification_session_id.respond_to?(:call)
|
|
220
|
+
return @verification_session_id if @verification_session_id.is_a?(String)
|
|
221
|
+
|
|
222
|
+
if @verification_session_id.is_a?(Symbol)
|
|
223
|
+
session = env["rack.session"]
|
|
224
|
+
return session[@verification_session_id] if session
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
session = env["rack.session"]
|
|
228
|
+
return nil unless session
|
|
229
|
+
|
|
230
|
+
return session.id if session.respond_to?(:id) && session.id
|
|
231
|
+
|
|
232
|
+
options = env["rack.session.options"]
|
|
233
|
+
return options[:id] if options.is_a?(Hash) && options[:id]
|
|
234
|
+
|
|
235
|
+
session[:session_id] || session["session_id"]
|
|
236
|
+
rescue StandardError
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def verified_session?(env)
|
|
241
|
+
return true if env["reputable.verified"]
|
|
242
|
+
|
|
243
|
+
session = env["rack.session"]
|
|
244
|
+
return false unless session
|
|
245
|
+
|
|
246
|
+
@verified_session_keys.any? do |key|
|
|
247
|
+
session[key] || session[key.to_s]
|
|
248
|
+
end
|
|
249
|
+
rescue StandardError
|
|
250
|
+
false
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def build_blocked_page_response(client_ip)
|
|
254
|
+
options = blocked_page_options.merge(client_ip: client_ip)
|
|
255
|
+
Reputable::BlockedPage.response(**options)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def blocked_page_options
|
|
259
|
+
config = Reputable.configuration
|
|
260
|
+
defaults = {
|
|
261
|
+
site_name: config.site_name,
|
|
262
|
+
support_email: config.support_email,
|
|
263
|
+
support_url: config.support_url
|
|
264
|
+
}
|
|
265
|
+
defaults.merge(@blocked_page_options)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def redirect_response(url, status)
|
|
269
|
+
headers = {
|
|
270
|
+
"Location" => url,
|
|
271
|
+
"Content-Type" => "text/html; charset=utf-8",
|
|
272
|
+
"Cache-Control" => "no-store"
|
|
273
|
+
}
|
|
274
|
+
[status, headers, ["Redirecting to verification..."]]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def resolve_blocked_redirect_url(env)
|
|
278
|
+
return @blocked_redirect_url.call(env) if @blocked_redirect_url.respond_to?(:call)
|
|
279
|
+
return @blocked_redirect_url if @blocked_redirect_url.is_a?(String)
|
|
280
|
+
|
|
281
|
+
nil
|
|
282
|
+
rescue StandardError
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
|
|
96
286
|
def skip_request?(env)
|
|
97
287
|
return true if @skip_if&.call(env)
|
|
98
288
|
|
data/lib/reputable/rails.rb
CHANGED
|
@@ -25,11 +25,13 @@ module Reputable
|
|
|
25
25
|
)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Trust the current user's IP (
|
|
29
|
-
def trust_current_ip(reason:, **metadata)
|
|
28
|
+
# Trust the current user's IP (behavioral by default, verified optional)
|
|
29
|
+
def trust_current_ip(reason:, status: :trusted_behavior, ttl: nil, **metadata)
|
|
30
30
|
Reputable::Reputation.trust_ip(
|
|
31
31
|
request.remote_ip,
|
|
32
32
|
reason: reason,
|
|
33
|
+
status: status,
|
|
34
|
+
ttl: ttl,
|
|
33
35
|
**metadata
|
|
34
36
|
)
|
|
35
37
|
end
|
|
@@ -90,6 +92,30 @@ module Reputable
|
|
|
90
92
|
# Verification redirect helpers
|
|
91
93
|
# ========================================
|
|
92
94
|
|
|
95
|
+
# Render the standard blocked page
|
|
96
|
+
def render_reputable_blocked_page(
|
|
97
|
+
status: 403,
|
|
98
|
+
site_name: nil,
|
|
99
|
+
support_email: nil,
|
|
100
|
+
support_url: nil,
|
|
101
|
+
heading: "Access blocked",
|
|
102
|
+
message: nil,
|
|
103
|
+
show_ip: true
|
|
104
|
+
)
|
|
105
|
+
config = Reputable.configuration
|
|
106
|
+
html = Reputable::BlockedPage.html(
|
|
107
|
+
site_name: site_name || config.site_name,
|
|
108
|
+
support_email: support_email || config.support_email,
|
|
109
|
+
support_url: support_url || config.support_url,
|
|
110
|
+
client_ip: request.remote_ip,
|
|
111
|
+
heading: heading,
|
|
112
|
+
message: message,
|
|
113
|
+
show_ip: show_ip
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
render html: html.html_safe, status: status, content_type: "text/html"
|
|
117
|
+
end
|
|
118
|
+
|
|
93
119
|
# Check if the current session has already passed verification
|
|
94
120
|
# @param session_key [Symbol]
|
|
95
121
|
# @return [Boolean]
|
|
@@ -143,6 +169,47 @@ module Reputable
|
|
|
143
169
|
) and return
|
|
144
170
|
end
|
|
145
171
|
|
|
172
|
+
# Enforce reputation-based access control.
|
|
173
|
+
# - If IP is blocked, render a blocked page.
|
|
174
|
+
# - If IP is challenged, run verification flow.
|
|
175
|
+
def require_reputable_reputation!(
|
|
176
|
+
return_url: request.original_url,
|
|
177
|
+
failure_url: nil,
|
|
178
|
+
session_id: session.id,
|
|
179
|
+
force_challenge: false,
|
|
180
|
+
session_key: :reputable_verified_at,
|
|
181
|
+
blocked_status: 403,
|
|
182
|
+
blocked_site_name: nil,
|
|
183
|
+
blocked_support_email: nil,
|
|
184
|
+
blocked_support_url: nil,
|
|
185
|
+
blocked_heading: "Access blocked",
|
|
186
|
+
blocked_message: nil,
|
|
187
|
+
blocked_show_ip: true
|
|
188
|
+
)
|
|
189
|
+
if current_ip_blocked?
|
|
190
|
+
render_reputable_blocked_page(
|
|
191
|
+
status: blocked_status,
|
|
192
|
+
site_name: blocked_site_name,
|
|
193
|
+
support_email: blocked_support_email,
|
|
194
|
+
support_url: blocked_support_url,
|
|
195
|
+
heading: blocked_heading,
|
|
196
|
+
message: blocked_message,
|
|
197
|
+
show_ip: blocked_show_ip
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return unless current_ip_challenged?
|
|
203
|
+
|
|
204
|
+
require_reputable_verification!(
|
|
205
|
+
return_url: return_url,
|
|
206
|
+
failure_url: failure_url,
|
|
207
|
+
session_id: session_id,
|
|
208
|
+
force_challenge: force_challenge,
|
|
209
|
+
session_key: session_key
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
146
213
|
private
|
|
147
214
|
|
|
148
215
|
def reputable_verification_url(return_url:, failure_url:, session_id:, force_challenge:)
|
data/lib/reputable/reputation.rb
CHANGED
|
@@ -62,14 +62,16 @@ module Reputable
|
|
|
62
62
|
false
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# Convenience method: Trust an IP (
|
|
66
|
-
|
|
65
|
+
# Convenience method: Trust an IP (behavioral by default)
|
|
66
|
+
# @param status [Symbol] :trusted_behavior or :trusted_verified
|
|
67
|
+
# @param ttl [Integer, nil] TTL in seconds (0 = forever, nil = use default)
|
|
68
|
+
def trust_ip(ip, reason: "manual_trust", status: :trusted_behavior, ttl: nil, **metadata)
|
|
67
69
|
apply(
|
|
68
70
|
entity_type: :ip,
|
|
69
71
|
entity_id: ip,
|
|
70
|
-
status:
|
|
72
|
+
status: status,
|
|
71
73
|
reason: reason,
|
|
72
|
-
ttl:
|
|
74
|
+
ttl: ttl,
|
|
73
75
|
metadata: metadata
|
|
74
76
|
)
|
|
75
77
|
end
|
data/lib/reputable/version.rb
CHANGED
data/lib/reputable.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "reputable/configuration"
|
|
|
7
7
|
require_relative "reputable/connection"
|
|
8
8
|
require_relative "reputable/tracker"
|
|
9
9
|
require_relative "reputable/reputation"
|
|
10
|
+
require_relative "reputable/blocked_page"
|
|
10
11
|
require_relative "reputable/middleware"
|
|
11
12
|
|
|
12
13
|
# Optional Rails integration (only load if Rails is defined)
|
|
@@ -33,8 +34,8 @@ end
|
|
|
33
34
|
# path: "/products/123"
|
|
34
35
|
# )
|
|
35
36
|
#
|
|
36
|
-
# @example Apply reputation after payment
|
|
37
|
-
# Reputable.trust_ip("203.0.113.45", reason: "payment_completed")
|
|
37
|
+
# @example Apply verified reputation after payment
|
|
38
|
+
# Reputable.trust_ip("203.0.113.45", reason: "payment_completed", status: :trusted_verified, ttl: 0)
|
|
38
39
|
#
|
|
39
40
|
# @example Disable completely via ENV
|
|
40
41
|
# # In your environment: REPUTABLE_ENABLED=false
|
|
@@ -88,8 +89,8 @@ module Reputable
|
|
|
88
89
|
Reputation.apply(entity_type: entity_type, entity_id: entity_id, status: status, **options)
|
|
89
90
|
end
|
|
90
91
|
|
|
91
|
-
def trust_ip(ip, reason: "manual_trust", **metadata)
|
|
92
|
-
Reputation.trust_ip(ip, reason: reason, **metadata)
|
|
92
|
+
def trust_ip(ip, reason: "manual_trust", status: :trusted_behavior, ttl: nil, **metadata)
|
|
93
|
+
Reputation.trust_ip(ip, reason: reason, status: status, ttl: ttl, **metadata)
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
def block_ip(ip, reason: "manual_block", **metadata)
|
|
@@ -171,6 +172,14 @@ module Reputable
|
|
|
171
172
|
"#{verify_url}?token=#{token}"
|
|
172
173
|
end
|
|
173
174
|
|
|
175
|
+
# Build the hosted blocked page URL
|
|
176
|
+
# @return [String]
|
|
177
|
+
def blocked_page_url
|
|
178
|
+
base_url = configuration.base_url
|
|
179
|
+
base_url = base_url.chomp("/")
|
|
180
|
+
"#{base_url}/_reputable/verify/blocked"
|
|
181
|
+
end
|
|
182
|
+
|
|
174
183
|
# Verify the signature of a redirect return
|
|
175
184
|
# @param params [Hash] Request query parameters
|
|
176
185
|
# @return [Boolean] true if valid logic and passed signature check
|
data/reputable.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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
|
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.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Reputable
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -126,8 +126,10 @@ files:
|
|
|
126
126
|
- Gemfile
|
|
127
127
|
- Gemfile.lock
|
|
128
128
|
- README.md
|
|
129
|
+
- RELEASING.md
|
|
129
130
|
- Rakefile
|
|
130
131
|
- lib/reputable.rb
|
|
132
|
+
- lib/reputable/blocked_page.rb
|
|
131
133
|
- lib/reputable/configuration.rb
|
|
132
134
|
- lib/reputable/connection.rb
|
|
133
135
|
- lib/reputable/middleware.rb
|
|
@@ -135,6 +137,7 @@ files:
|
|
|
135
137
|
- lib/reputable/reputation.rb
|
|
136
138
|
- lib/reputable/tracker.rb
|
|
137
139
|
- lib/reputable/version.rb
|
|
140
|
+
- reputable.gemspec
|
|
138
141
|
homepage: https://github.com/reputable-click/reputable-rb
|
|
139
142
|
licenses:
|
|
140
143
|
- MIT
|