From f75d5c489abc2b32d01ceb5c92d62c6c3e940606 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 20 Apr 2026 16:55:15 -0600 Subject: [PATCH] refactor(httpcache): use http.Header instead of AuthHeader/AuthValue Cacher.Header is a stdlib http.Header that's merged into every request. Authorization is stripped on redirect unconditionally (presigned S3/R2 targets, etc). Callers build the header with the usual http.Header literal; BasicAuth/Bearer still produce the Authorization value. --- cmd/check-ip/main.go | 20 +++++++-------- net/geoip/cmd/geoip-update/main.go | 12 ++++----- net/geoip/geoip_integration_test.go | 10 +++++--- net/httpcache/httpcache.go | 38 +++++++++++++---------------- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/cmd/check-ip/main.go b/cmd/check-ip/main.go index f75b5e5..07da45b 100644 --- a/cmd/check-ip/main.go +++ b/cmd/check-ip/main.go @@ -9,6 +9,7 @@ import ( "flag" "fmt" "log" + "net/http" "os" "os/signal" "path/filepath" @@ -143,20 +144,19 @@ func main() { asnTarPath := filepath.Join(maxmindDir, "GeoLite2-ASN.tar.gz") var geoSet *dataset.Set if cfg.GeoIPBasicAuth != "" { + authHeader := http.Header{"Authorization": []string{cfg.GeoIPBasicAuth}} geoSet = dataset.NewSet( &httpcache.Cacher{ - URL: geoip.DownloadBase + "/GeoLite2-City/download?suffix=tar.gz", - Path: cityTarPath, - MaxAge: 3 * 24 * time.Hour, - AuthHeader: "Authorization", - AuthValue: cfg.GeoIPBasicAuth, + URL: geoip.DownloadBase + "/GeoLite2-City/download?suffix=tar.gz", + Path: cityTarPath, + MaxAge: 3 * 24 * time.Hour, + Header: authHeader, }, &httpcache.Cacher{ - URL: geoip.DownloadBase + "/GeoLite2-ASN/download?suffix=tar.gz", - Path: asnTarPath, - MaxAge: 3 * 24 * time.Hour, - AuthHeader: "Authorization", - AuthValue: cfg.GeoIPBasicAuth, + URL: geoip.DownloadBase + "/GeoLite2-ASN/download?suffix=tar.gz", + Path: asnTarPath, + MaxAge: 3 * 24 * time.Hour, + Header: authHeader, }, ) } else { diff --git a/net/geoip/cmd/geoip-update/main.go b/net/geoip/cmd/geoip-update/main.go index f4617c2..f848160 100644 --- a/net/geoip/cmd/geoip-update/main.go +++ b/net/geoip/cmd/geoip-update/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "net/http" "os" "path/filepath" "time" @@ -45,18 +46,17 @@ func main() { os.Exit(1) } - auth := httpcache.BasicAuth(cfg.AccountID, cfg.LicenseKey) + authHeader := http.Header{"Authorization": []string{httpcache.BasicAuth(cfg.AccountID, cfg.LicenseKey)}} maxAge := time.Duration(*freshDays) * 24 * time.Hour exitCode := 0 for _, edition := range cfg.EditionIDs { path := filepath.Join(outDir, edition+".tar.gz") cacher := &httpcache.Cacher{ - URL: geoip.DownloadBase + "/" + edition + "/download?suffix=tar.gz", - Path: path, - MaxAge: maxAge, - AuthHeader: "Authorization", - AuthValue: auth, + URL: geoip.DownloadBase + "/" + edition + "/download?suffix=tar.gz", + Path: path, + MaxAge: maxAge, + Header: authHeader, } updated, err := cacher.Fetch() if err != nil { diff --git a/net/geoip/geoip_integration_test.go b/net/geoip/geoip_integration_test.go index b9b90d6..ff155c0 100644 --- a/net/geoip/geoip_integration_test.go +++ b/net/geoip/geoip_integration_test.go @@ -3,6 +3,7 @@ package geoip_test import ( + "net/http" "os" "path/filepath" "testing" @@ -50,10 +51,11 @@ func geoipConf(t *testing.T) *geoip.Conf { func newCacher(cfg *geoip.Conf, edition, path string) *httpcache.Cacher { return &httpcache.Cacher{ - URL: geoip.DownloadBase + "/" + edition + "/download?suffix=tar.gz", - Path: path, - AuthHeader: "Authorization", - AuthValue: httpcache.BasicAuth(cfg.AccountID, cfg.LicenseKey), + URL: geoip.DownloadBase + "/" + edition + "/download?suffix=tar.gz", + Path: path, + Header: http.Header{ + "Authorization": []string{httpcache.BasicAuth(cfg.AccountID, cfg.LicenseKey)}, + }, } } diff --git a/net/httpcache/httpcache.go b/net/httpcache/httpcache.go index 783f9b6..0f218e5 100644 --- a/net/httpcache/httpcache.go +++ b/net/httpcache/httpcache.go @@ -14,14 +14,14 @@ import ( ) // BasicAuth returns an HTTP Basic Authorization header value: -// "Basic " + base64(user:pass). Assign to Cacher.AuthValue with -// AuthHeader "Authorization". +// "Basic " + base64(user:pass). Pair with the "Authorization" header in +// Cacher.Header. func BasicAuth(user, pass string) string { return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass)) } // Bearer returns a Bearer Authorization header value: "Bearer " + token. -// Assign to Cacher.AuthValue with AuthHeader "Authorization". +// Pair with the "Authorization" header in Cacher.Header. func Bearer(token string) string { return "Bearer " + token } @@ -45,12 +45,10 @@ const ( // Caching — ETag and Last-Modified values are persisted to a .meta // sidecar file so conditional GETs survive process restarts. // -// Auth — AuthHeader/AuthValue set a request header on every attempt. Auth is -// stripped before following redirects so presigned targets (e.g. S3/R2 URLs) -// never receive credentials. Use any scheme: "Authorization"/"Bearer token", -// "X-API-Key"/"secret", "Authorization"/"Basic base64(user:pass)", etc. The -// BasicAuth and Bearer helpers produce the right AuthValue for the common -// cases. +// Header — any values in Header are sent on every request. Authorization +// headers are stripped before following redirects so presigned targets +// (e.g. S3/R2 URLs) never receive credentials. The BasicAuth and Bearer +// helpers produce Authorization values for the common cases. type Cacher struct { URL string Path string @@ -58,8 +56,7 @@ type Cacher struct { Timeout time.Duration // 0 uses 5m; caps overall request including body read MaxAge time.Duration // 0 disables; skip HTTP if file mtime is within this MinInterval time.Duration // 0 disables; skip HTTP if last Fetch attempt was within this - AuthHeader string // e.g. "Authorization" or "X-API-Key" - AuthValue string // e.g. "Bearer token" or "Basic base64(user:pass)" + Header http.Header // headers sent on every request (Authorization is stripped on redirect) mu sync.Mutex etag string @@ -166,19 +163,18 @@ func (c *Cacher) Fetch() (updated bool, err error) { TLSHandshakeTimeout: connTimeout, } - if c.AuthHeader != "" { - req.Header.Set(c.AuthHeader, c.AuthValue) + for k, vs := range c.Header { + for _, v := range vs { + req.Header.Add(k, v) + } } client := &http.Client{Timeout: timeout, Transport: transport} - if c.AuthHeader != "" { - // Strip auth before following any redirect — redirect targets (e.g. - // presigned S3/R2 URLs) must not receive our credentials. - authHeader := c.AuthHeader - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - req.Header.Del(authHeader) - return nil - } + // Strip Authorization before following any redirect — redirect targets + // (e.g. presigned S3/R2 URLs) must not receive our credentials. + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + req.Header.Del("Authorization") + return nil } resp, err := client.Do(req)