Module: OpenReceive::Server::ClientIp
- Defined in:
- lib/openreceive/server/client_ip.rb
Overview
Client-IP bucketing shared by rate limiting and attempt-row stamping. Ports the JS clientIpBucket exactly: the same input string must produce the same bucket string in both engines (mirrored tests in tests/rate-limit.test.mjs and the Ruby server tests).
- IPv4-mapped IPv6 (
::ffff:a.b.c.d) collapses to the plain IPv4, so the same client never gets two independent budgets. - IPv6 buckets to its /64 (
2001:db8:1:2::/64): privacy extensions rotate the low 64 bits freely, so per-address budgets would hand every IPv6 payer an unlimited stream of fresh budgets. - IPv4 and already-bucketed values pass through unchanged (idempotent).
- Unparsable input passes through as-is — an odd value still gets SOME consistent bucket rather than disabling the limit.
Class Method Summary collapse
-
.attributed(raw) ⇒ Object
nil/empty-safe wrapper mirroring the JS handler's extractClientIp: no attributable IP stays nil (the limiter fails open); anything else is normalized into the bucket that is both stored and counted.
- .bucket(ip) ⇒ Object
- .expand_ipv6(value) ⇒ Object
- .hextets_of(segment) ⇒ Object
Class Method Details
.attributed(raw) ⇒ Object
nil/empty-safe wrapper mirroring the JS handler's extractClientIp: no attributable IP stays nil (the limiter fails open); anything else is normalized into the bucket that is both stored and counted.
24 25 26 27 28 |
# File 'lib/openreceive/server/client_ip.rb', line 24 def attributed(raw) value = raw.to_s return nil if value.strip.empty? bucket(value) end |
.bucket(ip) ⇒ Object
30 31 32 33 34 35 36 37 38 39 |
# File 'lib/openreceive/server/client_ip.rb', line 30 def bucket(ip) value = ip.to_s.strip.downcase value = value.delete_prefix("::ffff:") if value.start_with?("::ffff:") && value.include?(".") return value unless value.include?(":") return value if value.end_with?("/64") address = value.split("%", -1).first || "" hextets = (address) return value if hextets.nil? "#{hextets.first(4).join(':')}::/64" end |
.expand_ipv6(value) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/openreceive/server/client_ip.rb', line 41 def (value) parts = value.split("::", -1) return nil if parts.length > 2 || value.empty? head = hextets_of(parts[0] || "") tail = parts.length == 2 ? hextets_of(parts[1] || "") : [] return nil if head.nil? || tail.nil? return head.length == 8 ? head : nil if parts.length == 1 missing = 8 - head.length - tail.length return nil if missing < 1 head + Array.new(missing, "0") + tail end |
.hextets_of(segment) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/openreceive/server/client_ip.rb', line 53 def hextets_of(segment) return [] if segment == "" groups = [] segment.split(":", -1).each do |group| if /\A[0-9a-f]{1,4}\z/.match?(group) groups << group.sub(/\A0+(?=.)/, "") elsif /\A\d{1,3}(\.\d{1,3}){3}\z/.match?(group) octets = group.split(".").map { |octet| Integer(octet, 10) } return nil if octets.any? { |octet| octet > 255 } # Embedded IPv4 tail expands to two hextets. groups << (((octets[0] << 8) | octets[1]).to_s(16)) groups << (((octets[2] << 8) | octets[3]).to_s(16)) else return nil end end groups end |