mirror of
https://github.com/therootcompany/golib.git
synced 2026-04-24 20:58:00 +00:00
httpcache.Cacher gains: - Username/Password: Basic Auth, stripped before following redirects - MaxAge: skip HTTP if local file mtime is within this duration - MinInterval: skip HTTP if last Fetch attempt was within this duration - Transform: post-process response body (e.g. extract .mmdb from tar.gz) geoip.Downloader now builds an httpcache.Cacher via NewCacher(), removing its own HTTP client. ExtractMMDB is now exported for use as a Transform. check-ip-blacklist renamed to check-ip; adds -city-db / -asn-db flags for GeoLite2 lookup (country, city, subdivision, ASN) printed after each blocklist result.
117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package geoip
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
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,
|
|
Username: d.AccountID,
|
|
Password: d.LicenseKey,
|
|
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
|
|
}
|
|
}
|