refactor(net/formmailer): delegate GeoIP downloads

This commit is contained in:
AJ ONeal 2026-09-09 11:37:26 -06:00
parent 219365ffd8
commit 37c004cec4
No known key found for this signature in database
3 changed files with 14 additions and 106 deletions

View File

@ -235,21 +235,10 @@ type FormMailer struct {
// --- GeoIP configuration (used by Run) ---
// GeoIPConfPath is the path to a GeoIP.conf file. Zero searches
// ./GeoIP.conf and ~/.config/maxmind/GeoIP.conf. Its presence selects
// download mode (formmailer owns the geoip-update loop); its absence
// falls back to polling pre-existing archives.
GeoIPConfPath string
// GeoIPDir is the directory holding the GeoLite2 tarballs. Zero uses
// ~/.cache/maxmind. Ignored when using programmatic setup.
// GeoIPDir is the directory holding the GeoLite2 tarballs. An external
// geoip-update command owns downloads. Zero uses ~/.cache/maxmind.
// Ignored when using programmatic setup.
GeoIPDir string
// GeoIPBaseURL overrides the MaxMind download base. Zero uses the
// geoip.DownloadBase default. Library-level; not exposed on the CLI.
GeoIPBaseURL string
// GeoIPFreshDays gates downloads: skip HTTP when a local archive is
// younger than N days. Zero uses 3. Maps to httpcache.Cacher.MaxAge;
// a library-level option, rare to expose all the way up to the CLI.
GeoIPFreshDays int
// AllowedCountries — if non-nil, only requests from listed ISO codes are
// accepted. Unknown country ("") is always allowed. Requires GeoIP to be
// loaded.
@ -343,9 +332,8 @@ func (fm *FormMailer) contentType() string {
// Data sources:
// - Blocklist: gitshallow clones the configured repo, ipcohort loads the
// inbound cohort files, dataset manages hot-swap.
// - GeoIP: geoip.Fetcher pulls the GeoLite2 City + ASN tarballs via
// httpcache on each refresh tick; geoip.Open extracts them in memory
// and dataset manages hot-swap. formmailer owns its own downloads.
// - GeoIP: an external geoip-update job owns downloads; formmailer polls
// local City + ASN tarballs and geoip.Open extracts them in memory.
//
// Background refreshes run at RefreshInterval (default 47min). The server
// starts serving immediately; /healthz returns 503 until both blocklist and
@ -401,16 +389,13 @@ func (fm *FormMailer) Run(ctx context.Context) error {
})
}
// GeoIP directory. Archives live here; formmailer either downloads them
// (when a GeoIP.conf is present) or polls pre-existing ones (an external
// geoip-update job owns the downloads).
// GeoIP archives are downloaded by the external geoip-update command.
// Formmailer only polls and reloads the local tarballs.
geoDir := filepath.Join(fm.CacheDir, "maxmind")
if fm.GeoIPDir != "" {
geoDir = fm.GeoIPDir
}
// dirPopulated reports whether dir holds any entries. Used to pick the
// poll mode when no GeoIP.conf is present.
dirPopulated := func(dir string) bool {
entries, err := os.ReadDir(dir)
if err != nil {
@ -419,51 +404,10 @@ func (fm *FormMailer) Run(ctx context.Context) error {
return len(entries) > 0
}
// Decide GeoIP mode from what's on disk. A present GeoIP.conf selects
// download mode (formmailer owns the geoip-update loop via
// geoip.Fetcher); otherwise, if the archive dir is already populated,
// poll it (external job owns downloads). Either way the view loader
// extracts the archives in memory. The refresh interval is the
// dataset layer's job (Set.Tick); freshness gating is httpcache's
// (Cacher.MaxAge == fresh-days).
confExists := fm.GeoIPConfPath != ""
if confExists {
if _, err := os.Stat(fm.GeoIPConfPath); err != nil {
confExists = false
log.Printf("geoip: configured conf %s not found; falling back to poll mode", fm.GeoIPConfPath)
}
}
if !confExists {
for _, p := range geoip.DefaultConfPaths() {
if _, err := os.Stat(p); err == nil {
fm.GeoIPConfPath = p
confExists = true
break
}
}
}
var geoSet *dataset.Set
var geoView *dataset.View[geoip.Databases]
geoMode := "disabled"
switch {
case confExists:
// Download mode: formmailer owns the geoip-update loop.
geoMode = "download"
geoFetcher, ferr := geoip.NewFetcher(fm.GeoIPConfPath)
if ferr != nil {
log.Printf("geoip: %v", ferr)
} else {
geoFetcher.Dir = geoDir
geoFetcher.BaseURL = fm.GeoIPBaseURL
geoFetcher.FreshDays = fm.GeoIPFreshDays
geoSet = dataset.NewSet(geoFetcher)
geoView = dataset.Add(geoSet, func(ctx context.Context) (*geoip.Databases, error) {
return geoip.Open(geoDir)
})
}
case dirPopulated(geoDir):
// Poll mode: an external job owns the downloads; we just reload.
if dirPopulated(geoDir) {
geoMode = "poll"
cityPath := filepath.Join(geoDir, geoip.TarGzName(geoip.CityEdition))
asnPath := filepath.Join(geoDir, geoip.TarGzName(geoip.ASNEdition))
@ -575,14 +519,9 @@ func (fm *FormMailer) Run(ctx context.Context) error {
if fm.BlocklistRepo != "" {
fmt.Println("Blocklist: enabled")
}
if geoSet != nil {
switch geoMode {
case "download":
fmt.Printf("GeoIP: download mode (conf=%s, dir=%s)\n", fm.GeoIPConfPath, geoDir)
case "poll":
if geoSet != nil && geoMode == "poll" {
fmt.Printf("GeoIP: poll mode (dir=%s)\n", geoDir)
}
}
if len(fm.AllowedCountries) > 0 {
fmt.Printf("Country gate: %s\n", strings.Join(fm.AllowedCountries, ", "))
}

View File

@ -10,6 +10,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/therootcompany/golib/https v0.9.0 h1:D9gh0K6ljkvF805JUbylITn8SHPMo08q5PglQGXsK8c=
github.com/therootcompany/golib/https v0.9.0/go.mod h1:ilExqR/Uh7oB36oy9mwuNhANcFh+zdCfD3uJiZ11YAY=
github.com/therootcompany/golib/net/geoip v0.5.3 h1:wps0TrRoGYw7XA5mWP0uOkGBF5oZ5rQ+1JPIAkRYrOU=
github.com/therootcompany/golib/net/geoip v0.5.3/go.mod h1:3CuuG2igsZySLNrRxoRTHPw3fmw9c4b0jRajSOOKJNU=
github.com/therootcompany/golib/net/gitshallow v0.9.3 h1:FKi0IvpqEDnYasjTmkRmXDdi+y3bhQBcjgvggwAwxFs=
github.com/therootcompany/golib/net/gitshallow v0.9.3/go.mod h1:S/3OK7wMOqJJjP79U9Qw/l3jjEyrb/zsjJ8HAPAX/Fc=
github.com/therootcompany/golib/net/httpcache v0.5.1 h1:vQM758tBwPEqCo7ojpSeL3fHqgq29LYmvD2hvDTBfKY=

View File

@ -3,8 +3,6 @@ package formmailer
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
@ -47,41 +45,10 @@ func runOnce(t *testing.T, fm *FormMailer) string {
})
}
// TestRunGeoModeSelection sanity-checks the hybrid GeoIP mode selection:
// - conf present -> download mode
// - conf absent + populated -> poll mode
// - conf absent + empty dir -> disabled
// TestRunGeoModeSelection checks that formmailer only polls existing
// archives; downloading is owned by the external geoip-update command.
func TestRunGeoModeSelection(t *testing.T) {
t.Run("download", func(t *testing.T) {
dir := t.TempDir()
conf := filepath.Join(dir, "GeoIP.conf")
if err := os.WriteFile(conf, []byte(
"AccountID 123456\nLicenseKey testkey\nEditionIDs GeoLite2-City GeoLite2-ASN\n"), 0o600); err != nil {
t.Fatal(err)
}
// 500 server so the owned download loop fails fast; the mode is
// selected and logged before serving regardless.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nope", http.StatusInternalServerError)
}))
defer srv.Close()
out := runOnce(t, &FormMailer{
CacheDir: dir,
GeoIPConfPath: conf,
GeoIPBaseURL: srv.URL,
SMTPHost: "localhost:25",
SMTPFrom: "a@example.com",
SMTPTo: []string{"b@example.com"},
})
if !strings.Contains(out, "GeoIP: download mode") {
t.Fatalf("missing download mode line; stdout:\n%s", out)
}
})
t.Run("poll", func(t *testing.T) {
// Point HOME at a temp dir so the default conf discovery can't find a
// real ~/.config/maxmind/GeoIP.conf.
home := t.TempDir()
t.Setenv("HOME", home)