// Package maxmind provides utilities for downloading and managing MaxMind GeoIP databases. // // This package handles downloading GeoIP databases from MaxMind's servers, // extracting them from compressed archives, and validating database editions. // It supports both commercial GeoIP2 and free GeoLite2 database editions. // // The implementation is based on the approach used in the Kubernetes ingress-nginx project. // See: https://github.com/kubernetes/ingress-nginx/pull/4896/files package maxmind import ( "archive/tar" "compress/gzip" "fmt" "io" "net/http" "os" "path" "strings" ) // LicenseKey is the MaxMind license key used to download databases. // This must be set to a valid license key obtained from MaxMind. var LicenseKey = "" // EditionIDs is a comma-separated list of MaxMind database editions to download. // Examples: "GeoLite2-City,GeoLite2-Country" or "GeoIP2-ISP". var EditionIDs = "" // EditionFiles contains the list of database files that have been downloaded. // This slice is populated by DownloadGeoLite2DB after successful downloads. var EditionFiles []string // Path specifies the directory where MaxMind databases should be stored. // If empty, a default system path will be used. var Path string const ( dbExtension = ".mmdb" maxmindURL = "https://download.maxmind.com/app/geoip_download?license_key=%v&edition_id=%v&suffix=tar.gz" ) // GeoLite2DBExists checks if all required MaxMind databases exist in the filesystem. // It verifies that all databases specified in EditionIDs are present in the Path directory. // Returns true only if all specified databases are found. func GeoLite2DBExists() bool { for _, dbName := range strings.Split(EditionIDs, ",") { if !fileExists(path.Join(Path, dbName+dbExtension)) { return false } } return true } // DownloadGeoLite2DB downloads all databases specified in EditionIDs from MaxMind. // It requires a valid LicenseKey and updates EditionFiles with successfully downloaded files. // Returns an error if any download fails. func DownloadGeoLite2DB() error { for _, dbName := range strings.Split(EditionIDs, ",") { err := downloadDatabase(dbName) if err != nil { return err } EditionFiles = append(EditionFiles, dbName+dbExtension) } return nil } // downloadDatabase downloads a single MaxMind database by name. // It fetches the database archive from MaxMind, extracts the .mmdb file, // and saves it to the specified Path directory. func downloadDatabase(dbName string) error { url := fmt.Sprintf(maxmindURL, LicenseKey, dbName) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP status %v", resp.Status) } archive, err := gzip.NewReader(resp.Body) if err != nil { return err } defer archive.Close() mmdbFile := dbName + dbExtension tarReader := tar.NewReader(archive) for true { header, err := tarReader.Next() if err == io.EOF { break } if err != nil { return err } switch header.Typeflag { case tar.TypeReg: if !strings.HasSuffix(header.Name, mmdbFile) { continue } outFile, err := os.Create(path.Join(Path, mmdbFile)) if err != nil { return err } defer outFile.Close() if _, err := io.Copy(outFile, tarReader); err != nil { return err } return nil } } return fmt.Errorf("the URL %v does not contains the database %v", fmt.Sprintf(maxmindURL, "XXXXXXX", dbName), mmdbFile) } // ValidateGeoLite2DBEditions validates that all specified database editions are recognized. // It checks each edition in EditionIDs against a list of known MaxMind database editions. // Returns an error if any unknown edition names are found. func ValidateGeoLite2DBEditions() error { allowedEditions := map[string]bool{ "GeoIP2-Anonymous-IP": true, "GeoIP2-Country": true, "GeoIP2-City": true, "GeoIP2-Connection-Type": true, "GeoIP2-Domain": true, "GeoIP2-ISP": true, "GeoIP2-ASN": true, "GeoLite2-ASN": true, "GeoLite2-Country": true, "GeoLite2-City": true, } for _, edition := range strings.Split(EditionIDs, ",") { if !allowedEditions[edition] { return fmt.Errorf("unknown Maxmind GeoIP2 edition name: '%s'", edition) } } return nil } // fileExists checks if a file exists at the given path and is not a directory. // It returns true only if the path points to an existing regular file. func fileExists(filePath string) bool { info, err := os.Stat(filePath) if os.IsNotExist(err) { return false } return !info.IsDir() }