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 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
8
+ gem "rspec", "~> 3.0"
9
+ gem "rubocop", "~> 1.21"
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