Class: Otto::Privacy::GeoResolver
- Inherits:
-
Object
- Object
- Otto::Privacy::GeoResolver
- Defined in:
- lib/otto/privacy/geo_resolver.rb
Overview
Lightweight geo-location resolution for IP addresses
Provides country-level geo-location. Headers from major CDN/infrastructure providers are checked first; an optional local MaxMind-format (.mmdb) database gives an offline fallback that operates on Otto's already-MASKED IP (no external API calls, and the unmasked address never reaches the resolver).
Resolution order (first hit wins), when a privacy Config is supplied:
- App-configured trusted header (Config#geo_header), e.g. 'X-Client-Country'
- Built-in CDN/infrastructure provider headers (see below)
- Custom resolver hook (GeoResolver.custom_resolver)
- Local MMDB lookup, masked before lookup (Config#geo_db_reader)
- '**' (unknown)
Steps 1 and 2 are SKIPPED when the request's geo headers are not trusted. Every geo header is client-spoofable unless you are actually behind the CDN that sets it, so the middleware trusts them only for a request that arrived via a configured trusted proxy; otherwise resolution falls straight to the custom resolver / database.
Resolution is HONEST: when no header, custom resolver, or database resolves a country, the answer is '**' (unknown). Otto does not guess from a hardcoded IP-range table — configure a database or an edge header for real geo-location.
Supported CDN/Infrastructure Headers:
- Cloudflare: CF-IPCountry
- AWS CloudFront: CloudFront-Viewer-Country
- Fastly: Fastly-Client-IP-Country
- Akamai: X-Akamai-Edgescape (country_code=XX format)
- Azure Front Door: X-Azure-ClientIP-Country
- Vercel: X-Vercel-IP-Country
- Semi-standard: X-Geo-Country, X-Country-Code, Country-Code
Resolution flow
Request → Headers trusted?
├─ Yes → Config#geo_header set & valid? → Return country
│ └─ Provider header present & valid? → Return country
└─ (headers skipped when not trusted)
→ Custom Resolver configured?
├─ Valid → Return country
└─ Invalid/Error → Continue
→ Local MMDB reader configured?
├─ Hit → Return country
└─ Miss → Unknown ('**')
Constant Summary collapse
- UNKNOWN =
Unknown country code (not ISO 3166-1 alpha-2, intentionally distinct)
'**'- PRIMARY_COUNTRY_HEADERS =
Check CDN/infrastructure provider geo headers
Headers are checked in order of reliability and deployment frequency:
- Cloudflare (CF-IPCountry) - Most widely deployed
- AWS CloudFront (CloudFront-Viewer-Country)
- Fastly (Fastly-Client-IP-Country)
- Akamai (X-Akamai-Edgescape) - Complex format, extract country
- Azure Front Door (X-Azure-ClientIP-Country)
- Vercel (X-Vercel-IP-Country)
- Semi-standard headers (X-Geo-Country, X-Country-Code, Country-Code)
Simple provider headers whose value is the country code directly, checked ahead of Akamai (whose value is a compound Edgescape string). Cloudflare first (most widely deployed), then AWS CloudFront, then Fastly.
%w[ HTTP_CF_IPCOUNTRY HTTP_CLOUDFRONT_VIEWER_COUNTRY HTTP_FASTLY_CLIENT_IP_COUNTRY ].freeze
- SECONDARY_COUNTRY_HEADERS =
Remaining direct country-code headers, checked after Akamai: Azure Front Door, Vercel, then the least-reliable semi-standard headers.
%w[ HTTP_X_AZURE_CLIENTIP_COUNTRY HTTP_X_VERCEL_IP_COUNTRY HTTP_X_GEO_COUNTRY HTTP_X_COUNTRY_CODE HTTP_COUNTRY_CODE ].freeze
Class Attribute Summary collapse
-
.custom_resolver ⇒ Object
Returns the value of attribute custom_resolver.
Class Method Summary collapse
-
.resolve(ip, env = {}, config = nil, headers_trusted: true) ⇒ String
Resolve country code for an IP address.
Class Attribute Details
.custom_resolver ⇒ Object
Returns the value of attribute custom_resolver.
118 119 120 |
# File 'lib/otto/privacy/geo_resolver.rb', line 118 def custom_resolver @custom_resolver end |
Class Method Details
.resolve(ip, env = {}, config = nil, headers_trusted: true) ⇒ String
Resolve country code for an IP address.
Resolution order (first hit wins). Header steps (1–2) are skipped when
headers_trusted is false, since geo headers are client-spoofable
unless the request actually arrived through the trusted CDN/proxy:
- App-configured trusted header (+config.geo_header+)
- Built-in CDN/infrastructure provider headers
- Custom resolver hook (custom_resolver)
- Local MMDB lookup (+config.geo_db_reader+), masked before lookup
- '**' for unknown (no guessing)
Country-level MMDB networks are almost always >= /24, so a /24-masked
x.y.z.0 resolves to the same country as the real IP. The database
lookup masks internally, and the Otto middleware additionally hands this
method a masked IP and a masked env, so neither the database nor a custom
resolver ever sees the unmasked address.
168 169 170 171 172 173 174 |
# File 'lib/otto/privacy/geo_resolver.rb', line 168 def self.resolve(ip, env = {}, config = nil, headers_trusted: true) return UNKNOWN if ip.nil? || ip.empty? # Resolution is honest: when no header, custom resolver, or database # resolves a country, the answer is '**' (unknown) — never a guess. resolve_from_sources(ip, env, config, headers_trusted) || UNKNOWN end |