| 1 | package console |
| 2 | |
| 3 | // The console's GeoIP reader (geoip spec §11.3): it follows the generation |
| 4 | // this box's own edge mirrors, checking /data/carlos/geoip/current once a |
| 5 | // minute, so region guesses and the /geo endpoint answer from the published |
| 6 | // database without a second download or a restart. |
| 7 | |
| 8 | import ( |
| 9 | "errors" |
| 10 | "log" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "sync" |
| 14 | "time" |
| 15 | |
| 16 | "github.com/carlosframework/platform/internal/geo" |
| 17 | "github.com/carlosframework/platform/internal/geoip" |
| 18 | ) |
| 19 | |
| 20 | // followerExpired is D1's rule; a var so a test can apply it to the |
| 21 | // GeoIP2 fixtures, which never expire. |
| 22 | var followerExpired = geoip.Expired |
| 23 | |
| 24 | type geoipFollower struct { |
| 25 | dir string |
| 26 | sw *geo.Swappable |
| 27 | now func() time.Time |
| 28 | |
| 29 | mu sync.Mutex |
| 30 | loadedSHA string |
| 31 | lastErrLog time.Time |
| 32 | } |
| 33 | |
| 34 | // NewGeoipFollower follows dir (the edge's local GeoIP directory) into sw. |
| 35 | func NewGeoipFollower(dir string, sw *geo.Swappable) *geoipFollower { |
| 36 | return &geoipFollower{dir: dir, sw: sw, now: time.Now} |
| 37 | } |
| 38 | |
| 39 | // Tick is one check (spec §11.3's table). |
| 40 | func (f *geoipFollower) Tick() { |
| 41 | f.mu.Lock() |
| 42 | defer f.mu.Unlock() |
| 43 | // D1 on what is LOADED, first and whatever the local state says: an |
| 44 | // unreadable or unopenable `current` must never keep an expired reader |
| 45 | // serving (code review R3-1). |
| 46 | if src, build, ok := f.sw.Loaded(); ok && followerExpired(src, build, f.now()) { |
| 47 | f.drop("console: geoip database expired (GeoLite2, 30 days after build) and was closed") |
| 48 | } |
| 49 | raw, err := os.ReadFile(filepath.Join(f.dir, geoip.LocalCurrentFile)) |
| 50 | var cur geoip.LocalCurrent |
| 51 | if err == nil { |
| 52 | cur, err = geoip.ParseLocalCurrent(raw) |
| 53 | } |
| 54 | switch { |
| 55 | case errors.Is(err, os.ErrNotExist) || (err == nil && cur.SHA256 == ""): |
| 56 | f.drop("") // off, or nothing published yet: the normal empty start |
| 57 | return |
| 58 | case err != nil: |
| 59 | f.logHourly("console: geoip local state unreadable; keeping the loaded database") |
| 60 | return |
| 61 | case followerExpired(cur.DatabaseType, time.Unix(cur.BuildEpoch, 0), f.now()): |
| 62 | f.drop("console: geoip database expired (GeoLite2, 30 days after build) and was closed") |
| 63 | return |
| 64 | case cur.SHA256 == f.loadedSHA: |
| 65 | return |
| 66 | } |
| 67 | db, err := geo.OpenMMDBTrusted(filepath.Join(f.dir, cur.SHA256+".mmdb")) |
| 68 | if err != nil { |
| 69 | // The edge may be mid-swap; the next tick sees the settled state. |
| 70 | f.logHourly("console: geoip generation named by the edge is unopenable; keeping the loaded database") |
| 71 | return |
| 72 | } |
| 73 | if followerExpired(db.Source(), db.BuildEpoch(), f.now()) { |
| 74 | db.Close() // the file's own metadata, not just what `current` claims |
| 75 | f.drop("console: geoip database expired (GeoLite2, 30 days after build) and was closed") |
| 76 | return |
| 77 | } |
| 78 | f.sw.Swap(db) |
| 79 | f.loadedSHA = cur.SHA256 |
| 80 | log.Printf("console: geoip database %s (%s) followed from this box's edge", cur.SHA256[:8], db.Source()) |
| 81 | } |
| 82 | |
| 83 | func (f *geoipFollower) drop(msg string) { |
| 84 | if _, _, loaded := f.sw.Loaded(); f.loadedSHA == "" && !loaded { |
| 85 | return |
| 86 | } |
| 87 | f.sw.Swap(nil) |
| 88 | f.loadedSHA = "" |
| 89 | if msg != "" { |
| 90 | log.Print(msg) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func (f *geoipFollower) logHourly(msg string) { |
| 95 | if now := f.now(); now.Sub(f.lastErrLog) >= time.Hour { |
| 96 | f.lastErrLog = now |
| 97 | log.Print(msg) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | // Run ticks once a minute, forever (process lifetime, like the console's |
| 102 | // other loops started from cmd/console). |
| 103 | func (f *geoipFollower) Run() { |
| 104 | f.Tick() |
| 105 | for range time.Tick(time.Minute) { |
| 106 | f.Tick() |
| 107 | } |
| 108 | } |
| 109 | |