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: 1. App-configured trusted header (Config#geo_header), e.g. ‘X-Client-Country’ 2. Built-in CDN/infrastructure provider headers (see below) 3. Custom resolver hook (GeoResolver.custom_resolver) 4. Local MMDB lookup, masked before lookup (Config#geo_db_reader) 5. ‘**’ (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: 1. Cloudflare (CF-IPCountry) - Most widely deployed 2. AWS CloudFront (CloudFront-Viewer-Country) 3. Fastly (Fastly-Client-IP-Country) 4. Akamai (X-Akamai-Edgescape) - Complex format, extract country 5. Azure Front Door (X-Azure-ClientIP-Country) 6. Vercel (X-Vercel-IP-Country) 7. 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: 1. App-configured trusted header (+config.geo_header+) 2. Built-in CDN/infrastructure provider headers 3. Custom resolver hook (custom_resolver) 4. Local MMDB lookup (+config.geo_db_reader+), masked before lookup 5. ‘**’ 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 |