// 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 required for downloading databases. // This must be set to a valid license key obtained from your MaxMind account // at https://www.maxmind.com/en/accounts/current/license-key var LicenseKey = "" // EditionIDs specifies which MaxMind database editions to download. // This should be a comma-separated list of valid edition names. // Examples: "GeoLite2-City,GeoLite2-Country" or "GeoIP2-ISP,GeoIP2-City" var EditionIDs = "" // EditionFiles contains the filenames of successfully downloaded database files. // This slice is automatically populated by DownloadGeoLite2DB and can be used // to verify which databases are available after download completion. var EditionFiles []string // Path specifies the target directory for storing downloaded MaxMind databases. // This should be set to a writable directory path. If empty, the current // working directory will be used as the default location. var Path string const ( dbExtension = ".mmdb" maxmindURL = "https://download.maxmind.com/app/geoip_download?license_key=%v&edition_id=%v&suffix=tar.gz" ) // GeoLite2DBExists verifies that all required MaxMind databases exist on disk. // // It checks for the presence of each database specified in EditionIDs within // the Path directory. Returns true only if every specified database file is // found and accessible. This is useful for determining if downloads are needed. 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's servers. // // This function requires a valid LicenseKey to be set and will download each database // edition listed in EditionIDs. Successfully downloaded files are added to EditionFiles. // If any download fails, the function returns an error and stops processing remaining databases. 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 and extracts a single MaxMind database by edition name. // // This function handles the complete download process: fetching the gzipped tar archive // from MaxMind's download API, extracting the .mmdb database file from the archive, // and saving it to the configured Path directory. The download URL includes the // license key for authentication. 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. // // This function checks each edition name in EditionIDs against the list of known // MaxMind database editions, including both commercial GeoIP2 and free GeoLite2 variants. // Returns an error if any edition name is not recognized, helping catch typos // or outdated edition names before attempting downloads. 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 is a utility function that checks if a regular file exists at the given path. // // Unlike a simple os.Stat check, this function specifically verifies that the path // points to a regular file (not a directory or other file type). Returns false // if the path doesn't exist, points to a directory, or encounters an error. func fileExists(filePath string) bool { info, err := os.Stat(filePath) if os.IsNotExist(err) { return false } return !info.IsDir() }