carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package geoip
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 "time"
8)
9
10// FeedSpec is one supported feed (spec §10.1): the env name, the database
11// type it must deliver, and its provider.
12type FeedSpec struct{ Name, Type, Provider string }
13
14var feeds = map[string]FeedSpec{
15 "dbip-city-lite": {"dbip-city-lite", "DBIP-City-Lite", "dbip"},
16 "dbip-country-lite": {"dbip-country-lite", "DBIP-Country-Lite", "dbip"},
17 "geolite2-city": {"geolite2-city", "GeoLite2-City", "maxmind"},
18 "geolite2-country": {"geolite2-country", "GeoLite2-Country", "maxmind"},
19 // The paid upgrade (Paul, 2026-10-07): MaxMind's commercial databases,
20 // the same download API and credentials. Not GeoLite2, so no D1 expiry,
21 // and no attribution the platform knows of: the operator's MaxMind
22 // licence governs.
23 "geoip2-city": {"geoip2-city", "GeoIP2-City", "maxmind"},
24 "geoip2-country": {"geoip2-country", "GeoIP2-Country", "maxmind"},
25}
26
27// ParseFeed resolves CONSOLE_GEOIP_FEED.
28func ParseFeed(name string) (FeedSpec, bool) {
29 s, ok := feeds[strings.TrimSpace(name)]
30 return s, ok
31}
32
33// DBIPURL is the month's DB-IP Lite download (spec §10.2).
34func DBIPURL(base string, spec FeedSpec, month time.Time) string {
35 if base == "" {
36 base = "https://download.db-ip.com"
37 }
38 kind := strings.TrimSuffix(strings.TrimPrefix(spec.Name, "dbip-"), "-lite") // city | country
39 return fmt.Sprintf("%s/free/dbip-%s-lite-%s.mmdb.gz", base, kind, month.UTC().Format("2006-01"))
40}
41
42// MaxMindURL is the GeoLite2 permalink; suffix is "tar.gz" or "tar.gz.sha256".
43func MaxMindURL(base string, spec FeedSpec, suffix string) string {
44 if base == "" {
45 base = "https://download.maxmind.com"
46 }
47 return fmt.Sprintf("%s/geoip/databases/%s/download?suffix=%s", base, spec.Type, suffix)
48}
49
50// FeedStatus is control/geoip/feed.json, written by the console after every
51// check (spec §10.4). Box names the console's box: the one box whose mirror
52// raises the feed reason, so it fires once.
53type FeedStatus struct {
54 V int `json:"v"`
55 Feed string `json:"feed"`
56 Box string `json:"box"`
57 LastCheck time.Time `json:"last_check"`
58 LastSuccess time.Time `json:"last_success,omitempty"`
59 LastEdition string `json:"last_edition,omitempty"`
60 ConsecutiveFailures int `json:"consecutive_failures"`
61 LastFailure string `json:"last_failure,omitempty"` // a fixed category, never an error text
62 ConsecutiveNotFound int `json:"consecutive_not_found"`
63}
64
65// FeedNotFoundAlert is how many failing checks in a row raise the alert.
66const FeedNotFoundAlert = 7
67
68// ParseFeedStatus decodes feed.json.
69func ParseFeedStatus(raw []byte) (FeedStatus, error) {
70 var s FeedStatus
71 if err := json.Unmarshal(raw, &s); err != nil {
72 return FeedStatus{}, fmt.Errorf("geoip feed status: %w", err)
73 }
74 if s.V != 1 {
75 return FeedStatus{}, fmt.Errorf("geoip feed status: version %d: %w", s.V, ErrUnknownVersion)
76 }
77 return s, nil
78}
79
80// Failing is the alert's feed reason: 7 failed or not-found checks in a row.
81func (s FeedStatus) Failing() bool {
82 return s.ConsecutiveFailures >= FeedNotFoundAlert || s.ConsecutiveNotFound >= FeedNotFoundAlert
83}
84