refactor(geoip,check-ip): lift download/refresh out of geoip into cmd

geoip.Open now just opens files; download/refresh/polling logic lives at
the cmd layer using dataset.Group with a combined httpcache.Cacher
fetcher (or PollFiles when no GeoIP.conf is available). Removes
geoip.OpenDatabases — the library is no longer concerned with refresh.
This commit is contained in:
AJ ONeal 2026-04-20 16:10:51 -06:00
parent d8b6638d97
commit 9b92136f91
No known key found for this signature in database
3 changed files with 50 additions and 48 deletions

View File

@ -37,7 +37,7 @@ type IPCheck struct {
inbound *dataset.View[ipcohort.Cohort] inbound *dataset.View[ipcohort.Cohort]
outbound *dataset.View[ipcohort.Cohort] outbound *dataset.View[ipcohort.Cohort]
geo *geoip.Databases geo *dataset.View[geoip.Databases]
} }
func printVersion(w *os.File) { func printVersion(w *os.File) {
@ -103,16 +103,16 @@ func main() {
} }
maxmind := filepath.Join(cfg.CacheDir, "maxmind") maxmind := filepath.Join(cfg.CacheDir, "maxmind")
geo, err := geoip.OpenDatabases( cityPath := filepath.Join(maxmind, geoip.CityEdition+".mmdb")
cfg.ConfPath, asnPath := filepath.Join(maxmind, geoip.ASNEdition+".mmdb")
filepath.Join(maxmind, geoip.CityEdition+".mmdb"), geoGroup := dataset.NewGroup(geoFetcher(cfg.ConfPath, cityPath, asnPath))
filepath.Join(maxmind, geoip.ASNEdition+".mmdb"), cfg.geo = dataset.Add(geoGroup, func() (*geoip.Databases, error) {
) return geoip.Open(cityPath, asnPath)
if err != nil { })
if err := geoGroup.Load(context.Background()); err != nil {
log.Fatalf("geoip: %v", err) log.Fatalf("geoip: %v", err)
} }
defer func() { _ = geo.Close() }() defer func() { _ = cfg.geo.Value().Close() }()
cfg.geo = geo
if cfg.Bind == "" { if cfg.Bind == "" {
return return
@ -121,9 +121,48 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() defer stop()
go group.Tick(ctx, refreshInterval, func(err error) { go group.Tick(ctx, refreshInterval, func(err error) {
log.Printf("refresh: %v", err) log.Printf("blocklists refresh: %v", err)
})
go geoGroup.Tick(ctx, refreshInterval, func(err error) {
log.Printf("geoip refresh: %v", err)
}) })
if err := cfg.serve(ctx); err != nil { if err := cfg.serve(ctx); err != nil {
log.Fatalf("serve: %v", err) log.Fatalf("serve: %v", err)
} }
} }
// geoFetcher returns a Fetcher for the GeoLite2 City + ASN .mmdb files.
// With a GeoIP.conf (explicit path or auto-discovered) both files are
// downloaded via httpcache conditional GETs; otherwise the files are
// expected to exist on disk and are polled for out-of-band changes.
func geoFetcher(confPath, cityPath, asnPath string) dataset.Fetcher {
if confPath == "" {
for _, p := range geoip.DefaultConfPaths() {
if _, err := os.Stat(p); err == nil {
confPath = p
break
}
}
}
if confPath == "" {
return dataset.PollFiles(cityPath, asnPath)
}
conf, err := geoip.ParseConf(confPath)
if err != nil {
log.Fatalf("geoip-conf: %v", err)
}
dl := geoip.New(conf.AccountID, conf.LicenseKey)
city := dl.NewCacher(geoip.CityEdition, cityPath)
asn := dl.NewCacher(geoip.ASNEdition, asnPath)
return dataset.FetcherFunc(func() (bool, error) {
cityUpdated, err := city.Fetch()
if err != nil {
return false, fmt.Errorf("fetch %s: %w", geoip.CityEdition, err)
}
asnUpdated, err := asn.Fetch()
if err != nil {
return false, fmt.Errorf("fetch %s: %w", geoip.ASNEdition, err)
}
return cityUpdated || asnUpdated, nil
})
}

View File

@ -42,7 +42,7 @@ func (c *IPCheck) handle(w http.ResponseWriter, r *http.Request) {
Blocked: in || out, Blocked: in || out,
BlockedInbound: in, BlockedInbound: in,
BlockedOutbound: out, BlockedOutbound: out,
Geo: c.geo.Lookup(ip), Geo: c.geo.Value().Lookup(ip),
} }
if r.URL.Query().Get("format") == "json" || if r.URL.Query().Get("format") == "json" ||

View File

@ -4,8 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"os"
"path/filepath"
"github.com/oschwald/geoip2-golang" "github.com/oschwald/geoip2-golang"
) )
@ -16,41 +14,6 @@ type Databases struct {
ASN *geoip2.Reader ASN *geoip2.Reader
} }
// OpenDatabases resolves configuration, downloads stale .mmdb files (when a
// GeoIP.conf with credentials is available), and opens the readers.
//
// - confPath="" → auto-discover from DefaultConfPaths
// - conf found → auto-download to cityPath/asnPath
// - no conf → cityPath and asnPath must point to existing .mmdb files
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)
}
if err := os.MkdirAll(filepath.Dir(cityPath), 0o755); err != nil {
return nil, err
}
dl := New(cfg.AccountID, cfg.LicenseKey)
if _, err := dl.NewCacher(CityEdition, cityPath).Fetch(); err != nil {
return nil, fmt.Errorf("fetch %s: %w", CityEdition, err)
}
if _, err := dl.NewCacher(ASNEdition, asnPath).Fetch(); err != nil {
return nil, fmt.Errorf("fetch %s: %w", ASNEdition, err)
}
}
return Open(cityPath, asnPath)
}
// Open opens city and ASN .mmdb files from the given paths. // Open opens city and ASN .mmdb files from the given paths.
func Open(cityPath, asnPath string) (*Databases, error) { func Open(cityPath, asnPath string) (*Databases, error) {
city, err := geoip2.Open(cityPath) city, err := geoip2.Open(cityPath)