carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package 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
8import (
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.
22var followerExpired = geoip.Expired
23
24type 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.
35func 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).
40func (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
83func (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
94func (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).
103func (f *geoipFollower) Run() {
104 f.Tick()
105 for range time.Tick(time.Minute) {
106 f.Tick()
107 }
108}
109