mirror of
https://github.com/therootcompany/golib.git
synced 2026-04-24 20:58:00 +00:00
OpenDatabases(confPath, cityPath, asnPath) handles conf discovery, cache dir setup, and Databases construction. DefaultConfPaths lists the standard GeoIP.conf locations. cmd/check-ip/geo.go deleted; main calls one function.
186 lines
5.0 KiB
Go
186 lines
5.0 KiB
Go
package geoip
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/therootcompany/golib/net/httpcache"
|
|
)
|
|
|
|
const (
|
|
CityEdition = "GeoLite2-City"
|
|
ASNEdition = "GeoLite2-ASN"
|
|
CountryEdition = "GeoLite2-Country"
|
|
|
|
downloadBase = "https://download.maxmind.com/geoip/databases"
|
|
defaultFreshDays = 3
|
|
defaultTimeout = 5 * time.Minute
|
|
)
|
|
|
|
// Downloader fetches MaxMind GeoLite2 .mmdb files from the download API.
|
|
// For one-shot use call Fetch; for polling loops call NewCacher and reuse
|
|
// the Cacher so ETag state is preserved across calls.
|
|
type Downloader struct {
|
|
AccountID string
|
|
LicenseKey string
|
|
FreshDays int // 0 uses 3
|
|
Timeout time.Duration // 0 uses 5m
|
|
}
|
|
|
|
// DefaultConfPaths returns the standard locations where GeoIP.conf is looked
|
|
// up: ./GeoIP.conf, then ~/.config/maxmind/GeoIP.conf.
|
|
func DefaultConfPaths() []string {
|
|
paths := []string{"GeoIP.conf"}
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
paths = append(paths, filepath.Join(home, ".config", "maxmind", "GeoIP.conf"))
|
|
}
|
|
return paths
|
|
}
|
|
|
|
// OpenDatabases discovers credentials and paths, then returns a ready-to-Init
|
|
// Databases. Returns nil with no error when geoip is not configured.
|
|
//
|
|
// - confPath="" → auto-discover from DefaultConfPaths
|
|
// - conf found → auto-download; cityPath/asnPath override default locations
|
|
// - no conf → cityPath and asnPath must point to existing .mmdb files
|
|
// - no conf and no paths → geoip disabled (returns nil, nil)
|
|
func OpenDatabases(confPath, cityPath, asnPath string) (*Databases, error) {
|
|
if confPath == "" {
|
|
for _, p := range DefaultConfPaths() {
|
|
if _, err := os.Stat(p); err == nil {
|
|
confPath = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if confPath != "" {
|
|
cfg, err := ParseConf(confPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("geoip-conf: %w", err)
|
|
}
|
|
dbDir := cfg.DatabaseDirectory
|
|
if dbDir == "" {
|
|
if dbDir, err = DefaultCacheDir(); err != nil {
|
|
return nil, fmt.Errorf("geoip cache dir: %w", err)
|
|
}
|
|
}
|
|
if err := os.MkdirAll(dbDir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("mkdir %s: %w", dbDir, err)
|
|
}
|
|
if cityPath == "" {
|
|
cityPath = filepath.Join(dbDir, CityEdition+".mmdb")
|
|
}
|
|
if asnPath == "" {
|
|
asnPath = filepath.Join(dbDir, ASNEdition+".mmdb")
|
|
}
|
|
return New(cfg.AccountID, cfg.LicenseKey).NewDatabases(cityPath, asnPath), nil
|
|
}
|
|
|
|
if cityPath == "" && asnPath == "" {
|
|
return nil, nil
|
|
}
|
|
return NewDatabases(cityPath, asnPath), nil
|
|
}
|
|
|
|
// DefaultCacheDir returns the OS cache directory for MaxMind databases,
|
|
// e.g. ~/.cache/maxmind on Linux or ~/Library/Caches/maxmind on macOS.
|
|
func DefaultCacheDir() (string, error) {
|
|
base, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(base, "maxmind"), nil
|
|
}
|
|
|
|
// New returns a Downloader configured with the given credentials.
|
|
func New(accountID, licenseKey string) *Downloader {
|
|
return &Downloader{AccountID: accountID, LicenseKey: licenseKey}
|
|
}
|
|
|
|
// NewCacher returns an httpcache.Cacher pre-configured for this edition and
|
|
// path. Hold the Cacher and call Fetch() on it periodically — ETag state is
|
|
// preserved across calls, enabling conditional GETs that skip the download
|
|
// count on unchanged releases.
|
|
func (d *Downloader) NewCacher(edition, path string) *httpcache.Cacher {
|
|
freshDays := d.FreshDays
|
|
if freshDays == 0 {
|
|
freshDays = defaultFreshDays
|
|
}
|
|
timeout := d.Timeout
|
|
if timeout == 0 {
|
|
timeout = defaultTimeout
|
|
}
|
|
creds := base64.StdEncoding.EncodeToString([]byte(d.AccountID + ":" + d.LicenseKey))
|
|
return &httpcache.Cacher{
|
|
URL: fmt.Sprintf("%s/%s/download?suffix=tar.gz", downloadBase, edition),
|
|
Path: path,
|
|
MaxAge: time.Duration(freshDays) * 24 * time.Hour,
|
|
Timeout: timeout,
|
|
AuthHeader: "Authorization",
|
|
AuthValue: "Basic " + creds,
|
|
Transform: ExtractMMDB,
|
|
}
|
|
}
|
|
|
|
// Fetch downloads edition to path if the file is stale. Convenience wrapper
|
|
// around NewCacher for one-shot use; ETag state is not retained.
|
|
func (d *Downloader) Fetch(edition, path string) (bool, error) {
|
|
return d.NewCacher(edition, path).Fetch()
|
|
}
|
|
|
|
// ExtractMMDB reads a MaxMind tar.gz archive, writes the .mmdb entry to path
|
|
// atomically (via tmp+rename), and sets its mtime to MaxMind's release date.
|
|
func ExtractMMDB(r io.Reader, path string) error {
|
|
gr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gr.Close()
|
|
|
|
tr := tar.NewReader(gr)
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
return fmt.Errorf("no .mmdb file found in archive")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasSuffix(hdr.Name, ".mmdb") {
|
|
continue
|
|
}
|
|
|
|
tmp := path + ".tmp"
|
|
f, err := os.Create(tmp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(f, tr); err != nil {
|
|
f.Close()
|
|
os.Remove(tmp)
|
|
return err
|
|
}
|
|
f.Close()
|
|
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
os.Remove(tmp)
|
|
return err
|
|
}
|
|
|
|
// Preserve MaxMind's release date so mtime == data age, not download time.
|
|
if !hdr.ModTime.IsZero() {
|
|
os.Chtimes(path, hdr.ModTime, hdr.ModTime)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|