package geoip import ( "fmt" "net" "github.com/oschwald/geoip2-golang" ) // Service holds optional GeoLite2 City and/or ASN MMDB readers. // Country-only DB не нужен: страна уже есть в City. type Service struct { city *geoip2.Reader asn *geoip2.Reader } // Open opens a single MMDB (City or ASN) — для тестов и простых случаев. func Open(path string) (*Service, error) { r, err := geoip2.Open(path) if err != nil { return nil, fmt.Errorf("geoip open %q: %w", path, err) } return &Service{city: r}, nil } // OpenDatabases opens city and/or asn paths; at least one must be non-empty. func OpenDatabases(cityPath, asnPath string) (*Service, error) { var s Service if cityPath != "" { r, err := geoip2.Open(cityPath) if err != nil { return nil, fmt.Errorf("geoip city open %q: %w", cityPath, err) } s.city = r } if asnPath != "" { r, err := geoip2.Open(asnPath) if err != nil { if s.city != nil { _ = s.city.Close() } return nil, fmt.Errorf("geoip asn open %q: %w", asnPath, err) } s.asn = r } if s.city == nil && s.asn == nil { return nil, fmt.Errorf("geoip: no database paths") } return &s, nil } // Close releases database handles. func (s *Service) Close() error { if s == nil { return nil } var first error if s.city != nil { if err := s.city.Close(); err != nil { first = err } s.city = nil } if s.asn != nil { if err := s.asn.Close(); err != nil && first == nil { first = err } s.asn = nil } return first } // Result combines City + ASN fields when available. type Result struct { CountryCode string CountryName string CityName string Latitude float64 Longitude float64 ASN uint64 ASOrg string } // Lookup fills geographic and/or ASN data for an IP string (IPv4/IPv6). func (s *Service) Lookup(ipStr string) (Result, bool) { var out Result if s == nil { return out, false } ip := net.ParseIP(ipStr) if ip == nil { return out, false } ok := false if s.city != nil { if rec, err := s.city.City(ip); err == nil { out.CountryCode = rec.Country.IsoCode if rec.Country.Names != nil { out.CountryName = rec.Country.Names["en"] } if rec.City.Names != nil { out.CityName = rec.City.Names["en"] } // Для карты важны координаты. Даже если MaxMind вернул (0,0), // лучше прокидывать их дальше, чем скрывать все точки. out.Latitude = rec.Location.Latitude out.Longitude = rec.Location.Longitude if out.CountryCode != "" || out.CountryName != "" || out.CityName != "" { ok = true } } } if s.asn != nil { if rec, err := s.asn.ASN(ip); err == nil { out.ASN = uint64(rec.AutonomousSystemNumber) out.ASOrg = rec.AutonomousSystemOrganization if out.ASN != 0 || out.ASOrg != "" { ok = true } } } return out, ok }