package geoip import ( "errors" "fmt" "net/netip" "strings" "sync" "github.com/oschwald/geoip2-golang/v2" ) var ( // ErrInvalidAddress reports that a caller supplied an invalid IP address. ErrInvalidAddress = errors.New("invalid IP address") // ErrCountryNotFound reports that the GeoIP database contains no country // record for a valid IP address. ErrCountryNotFound = errors.New("country not found") // ErrClosed reports that the resolver has already released its database // resources and can no longer serve lookups. ErrClosed = errors.New("geoip resolver is closed") ) type countryReader interface { Country(netip.Addr) (*geoip2.Country, error) Close() error } // Resolver resolves ISO 3166-1 alpha-2 country codes from a local MaxMind // country-capable database. // // Resolver is safe for concurrent lookups. Call Close when the process no // longer needs the memory-mapped database file. type Resolver struct { mu sync.RWMutex reader countryReader } // Open constructs a Resolver backed by the MaxMind database file at // databasePath. // // databasePath must point to a local country-capable .mmdb file such as // GeoLite2 Country. Open trims surrounding whitespace from databasePath before // opening the file. func Open(databasePath string) (*Resolver, error) { path := strings.TrimSpace(databasePath) if path == "" { return nil, errors.New("open geoip database: database path must not be empty") } reader, err := geoip2.Open(path) if err != nil { return nil, fmt.Errorf("open geoip database %q: %w", path, err) } return newResolver(reader), nil } // NewGeoIP is a legacy alias for Open. // // Deprecated: use Open for new code. func NewGeoIP(databasePath string) (*Resolver, error) { return Open(databasePath) } func newResolver(reader countryReader) *Resolver { return &Resolver{reader: reader} } // Country resolves addr to an uppercase ISO 3166-1 alpha-2 country code. // // Country returns ErrInvalidAddress when addr is not valid, ErrCountryNotFound // when the database contains no country for addr, and ErrClosed after Close // has been called successfully. func (r *Resolver) Country(addr netip.Addr) (string, error) { if !addr.IsValid() { return "", fmt.Errorf("lookup country: %w", ErrInvalidAddress) } if r == nil { return "", fmt.Errorf("lookup country for %s: %w", addr, ErrClosed) } r.mu.RLock() defer r.mu.RUnlock() if r.reader == nil { return "", fmt.Errorf("lookup country for %s: %w", addr, ErrClosed) } record, err := r.reader.Country(addr) if err != nil { return "", fmt.Errorf("lookup country for %s: %w", addr, err) } if record == nil { return "", fmt.Errorf("lookup country for %s: nil country record", addr) } if !record.HasData() || strings.TrimSpace(record.Country.ISOCode) == "" { return "", fmt.Errorf("lookup country for %s: %w", addr, ErrCountryNotFound) } code, err := normalizeCountryCode(record.Country.ISOCode) if err != nil { return "", fmt.Errorf("lookup country for %s: %w", addr, err) } return code, nil } // CountryString resolves raw to an uppercase ISO 3166-1 alpha-2 country code. // // CountryString trims surrounding whitespace from raw before parsing it as an // IP address and then delegates to Country. func (r *Resolver) CountryString(raw string) (string, error) { trimmed := strings.TrimSpace(raw) addr, err := netip.ParseAddr(trimmed) if err != nil { return "", fmt.Errorf("parse IP address %q: %w", raw, errors.Join(ErrInvalidAddress, err)) } return r.Country(addr) } // Close releases the underlying database resources. // // Close is idempotent and nil-safe. func (r *Resolver) Close() error { if r == nil { return nil } r.mu.Lock() defer r.mu.Unlock() if r.reader == nil { return nil } reader := r.reader r.reader = nil if err := reader.Close(); err != nil { return fmt.Errorf("close geoip resolver: %w", err) } return nil } func normalizeCountryCode(raw string) (string, error) { code := strings.ToUpper(strings.TrimSpace(raw)) if len(code) != 2 { return "", fmt.Errorf("invalid ISO 3166-1 alpha-2 code %q", raw) } for idx := 0; idx < len(code); idx++ { if code[idx] < 'A' || code[idx] > 'Z' { return "", fmt.Errorf("invalid ISO 3166-1 alpha-2 code %q", raw) } } return code, nil }