Class: Otto::Privacy::IPPrivacy
- Inherits:
-
Object
- Object
- Otto::Privacy::IPPrivacy
- Defined in:
- lib/otto/privacy/ip_privacy.rb
Overview
All methods return UTF-8 encoded strings for Rack compatibility. See file:docs/ipaddr-encoding-quirk.md for details on IPAddr#to_s behavior.
IP address anonymization utilities
Provides methods for masking and hashing IP addresses to enhance privacy while maintaining the ability to track sessions and analyze traffic patterns.
Class Method Summary collapse
-
.hash_ip(ip, key) ⇒ String
Hash an IP address for session correlation without storing the original.
-
.mask_forwarded_for(value, masked_ip) ⇒ String?
Redact the client identifier(s) in an RFC 7239 Forwarded header value.
-
.mask_ip(ip, octet_precision = 1) ⇒ String
Mask an IP address by zeroing out the specified number of octets/bits.
-
.private_or_localhost?(ip) ⇒ Boolean
Check if an IP address is localhost or private (RFC 1918).
-
.valid_ip?(ip) ⇒ Boolean
Check if an IP address is valid.
Class Method Details
.hash_ip(ip, key) ⇒ String
Hash an IP address for session correlation without storing the original
Uses HMAC-SHA256 with a daily-rotating key to create a consistent identifier for the same IP within a key rotation period, but different across rotations.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/otto/privacy/ip_privacy.rb', line 78 def self.hash_ip(ip, key) return nil if ip.nil? || ip.empty? raise ArgumentError, 'Key cannot be nil or empty' if key.nil? || key.empty? # Normalize IP address format before hashing normalized_ip = begin IPAddr.new(ip).to_s rescue IPAddr::InvalidAddressError => e raise ArgumentError, "Invalid IP address: #{ip} - #{e.}" end # Use HMAC-SHA256 for secure hashing with key OpenSSL::HMAC.hexdigest('SHA256', key, normalized_ip) end |
.mask_forwarded_for(value, masked_ip) ⇒ String?
Redact the client identifier(s) in an RFC 7239 Forwarded header value.
Only the for= node value is replaced with the masked IP; proto=,
host=, by= and the overall structure are preserved so downstream
scheme/host/proxy decisions (absolute URLs, redirects, secure cookies)
still work. Every for= element in the chain is redacted. IPv6 is
bracketed and quoted as the RFC requires. This is the structured-header
counterpart to the wholesale X-Forwarded-For swap.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/otto/privacy/ip_privacy.rb', line 135 def self.mask_forwarded_for(value, masked_ip) return value if value.nil? || masked_ip.nil? || masked_ip.empty? replacement = masked_ip.include?(':') ? %("[#{masked_ip}]") : masked_ip # Match `for=` only at an element/pair boundary (start, comma, or # semicolon) so a parameter merely ending in "for" is never touched. value.gsub(/(\A|[,;]\s*)for\s*=\s*("[^"]*"|[^;,]+)/i) do "#{Regexp.last_match(1)}for=#{replacement}" end end |
.mask_ip(ip, octet_precision = 1) ⇒ String
Mask an IP address by zeroing out the specified number of octets/bits
For IPv4: - octet_precision=1: Masks last octet (e.g., 192.168.1.100 → 192.168.1.0) - octet_precision=2: Masks last 2 octets (e.g., 192.168.1.100 → 192.168.0.0)
For IPv6: - octet_precision=1: Masks last 80 bits - octet_precision=2: Masks last 96 bits
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/otto/privacy/ip_privacy.rb', line 49 def self.mask_ip(ip, octet_precision = 1) return nil if ip.nil? || ip.empty? raise ArgumentError, "octet_precision must be 1 or 2, got: #{octet_precision}" unless [1, 2].include?(octet_precision) begin addr = IPAddr.new(ip) if addr.ipv4? mask_ipv4(addr, octet_precision) else mask_ipv6(addr, octet_precision) end rescue IPAddr::InvalidAddressError => e raise ArgumentError, "Invalid IP address: #{ip} - #{e.}" end end |
.private_or_localhost?(ip) ⇒ Boolean
Check if an IP address is localhost or private (RFC 1918)
Private/localhost IPs are not masked for development convenience.
113 114 115 116 117 118 119 120 |
# File 'lib/otto/privacy/ip_privacy.rb', line 113 def self.private_or_localhost?(ip) return false if ip.nil? || ip.empty? addr = IPAddr.new(ip) addr.private? || addr.loopback? rescue IPAddr::InvalidAddressError false end |
.valid_ip?(ip) ⇒ Boolean
Check if an IP address is valid
98 99 100 101 102 103 104 105 |
# File 'lib/otto/privacy/ip_privacy.rb', line 98 def self.valid_ip?(ip) return false if ip.nil? || ip.empty? IPAddr.new(ip) true rescue IPAddr::InvalidAddressError false end |