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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 022cbf56ba79954bcae6450e3fe7d31791498a1bbde2ee3d6add895124cde28b
|
|
4
|
+
data.tar.gz: 6f3cddf2cbc0f69241d7affd69d9322a521a03eb0727a6c7e1695d6fe3194106
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6a03a6f38827b0eb79ce4b8db2ed57640614822caf5f31165020da33324eaf39a6ded64a9d5a19e5b557a03e3b2e06348b02dcd78472a407f680cd18b4c4086d
|
|
7
|
+
data.tar.gz: 0a52fc4e6a6fe20b799841e2dd4e475afc0a8ff4a20a48240812b343538e8f2a3b0e11863b6a2dea89748b9f8f8931f45889142d5d7f690144223958cde890b6
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Reputable Ruby Client
|
|
2
|
+
|
|
3
|
+
Ruby gem for integrating with Reputable - bot detection and reputation scoring for Rails applications.
|
|
4
|
+
|
|
5
|
+
**Resilience First**: This gem is designed to never break your application. All operations fail silently with safe defaults.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'reputable'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Rails Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Create Initializer
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
# config/initializers/reputable.rb
|
|
27
|
+
Reputable.configure do |config|
|
|
28
|
+
# Required: Redis/Dragonfly URL (TLS supported via rediss://)
|
|
29
|
+
config.redis_url = ENV['REPUTABLE_REDIS_URL']
|
|
30
|
+
|
|
31
|
+
# Required: Your tenant ID
|
|
32
|
+
config.tenant_id = ENV['REPUTABLE_TENANT_ID']
|
|
33
|
+
|
|
34
|
+
# Optional: Enable logging (logs at debug level)
|
|
35
|
+
Reputable.logger = Rails.logger
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Add Middleware
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# config/application.rb
|
|
43
|
+
module YourApp
|
|
44
|
+
class Application < Rails::Application
|
|
45
|
+
# Add Reputable middleware (recommended: async mode, non-blocking)
|
|
46
|
+
config.middleware.use Reputable::Middleware, async: true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 3. Use Controller Helpers
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# app/controllers/application_controller.rb
|
|
55
|
+
class ApplicationController < ActionController::Base
|
|
56
|
+
include Reputable::Rails::ControllerHelpers
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# app/controllers/payments_controller.rb
|
|
60
|
+
class PaymentsController < ApplicationController
|
|
61
|
+
def create
|
|
62
|
+
@order = Order.create!(order_params)
|
|
63
|
+
|
|
64
|
+
if @order.payment_successful?
|
|
65
|
+
# Trust this IP after successful payment (forever)
|
|
66
|
+
trust_current_ip(reason: 'payment_completed', order_id: @order.id)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's it! Reputable will now track all requests and you can query/apply reputations.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Configuration Reference
|
|
77
|
+
|
|
78
|
+
### Environment Variables
|
|
79
|
+
|
|
80
|
+
All configuration can be set via environment variables:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Required
|
|
84
|
+
REPUTABLE_REDIS_URL=rediss://user:password@your-dragonfly.example.com:6379
|
|
85
|
+
REPUTABLE_TENANT_ID=your-tenant-id
|
|
86
|
+
|
|
87
|
+
# Optional: Disable entirely (useful for test environments)
|
|
88
|
+
REPUTABLE_ENABLED=false
|
|
89
|
+
|
|
90
|
+
# Optional: Connection tuning
|
|
91
|
+
REPUTABLE_CONNECT_TIMEOUT=0.5 # Redis connect timeout (seconds)
|
|
92
|
+
REPUTABLE_READ_TIMEOUT=0.5 # Redis read timeout (seconds)
|
|
93
|
+
REPUTABLE_WRITE_TIMEOUT=0.5 # Redis write timeout (seconds)
|
|
94
|
+
REPUTABLE_POOL_SIZE=5 # Connection pool size
|
|
95
|
+
REPUTABLE_POOL_TIMEOUT=1.0 # Pool checkout timeout (seconds)
|
|
96
|
+
|
|
97
|
+
# Optional: SSL (for custom certificates)
|
|
98
|
+
REPUTABLE_SSL_VERIFY=false # Disable SSL verification (NOT recommended for production)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Full Configuration Options
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Reputable.configure do |config|
|
|
105
|
+
# Redis connection (supports redis:// and rediss:// for TLS)
|
|
106
|
+
config.redis_url = ENV['REPUTABLE_REDIS_URL']
|
|
107
|
+
|
|
108
|
+
# Your tenant identifier
|
|
109
|
+
config.tenant_id = ENV['REPUTABLE_TENANT_ID']
|
|
110
|
+
|
|
111
|
+
# Connection pool settings
|
|
112
|
+
config.pool_size = 5 # Number of Redis connections
|
|
113
|
+
config.pool_timeout = 1.0 # Max wait for connection (seconds)
|
|
114
|
+
|
|
115
|
+
# Redis operation timeouts
|
|
116
|
+
config.connect_timeout = 0.5 # Connection timeout
|
|
117
|
+
config.read_timeout = 0.5 # Read timeout
|
|
118
|
+
config.write_timeout = 0.5 # Write timeout
|
|
119
|
+
|
|
120
|
+
# Custom SSL parameters (for self-signed certs, etc.)
|
|
121
|
+
config.ssl_params = {
|
|
122
|
+
ca_file: '/path/to/ca.crt',
|
|
123
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Customize TTLs (in seconds, 0 = forever)
|
|
127
|
+
config.default_ttls = {
|
|
128
|
+
trusted_verified: 0, # Forever
|
|
129
|
+
trusted_behavior: 30 * 24 * 3600, # 30 days
|
|
130
|
+
untrusted_challenge: 7 * 24 * 3600,
|
|
131
|
+
untrusted_block: 7 * 24 * 3600,
|
|
132
|
+
untrusted_ignore: 7 * 24 * 3600
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# IP header priority (for proxy environments)
|
|
136
|
+
# Default covers Heroku, Cloudflare, AWS ALB, nginx, HAProxy
|
|
137
|
+
config.ip_header_priority = %w[
|
|
138
|
+
HTTP_CF_CONNECTING_IP
|
|
139
|
+
HTTP_X_FORWARDED_FOR
|
|
140
|
+
HTTP_X_REAL_IP
|
|
141
|
+
HTTP_TRUE_CLIENT_IP
|
|
142
|
+
REMOTE_ADDR
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# Error callback (optional)
|
|
146
|
+
config.on_error = ->(error, context) {
|
|
147
|
+
# Report to your error tracking service
|
|
148
|
+
Sentry.capture_exception(error, extra: { context: context })
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Enable logging
|
|
153
|
+
Reputable.logger = Rails.logger
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Proxy & Load Balancer Support
|
|
159
|
+
|
|
160
|
+
The gem automatically handles IP extraction in proxy environments including:
|
|
161
|
+
|
|
162
|
+
- **Heroku** - Uses `X-Forwarded-For`
|
|
163
|
+
- **Cloudflare** - Uses `CF-Connecting-IP` (highest priority by default)
|
|
164
|
+
- **AWS ALB/ELB** - Uses `X-Forwarded-For`
|
|
165
|
+
- **nginx** - Uses `X-Real-IP` or `X-Forwarded-For`
|
|
166
|
+
- **HAProxy** - Uses `X-Forwarded-For`
|
|
167
|
+
- **Google Cloud Load Balancer** - Uses `X-Forwarded-For`
|
|
168
|
+
|
|
169
|
+
### How IP Extraction Works
|
|
170
|
+
|
|
171
|
+
1. Headers are checked in priority order (configurable via `ip_header_priority`)
|
|
172
|
+
2. For `X-Forwarded-For`, we parse the leftmost **public** IP (skipping private ranges)
|
|
173
|
+
3. Private IP ranges (10.x, 172.16.x, 192.168.x, etc.) are automatically filtered
|
|
174
|
+
|
|
175
|
+
### Custom Proxy Configuration
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# If your proxy uses a non-standard header
|
|
179
|
+
config.ip_header_priority = %w[
|
|
180
|
+
HTTP_X_MY_CUSTOM_IP
|
|
181
|
+
HTTP_X_FORWARDED_FOR
|
|
182
|
+
REMOTE_ADDR
|
|
183
|
+
]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## TLS/SSL Support
|
|
189
|
+
|
|
190
|
+
The gem fully supports TLS connections to Redis/Dragonfly:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# Use rediss:// scheme for TLS
|
|
194
|
+
config.redis_url = "rediss://user:password@your-server:6379"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### SSL Error Handling
|
|
198
|
+
|
|
199
|
+
All SSL errors are caught and logged, never breaking your application:
|
|
200
|
+
|
|
201
|
+
- Certificate verification failures
|
|
202
|
+
- Handshake timeouts
|
|
203
|
+
- Protocol errors
|
|
204
|
+
- Self-signed certificate issues
|
|
205
|
+
|
|
206
|
+
### Custom Certificates
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
config.ssl_params = {
|
|
210
|
+
ca_file: '/path/to/custom-ca.crt',
|
|
211
|
+
cert: OpenSSL::X509::Certificate.new(File.read('/path/to/client.crt')),
|
|
212
|
+
key: OpenSSL::PKey::RSA.new(File.read('/path/to/client.key'))
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Disable SSL Verification (Development Only)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# NOT recommended for production
|
|
220
|
+
REPUTABLE_SSL_VERIFY=false
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Middleware Configuration
|
|
226
|
+
|
|
227
|
+
### Basic Usage
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
# config/application.rb
|
|
231
|
+
config.middleware.use Reputable::Middleware
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### With Options
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
config.middleware.use Reputable::Middleware,
|
|
238
|
+
# Skip certain paths (health checks, assets, etc.)
|
|
239
|
+
skip_paths: ['/health', '/healthz', '/assets', '/packs'],
|
|
240
|
+
|
|
241
|
+
# Skip by file extension
|
|
242
|
+
skip_extensions: ['.js', '.css', '.png', '.jpg', '.svg'],
|
|
243
|
+
|
|
244
|
+
# Custom skip logic
|
|
245
|
+
skip_if: ->(env) {
|
|
246
|
+
env['HTTP_X_INTERNAL'] == 'true' ||
|
|
247
|
+
env['PATH_INFO'].start_with?('/admin')
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
# Add custom tags based on request
|
|
251
|
+
tag_builder: ->(env) {
|
|
252
|
+
tags = []
|
|
253
|
+
tags << "env:#{Rails.env}"
|
|
254
|
+
tags << "mobile:true" if env['HTTP_USER_AGENT']&.include?('Mobile')
|
|
255
|
+
tags
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
# Async mode (default: true) - tracking runs in background thread
|
|
259
|
+
async: true
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Default Skipped Paths
|
|
263
|
+
|
|
264
|
+
The middleware automatically skips:
|
|
265
|
+
- `/health`, `/healthz`, `/ready`, `/readyz`, `/live`, `/livez`
|
|
266
|
+
- `/metrics`, `/favicon.ico`
|
|
267
|
+
- Static assets (`.js`, `.css`, `.png`, `.jpg`, `.svg`, `.woff`, etc.)
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Controller Helpers (Rails)
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class ApplicationController < ActionController::Base
|
|
275
|
+
include Reputable::Rails::ControllerHelpers
|
|
276
|
+
end
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Available Methods
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
# Track current request manually (if not using middleware)
|
|
283
|
+
track_reputable_request(tags: ['custom:tag'])
|
|
284
|
+
|
|
285
|
+
# Trust methods (after successful actions)
|
|
286
|
+
trust_current_ip(reason: 'payment_completed', order_id: '123')
|
|
287
|
+
trust_current_user(reason: 'email_verified')
|
|
288
|
+
trust_current_session(reason: 'captcha_passed')
|
|
289
|
+
|
|
290
|
+
# Challenge/Block methods
|
|
291
|
+
challenge_current_ip(reason: 'suspicious_activity')
|
|
292
|
+
block_current_ip(reason: 'abuse_detected')
|
|
293
|
+
|
|
294
|
+
# Lookup methods
|
|
295
|
+
if current_ip_trusted?
|
|
296
|
+
# Skip CAPTCHA, higher rate limits
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
if current_ip_blocked?
|
|
300
|
+
render status: 403
|
|
301
|
+
return
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Get full reputation data
|
|
305
|
+
rep = current_ip_reputation
|
|
306
|
+
# => { status: 'trusted_verified', reason: 'payment', ... }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Manual API Usage
|
|
312
|
+
|
|
313
|
+
### Request Tracking
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
# Synchronous (blocks until complete)
|
|
317
|
+
Reputable.track_request(
|
|
318
|
+
ip: request.ip,
|
|
319
|
+
path: request.path,
|
|
320
|
+
query: request.query_string,
|
|
321
|
+
method: request.request_method,
|
|
322
|
+
session_id: session.id,
|
|
323
|
+
session_present: true,
|
|
324
|
+
user_agent: request.user_agent,
|
|
325
|
+
referer: request.referer,
|
|
326
|
+
tags: ['ctx:page:product']
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Asynchronous (fire-and-forget, recommended)
|
|
330
|
+
Reputable.track_request_async(
|
|
331
|
+
ip: request.ip,
|
|
332
|
+
path: request.path
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Reputation Management
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# Trust IP forever (after payment, verification, etc.)
|
|
340
|
+
Reputable.trust_ip(request.ip, reason: 'payment_completed', order_id: order.id)
|
|
341
|
+
|
|
342
|
+
# Trust a user
|
|
343
|
+
Reputable.trust_user(current_user.id, reason: 'email_verified')
|
|
344
|
+
|
|
345
|
+
# Trust a session (default: 24 hour TTL)
|
|
346
|
+
Reputable.trust_session(session.id, reason: 'captcha_passed')
|
|
347
|
+
|
|
348
|
+
# Challenge (require CAPTCHA, etc.)
|
|
349
|
+
Reputable.challenge_ip(request.ip, reason: 'unusual_activity')
|
|
350
|
+
|
|
351
|
+
# Block (with custom TTL)
|
|
352
|
+
Reputable.block_ip(request.ip, reason: 'abuse', ttl: 7.days.to_i)
|
|
353
|
+
|
|
354
|
+
# Ignore in analytics (internal monitoring, etc.)
|
|
355
|
+
Reputable.ignore_ip(request.ip, reason: 'internal_monitoring')
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Reputation Lookup (O(1) Redis)
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
# Quick boolean checks
|
|
362
|
+
Reputable.trusted_ip?(request.ip) # => true/false
|
|
363
|
+
Reputable.blocked_ip?(request.ip) # => true/false
|
|
364
|
+
Reputable.challenged_ip?(request.ip) # => true/false
|
|
365
|
+
|
|
366
|
+
# Get status string
|
|
367
|
+
Reputable.lookup_ip(request.ip)
|
|
368
|
+
# => "trusted_verified" or "untrusted_block" or nil
|
|
369
|
+
|
|
370
|
+
# Full lookup with metadata
|
|
371
|
+
Reputable.lookup_reputation(:ip, request.ip)
|
|
372
|
+
# => { status: "trusted_verified", reason: "payment_completed",
|
|
373
|
+
# source: "app_server", updated_at: 1703123456789,
|
|
374
|
+
# expires_at: 0, metadata: { order_id: "123" } }
|
|
375
|
+
|
|
376
|
+
# User/Session lookups
|
|
377
|
+
Reputable.trusted_user?(current_user.id)
|
|
378
|
+
Reputable.trusted_session?(session.id)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Resilience & Failsafe Features
|
|
384
|
+
|
|
385
|
+
### Never Breaks Your App
|
|
386
|
+
|
|
387
|
+
The gem is designed with resilience as the top priority:
|
|
388
|
+
|
|
389
|
+
1. **All operations fail silently** - Returns `false`/`nil` on any error
|
|
390
|
+
2. **No exceptions propagate** - Everything is wrapped in rescue blocks
|
|
391
|
+
3. **Circuit breaker** - After 5 failures, stops trying for 30 seconds
|
|
392
|
+
|
|
393
|
+
### Disable via Environment
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Completely disable (useful for test/CI environments)
|
|
397
|
+
REPUTABLE_ENABLED=false
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
# Check in code
|
|
402
|
+
if Reputable.enabled?
|
|
403
|
+
# Only when enabled
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Safe Return Values
|
|
408
|
+
|
|
409
|
+
| Operation | Returns on Failure |
|
|
410
|
+
|-----------|-------------------|
|
|
411
|
+
| `track_request` | `false` |
|
|
412
|
+
| `trust_ip`, `block_ip`, etc. | `false` |
|
|
413
|
+
| `lookup_ip`, `lookup_reputation` | `nil` |
|
|
414
|
+
| `trusted_ip?`, `blocked_ip?` | `false` |
|
|
415
|
+
| Middleware tracking | silently skipped |
|
|
416
|
+
|
|
417
|
+
### Circuit Breaker
|
|
418
|
+
|
|
419
|
+
- Opens after 5 consecutive failures
|
|
420
|
+
- While open, returns defaults immediately (no Redis calls)
|
|
421
|
+
- Resets after 30 seconds
|
|
422
|
+
|
|
423
|
+
### Error Callback
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
config.on_error = ->(error, context) {
|
|
427
|
+
# Log to your error service
|
|
428
|
+
Rails.logger.warn("Reputable error: #{error.class} in #{context}")
|
|
429
|
+
Sentry.capture_exception(error)
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Tags for Classification
|
|
436
|
+
|
|
437
|
+
Use tags to classify requests for behavioral analysis:
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
# Page context
|
|
441
|
+
tags: ['ctx:page:product']
|
|
442
|
+
tags: ['ctx:page:checkout']
|
|
443
|
+
tags: ['ctx:page:cart']
|
|
444
|
+
tags: ['ctx:page:login']
|
|
445
|
+
|
|
446
|
+
# Traffic source
|
|
447
|
+
tags: ['trust:channel:email']
|
|
448
|
+
tags: ['trust:channel:ad']
|
|
449
|
+
tags: ['trust:channel:organic']
|
|
450
|
+
|
|
451
|
+
# Trust signals (from verified actions)
|
|
452
|
+
tags: ['trust:financial:payment']
|
|
453
|
+
tags: ['trust:auth:login']
|
|
454
|
+
tags: ['trust:identity:verified']
|
|
455
|
+
tags: ['trust:interactive:captcha']
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Testing
|
|
461
|
+
|
|
462
|
+
### Disable in Test Environment
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
# config/environments/test.rb
|
|
466
|
+
ENV['REPUTABLE_ENABLED'] = 'false'
|
|
467
|
+
|
|
468
|
+
# Or in spec_helper.rb
|
|
469
|
+
RSpec.configure do |config|
|
|
470
|
+
config.before(:suite) do
|
|
471
|
+
ENV['REPUTABLE_ENABLED'] = 'false'
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Mock Lookups
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
# In tests, lookups return nil when disabled
|
|
480
|
+
expect(Reputable.trusted_ip?('1.2.3.4')).to eq(false)
|
|
481
|
+
expect(Reputable.lookup_ip('1.2.3.4')).to be_nil
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## How It Works
|
|
487
|
+
|
|
488
|
+
1. **Request Tracking**: Your Rails app pushes request data to Redis buffers
|
|
489
|
+
2. **Async Processing**: Reputable API processes buffers asynchronously
|
|
490
|
+
3. **Behavioral Analysis**: Requests go through classification and pattern analysis
|
|
491
|
+
4. **Reputation Storage**: Scores stored in Redis for O(1) lookups
|
|
492
|
+
|
|
493
|
+
### What's Available from Rails
|
|
494
|
+
|
|
495
|
+
- IP reputation and history
|
|
496
|
+
- Session tracking
|
|
497
|
+
- Request classification
|
|
498
|
+
- UA churn detection
|
|
499
|
+
- Cross-request pattern analysis
|
|
500
|
+
- Manual reputation overrides
|
|
501
|
+
|
|
502
|
+
### What Requires Edge Deployment
|
|
503
|
+
|
|
504
|
+
- JA4/JA3 TLS fingerprints
|
|
505
|
+
- HAProxy timing analysis
|
|
506
|
+
- Geo-latency heuristics
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## License
|
|
511
|
+
|
|
512
|
+
MIT
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "ipaddr"
|
|
5
|
+
|
|
6
|
+
module Reputable
|
|
7
|
+
# Configuration class for Reputable client
|
|
8
|
+
#
|
|
9
|
+
# Supports TLS connections with automatic SSL error handling.
|
|
10
|
+
# All SSL/connection errors are caught and logged, never breaking your app.
|
|
11
|
+
class Configuration
|
|
12
|
+
attr_accessor :redis_url, :redis_options, :tenant_id, :buffer_prefix,
|
|
13
|
+
:request_buffer_key, :reputation_buffer_key,
|
|
14
|
+
:default_ttls, :pool_size, :pool_timeout,
|
|
15
|
+
:connect_timeout, :read_timeout, :write_timeout,
|
|
16
|
+
:ssl_params, :trusted_proxies, :ip_header_priority,
|
|
17
|
+
:on_error
|
|
18
|
+
|
|
19
|
+
# Default TTLs in seconds (0 = forever)
|
|
20
|
+
DEFAULT_TTLS = {
|
|
21
|
+
trusted_verified: 0, # Forever
|
|
22
|
+
trusted_behavior: 30 * 24 * 3600, # 30 days
|
|
23
|
+
untrusted_challenge: 7 * 24 * 3600, # 7 days
|
|
24
|
+
untrusted_block: 7 * 24 * 3600, # 7 days
|
|
25
|
+
untrusted_ignore: 7 * 24 * 3600 # 7 days
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Default IP header priority (first match wins)
|
|
29
|
+
# Covers: Heroku, Cloudflare, AWS ALB/ELB, nginx, HAProxy, generic proxies
|
|
30
|
+
DEFAULT_IP_HEADERS = %w[
|
|
31
|
+
HTTP_CF_CONNECTING_IP
|
|
32
|
+
HTTP_X_FORWARDED_FOR
|
|
33
|
+
HTTP_X_REAL_IP
|
|
34
|
+
HTTP_TRUE_CLIENT_IP
|
|
35
|
+
HTTP_X_CLIENT_IP
|
|
36
|
+
HTTP_X_CLUSTER_CLIENT_IP
|
|
37
|
+
HTTP_FORWARDED
|
|
38
|
+
REMOTE_ADDR
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
# Common private/internal IP ranges to skip when parsing X-Forwarded-For
|
|
42
|
+
PRIVATE_IP_RANGES = %w[
|
|
43
|
+
127.0.0.0/8
|
|
44
|
+
10.0.0.0/8
|
|
45
|
+
172.16.0.0/12
|
|
46
|
+
192.168.0.0/16
|
|
47
|
+
::1/128
|
|
48
|
+
fc00::/7
|
|
49
|
+
fe80::/10
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
def initialize
|
|
53
|
+
@redis_url = ENV.fetch("REPUTABLE_REDIS_URL", "redis://127.0.0.1:6379")
|
|
54
|
+
@redis_options = {}
|
|
55
|
+
@tenant_id = ENV.fetch("REPUTABLE_TENANT_ID", "default")
|
|
56
|
+
@buffer_prefix = "reputable:buffer"
|
|
57
|
+
@pool_size = Integer(ENV.fetch("REPUTABLE_POOL_SIZE", "5"))
|
|
58
|
+
@pool_timeout = Float(ENV.fetch("REPUTABLE_POOL_TIMEOUT", "1.0"))
|
|
59
|
+
@connect_timeout = Float(ENV.fetch("REPUTABLE_CONNECT_TIMEOUT", "0.5"))
|
|
60
|
+
@read_timeout = Float(ENV.fetch("REPUTABLE_READ_TIMEOUT", "0.5"))
|
|
61
|
+
@write_timeout = Float(ENV.fetch("REPUTABLE_WRITE_TIMEOUT", "0.5"))
|
|
62
|
+
@default_ttls = DEFAULT_TTLS.dup
|
|
63
|
+
@ssl_params = nil # Use system defaults, or override for custom CA/certs
|
|
64
|
+
@trusted_proxies = nil # Additional trusted proxy IPs/ranges
|
|
65
|
+
@ip_header_priority = DEFAULT_IP_HEADERS.dup
|
|
66
|
+
@on_error = nil # Optional error callback: ->(error, context) { ... }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if Reputable is enabled (can be disabled via ENV)
|
|
70
|
+
def enabled?
|
|
71
|
+
env_value = ENV["REPUTABLE_ENABLED"]
|
|
72
|
+
return true if env_value.nil? # Enabled by default
|
|
73
|
+
|
|
74
|
+
!%w[0 false no off disabled].include?(env_value.to_s.downcase)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def request_buffer_key
|
|
78
|
+
@request_buffer_key || "#{buffer_prefix}:requests:#{tenant_id}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def reputation_buffer_key
|
|
82
|
+
@reputation_buffer_key || "#{buffer_prefix}:reputation:#{tenant_id}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def default_ttl_for(status)
|
|
86
|
+
status_sym = status.to_sym
|
|
87
|
+
@default_ttls[status_sym] || DEFAULT_TTLS[:untrusted_challenge]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if Redis URL uses TLS (rediss:// scheme)
|
|
91
|
+
def tls?
|
|
92
|
+
redis_url&.start_with?("rediss://")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build SSL params for Redis connection
|
|
96
|
+
# Merges custom ssl_params with sensible defaults
|
|
97
|
+
def effective_ssl_params
|
|
98
|
+
return nil unless tls?
|
|
99
|
+
|
|
100
|
+
defaults = {
|
|
101
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Allow disabling SSL verification via ENV (not recommended for production)
|
|
105
|
+
if ENV["REPUTABLE_SSL_VERIFY"] == "false"
|
|
106
|
+
defaults[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
ssl_params ? defaults.merge(ssl_params) : defaults
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Parse private IP ranges for filtering X-Forwarded-For
|
|
113
|
+
def private_ip_ranges
|
|
114
|
+
@private_ip_ranges ||= PRIVATE_IP_RANGES.map { |cidr| IPAddr.new(cidr) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Check if an IP is private/internal
|
|
118
|
+
def private_ip?(ip)
|
|
119
|
+
return true if ip.nil? || ip.empty?
|
|
120
|
+
|
|
121
|
+
addr = IPAddr.new(ip)
|
|
122
|
+
private_ip_ranges.any? { |range| range.include?(addr) }
|
|
123
|
+
rescue IPAddr::InvalidAddressError
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|