From 0d760f38ae890c43541f382202454621ea06fbd0 Mon Sep 17 00:00:00 2001
From: skidoodle <contact@albert.lol>
Date: Sat, 7 Sep 2024 03:21:10 +0200
Subject: [PATCH 1/4] maxmind

---
 .gitignore             |   1 +
 Dockerfile             |  20 +-
 db.go                  |  35 ++++
 docker-compose.dev.yml |  20 ++
 docker-compose.yml     |  13 +-
 iputils.go             | 176 +++++++++++++++++
 main.go                | 428 -----------------------------------------
 server.go              | 132 +++++++++++++
 8 files changed, 394 insertions(+), 431 deletions(-)
 create mode 100644 db.go
 create mode 100644 docker-compose.dev.yml
 create mode 100644 iputils.go
 create mode 100644 server.go

diff --git a/.gitignore b/.gitignore
index dca10b3..2de2e27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 *.mmdb
+.env**
diff --git a/Dockerfile b/Dockerfile
index 584431a..efa2c2a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,11 +4,27 @@ COPY go.mod go.sum ./
 RUN go mod download
 COPY . .
 RUN go build -o ipinfo .
+RUN go install github.com/maxmind/geoipupdate/v7/cmd/geoipupdate@latest
 RUN mkdir -p /build/data
 
 FROM alpine:latest
+RUN apk add --no-cache curl
 WORKDIR /app
 COPY --from=builder /build/ipinfo .
-COPY --from=builder /build/data /app/data
+COPY --from=builder /go/bin/geoipupdate /usr/local/bin/geoipupdate
+
+ENV GEOIPUPDATE_ACCOUNT_ID=${GEOIPUPDATE_ACCOUNT_ID}
+ENV GEOIPUPDATE_LICENSE_KEY=${GEOIPUPDATE_LICENSE_KEY}
+ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
+ENV GEOIPUPDATE_DB_DIR=/app
+
+RUN echo "AccountID ${GEOIPUPDATE_ACCOUNT_ID}" > /etc/GeoIP.conf && \
+    echo "LicenseKey ${GEOIPUPDATE_LICENSE_KEY}" >> /etc/GeoIP.conf && \
+    echo "EditionIDs ${GEOIPUPDATE_EDITION_IDS}" >> /etc/GeoIP.conf && \
+    echo "DatabaseDirectory ${GEOIPUPDATE_DB_DIR}" >> /etc/GeoIP.conf
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=5s CMD curl --fail http://localhost:3000/ || exit 1
+
 EXPOSE 3000
-CMD ["./ipinfo"]
+
+CMD ["sh", "-c", "geoipupdate && ./ipinfo"]
diff --git a/db.go b/db.go
new file mode 100644
index 0000000..cda1e84
--- /dev/null
+++ b/db.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+	"log"
+	"sync"
+
+	"github.com/oschwald/maxminddb-golang"
+)
+
+var cityDB *maxminddb.Reader
+var asnDB *maxminddb.Reader
+var dbMtx = new(sync.RWMutex)
+
+const (
+	cityDBPath = "./GeoLite2-City.mmdb"
+	asnDBPath  = "./GeoLite2-ASN.mmdb"
+)
+
+func initDatabases() {
+	var err error
+
+	cityDB, err = maxminddb.Open(cityDBPath)
+	if err != nil {
+		log.Fatalf("Error opening city database: %v", err)
+	}
+
+	asnDB, err = maxminddb.Open(asnDBPath)
+	if err != nil {
+		log.Fatalf("Error opening ASN database: %v", err)
+	}
+}
+
+func startUpdater() {
+	log.Println("MaxMind GeoIP databases will be updated by geoipupdate automatically.")
+}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..6da0029
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,20 @@
+version: '3.9'
+
+services:
+  ipinfo:
+    build: .
+    container_name: ipinfo
+    restart: unless-stopped
+    ports:
+      - "3000:3000"
+    environment:
+      GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
+      GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
+      GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN"
+      GEOIPUPDATE_DB_DIR: /app
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3000/"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+      start_period: 40s
diff --git a/docker-compose.yml b/docker-compose.yml
index d2e3fce..ccafcfc 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,4 @@
-version: '3.8'
+version: '3.9'
 
 services:
   ipinfo:
@@ -7,3 +7,14 @@ services:
     restart: unless-stopped
     ports:
       - "3000:3000"
+    environment:
+      GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
+      GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
+      GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN"
+      GEOIPUPDATE_DB_DIR: /app
+    healthcheck:
+      test: ["CMD", "curl", "-f", "http://localhost:3000/"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+      start_period: 40s
diff --git a/iputils.go b/iputils.go
new file mode 100644
index 0000000..27c6e73
--- /dev/null
+++ b/iputils.go
@@ -0,0 +1,176 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"regexp"
+	"strings"
+)
+
+var bogonNets = []*net.IPNet{
+	// IPv4
+	{IP: net.IPv4(0, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // "This" network
+	{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // Private-use networks
+	{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}, // Carrier-grade NAT
+	{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // Loopback
+	{IP: net.IPv4(127, 0, 53, 53), Mask: net.CIDRMask(32, 32)}, // Name collision occurrence
+	{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)}, // Link-local
+	{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // Private-use networks
+	{IP: net.IPv4(192, 0, 0, 0), Mask: net.CIDRMask(24, 32)}, // IETF protocol assignments
+	{IP: net.IPv4(192, 0, 2, 0), Mask: net.CIDRMask(24, 32)}, // TEST-NET-1
+	{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // Private-use networks
+	{IP: net.IPv4(198, 18, 0, 0), Mask: net.CIDRMask(15, 32)}, // Network interconnect device benchmark testing
+	{IP: net.IPv4(198, 51, 100, 0), Mask: net.CIDRMask(24, 32)}, // TEST-NET-2
+	{IP: net.IPv4(203, 0, 113, 0), Mask: net.CIDRMask(24, 32)}, // TEST-NET-3
+	{IP: net.IPv4(224, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // Multicast
+	{IP: net.IPv4(240, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // Reserved for future use
+	{IP: net.IPv4(255, 255, 255, 255), Mask: net.CIDRMask(32, 32)}, // Limited broadcast
+	// IPv6
+	{IP: net.ParseIP("::/128"), Mask: net.CIDRMask(128, 128)}, // Node-scope unicast unspecified address
+	{IP: net.ParseIP("::1/128"), Mask: net.CIDRMask(128, 128)}, // Node-scope unicast loopback address
+	{IP: net.ParseIP("::ffff:0:0/96"), Mask: net.CIDRMask(96, 128)}, // IPv4-mapped addresses
+	{IP: net.ParseIP("::/96"), Mask: net.CIDRMask(96, 128)}, // IPv4-compatible addresses
+	{IP: net.ParseIP("100::/64"), Mask: net.CIDRMask(64, 128)}, // Remotely triggered black hole addresses
+	{IP: net.ParseIP("2001:10::/28"), Mask: net.CIDRMask(28, 128)}, // Overlay routable cryptographic hash identifiers (ORCHID)
+	{IP: net.ParseIP("2001:db8::/32"), Mask: net.CIDRMask(32, 128)}, // Documentation prefix
+	{IP: net.ParseIP("fc00::/7"), Mask: net.CIDRMask(7, 128)}, // Unique local addresses (ULA)
+	{IP: net.ParseIP("fe80::/10"), Mask: net.CIDRMask(10, 128)}, // Link-local unicast
+	{IP: net.ParseIP("fec0::/10"), Mask: net.CIDRMask(10, 128)}, // Site-local unicast (deprecated)
+	{IP: net.ParseIP("ff00::/8"), Mask: net.CIDRMask(8, 128)}, // Multicast
+	// Additional Bogon Ranges
+	{IP: net.ParseIP("2002::/24"), Mask: net.CIDRMask(24, 128)}, // 6to4 bogon (0.0.0.0/8)
+	{IP: net.ParseIP("2002:a00::/24"), Mask: net.CIDRMask(24, 128)}, // 6to4 bogon (10.0.0.0/8)
+	{IP: net.ParseIP("2002:7f00::/24"), Mask: net.CIDRMask(24, 128)}, // 6to4 bogon (127.0.0.0/8)
+	{IP: net.ParseIP("2002:a9fe::/32"), Mask: net.CIDRMask(32, 128)}, // 6to4 bogon (169.254.0.0/16)
+	{IP: net.ParseIP("2002:ac10::/28"), Mask: net.CIDRMask(28, 128)}, // 6to4 bogon (172.16.0.0/12)
+	{IP: net.ParseIP("2002:c000::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (192.0.0.0/24)
+	{IP: net.ParseIP("2002:c000:200::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (192.0.2.0/24)
+	{IP: net.ParseIP("2002:c0a8::/32"), Mask: net.CIDRMask(32, 128)}, // 6to4 bogon (192.168.0.0/16)
+	{IP: net.ParseIP("2002:c612::/31"), Mask: net.CIDRMask(31, 128)}, // 6to4 bogon (198.18.0.0/15)
+	{IP: net.ParseIP("2002:c633:6400::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (198.51.100.0/24)
+	{IP: net.ParseIP("2002:cb00:7100::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (203.0.113.0/24)
+	{IP: net.ParseIP("2002:e000::/20"), Mask: net.CIDRMask(20, 128)}, // 6to4 bogon (224.0.0.0/4)
+	{IP: net.ParseIP("2002:f000::/20"), Mask: net.CIDRMask(20, 128)}, // 6to4 bogon (240.0.0.0/4)
+	{IP: net.ParseIP("2002:ffff:ffff::/48"), Mask: net.CIDRMask(48, 128)}, // 6to4 bogon (255.255.255.255/32)
+	{IP: net.ParseIP("2001::/40"), Mask: net.CIDRMask(40, 128)}, // Teredo bogon (0.0.0.0/8)
+	{IP: net.ParseIP("2001:0:a00::/40"), Mask: net.CIDRMask(40, 128)}, // Teredo bogon (10.0.0.0/8)
+	{IP: net.ParseIP("2001:0:7f00::/40"), Mask: net.CIDRMask(40, 128)}, // Teredo bogon (127.0.0.0/8)
+	{IP: net.ParseIP("2001:0:a9fe::/48"), Mask: net.CIDRMask(48, 128)}, // Teredo bogon (169.254.0.0/16)
+	{IP: net.ParseIP("2001:0:ac10::/44"), Mask: net.CIDRMask(44, 128)}, // Teredo bogon (172.16.0.0/12)
+	{IP: net.ParseIP("2001:0:c000::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (192.0.0.0/24)
+	{IP: net.ParseIP("2001:0:c000:200::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (192.0.2.0/24)
+	{IP: net.ParseIP("2001:0:c0a8::/48"), Mask: net.CIDRMask(48, 128)}, // Teredo bogon (192.168.0.0/16)
+	{IP: net.ParseIP("2001:0:c612::/47"), Mask: net.CIDRMask(47, 128)}, // Teredo bogon (198.18.0.0/15)
+	{IP: net.ParseIP("2001:0:c633:6400::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (198.51.100.0/24)
+	{IP: net.ParseIP("2001:0:cb00:7100::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (203.0.113.0/24)
+	{IP: net.ParseIP("2001:0:e000::/36"), Mask: net.CIDRMask(36, 128)}, // Teredo bogon (224.0.0.0/4)
+	{IP: net.ParseIP("2001:0:f000::/36"), Mask: net.CIDRMask(36, 128)}, // Teredo bogon (240.0.0.0/4)
+	{IP: net.ParseIP("2001:0:ffff:ffff::/64"), Mask: net.CIDRMask(64, 128)}, // Teredo bogon (255.255.255.255/32)
+}
+
+// Check if the IP is a bogon IP
+func isBogon(ip net.IP) bool {
+	for _, net := range bogonNets {
+		if net.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
+
+// Get the real IP address from the request headers
+func getRealIP(r *http.Request) string {
+	if realIP := r.Header.Get("CF-Connecting-IP"); realIP != "" {
+		return realIP
+	} else if realIP := r.Header.Get("X-Forwarded-For"); realIP != "" {
+		return strings.Split(realIP, ",")[0]
+	} else {
+		return extractIP(r.RemoteAddr)
+	}
+}
+
+// Lookup IP data in the databases
+func lookupIPData(ip net.IP) *dataStruct {
+	dbMtx.RLock()
+	defer dbMtx.RUnlock()
+
+	var cityRecord struct {
+		City struct {
+			Names map[string]string `maxminddb:"names"`
+		} `maxminddb:"city"`
+		Subdivisions []struct {
+			Names map[string]string `maxminddb:"names"`
+		} `maxminddb:"subdivisions"`
+		Country struct {
+			IsoCode string            `maxminddb:"iso_code"`
+			Names   map[string]string `maxminddb:"names"`
+		} `maxminddb:"country"`
+		Continent struct {
+			Code  string            `maxminddb:"code"`
+			Names map[string]string `maxminddb:"names"`
+		} `maxminddb:"continent"`
+		Location struct {
+			Latitude  float64 `maxminddb:"latitude"`
+			Longitude float64 `maxminddb:"longitude"`
+		} `maxminddb:"location"`
+	}
+	err := cityDB.Lookup(ip, &cityRecord)
+	if err != nil {
+		log.Printf("Error looking up city data: %v\n", err)
+		return nil
+	}
+
+	var asnRecord struct {
+		AutonomousSystemNumber       uint   `maxminddb:"autonomous_system_number"`
+		AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
+	}
+	err = asnDB.Lookup(ip, &asnRecord)
+	if err != nil {
+		log.Printf("Error looking up ASN data: %v\n", err)
+		return nil
+	}
+
+	hostname, err := net.LookupAddr(ip.String())
+	if err != nil || len(hostname) == 0 {
+		hostname = []string{""}
+	}
+
+	var sd *string
+	if len(cityRecord.Subdivisions) > 0 {
+		name := cityRecord.Subdivisions[0].Names["en"]
+		sd = &name
+	}
+
+	return &dataStruct{
+		IP:        toPtr(ip.String()),
+		Hostname:  toPtr(strings.TrimSuffix(hostname[0], ".")),
+		ASN:       toPtr(fmt.Sprintf("%d", asnRecord.AutonomousSystemNumber)),
+		Org:       toPtr(asnRecord.AutonomousSystemOrganization),
+		City:      toPtr(cityRecord.City.Names["en"]),
+		Region:    sd,
+		Country:   toPtr(cityRecord.Country.Names["en"]),
+		Continent: toPtr(cityRecord.Continent.Names["en"]),
+		Loc:       toPtr(fmt.Sprintf("%.4f,%.4f", cityRecord.Location.Latitude, cityRecord.Location.Longitude)),
+	}
+}
+
+// Convert string to pointer
+func toPtr(s string) *string {
+	if s == "" {
+		return nil
+	}
+	return &s
+}
+
+// Validate JSONP callback name
+var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`)
+
+// Extract the IP address from a string, removing unwanted characters
+func extractIP(ip string) string {
+	ip = strings.ReplaceAll(ip, "[", "")
+	ip = strings.ReplaceAll(ip, "]", "")
+	ss := strings.Split(ip, ":")
+	return strings.Join(ss[:len(ss)-1], ":")
+}
diff --git a/main.go b/main.go
index ac7f6f1..40698e4 100644
--- a/main.go
+++ b/main.go
@@ -1,435 +1,7 @@
 package main
 
-import (
-	"compress/gzip"
-	"encoding/json"
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"net/http"
-	"os"
-	"regexp"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/oschwald/maxminddb-golang"
-)
-
-var cityDB *maxminddb.Reader
-var asnDB *maxminddb.Reader
-var dbMtx = new(sync.RWMutex)
-
-var (
-	currCityFilename = time.Now().Format("2006-01") + "-city.mmdb"
-	currASNFilename  = time.Now().Format("2006-01") + "-asn.mmdb"
-)
-
-const (
-	cityDBURL = "https://download.db-ip.com/free/dbip-city-lite-%s.mmdb.gz"
-	asnDBURL  = "https://download.db-ip.com/free/dbip-asn-lite-%s.mmdb.gz"
-)
-
 func main() {
 	initDatabases()
 	go startUpdater()
 	startServer()
 }
-
-func initDatabases() {
-	var err error
-
-	cityDB, err = maxminddb.Open(currCityFilename)
-	if err != nil {
-		if os.IsNotExist(err) {
-			currCityFilename = ""
-			doUpdate()
-			if cityDB == nil {
-				log.Fatalf("Failed to initialize city database: %v", err)
-			}
-		} else {
-			log.Fatalf("Error opening city database: %v", err)
-		}
-	}
-
-	asnDB, err = maxminddb.Open(currASNFilename)
-	if err != nil {
-		if os.IsNotExist(err) {
-			currASNFilename = ""
-			doUpdate()
-			if asnDB == nil {
-				log.Fatalf("Failed to initialize ASN database: %v", err)
-			}
-		} else {
-			log.Fatalf("Error opening ASN database: %v", err)
-		}
-	}
-}
-
-func startUpdater() {
-	for range time.Tick(time.Hour * 24 * 7) {
-		doUpdate()
-	}
-}
-
-func startServer() {
-	log.Println("Server listening on :3000")
-	http.HandleFunc("/", handler)
-	log.Fatal(http.ListenAndServe(":3000", nil))
-}
-
-func doUpdate() {
-	log.Println("Fetching updates...")
-	currMonth := time.Now().Format("2006-01")
-	newCityFilename := currMonth + "-city.mmdb"
-	newASNFilename := currMonth + "-asn.mmdb"
-
-	updateDatabase(fmt.Sprintf(cityDBURL, currMonth), newCityFilename, func(newDB *maxminddb.Reader) {
-		dbMtx.Lock()
-		defer dbMtx.Unlock()
-		if cityDB != nil {
-			cityDB.Close()
-		}
-		cityDB = newDB
-		currCityFilename = newCityFilename
-		log.Printf("City GeoIP database updated to %s\n", currMonth)
-	})
-
-	updateDatabase(fmt.Sprintf(asnDBURL, currMonth), newASNFilename, func(newDB *maxminddb.Reader) {
-		dbMtx.Lock()
-		defer dbMtx.Unlock()
-		if asnDB != nil {
-			asnDB.Close()
-		}
-		asnDB = newDB
-		currASNFilename = newASNFilename
-		log.Printf("ASN GeoIP database updated to %s\n", currMonth)
-	})
-}
-
-func updateDatabase(url, dstFilename string, updateFunc func(*maxminddb.Reader)) {
-	resp, err := http.Get(url)
-	if err != nil {
-		log.Printf("Error fetching the updated DB from %s: %v\n", url, err)
-		return
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusOK {
-		log.Printf("Non-200 status code (%d) from %s, retry later...\n", resp.StatusCode, url)
-		return
-	}
-
-	dst, err := os.Create(dstFilename)
-	if err != nil {
-		log.Printf("Error creating file %s: %v\n", dstFilename, err)
-		return
-	}
-	defer dst.Close()
-
-	r, err := gzip.NewReader(resp.Body)
-	if err != nil {
-		log.Printf("Error creating gzip reader: %v\n", err)
-		return
-	}
-	defer r.Close()
-
-	log.Println("Copying new database...")
-	if _, err = io.Copy(dst, r); err != nil {
-		log.Printf("Error copying file: %v\n", err)
-		return
-	}
-
-	newDB, err := maxminddb.Open(dstFilename)
-	if err != nil {
-		log.Printf("Error opening new DB: %v\n", err)
-		return
-	}
-
-	updateFunc(newDB)
-}
-
-var invalidIPBytes = []byte("Please provide a valid IP address.")
-
-// Struct to hold IP data
-type dataStruct struct {
-	IP            *string `json:"ip"`
-	Hostname      *string `json:"hostname"`
-	ASN           *string `json:"asn"`
-	Org           *string `json:"org"`
-	City          *string `json:"city"`
-	Region        *string `json:"region"`
-	Country       *string `json:"country"`
-	Continent     *string `json:"continent"`
-	Loc           *string `json:"loc"`
-}
-
-type bogonDataStruct struct {
-	IP    string `json:"ip"`
-	Bogon bool   `json:"bogon"`
-}
-
-// List of bogon IP networks
-var bogonNets = []*net.IPNet{
-	// IPv4
-	{IP: net.IPv4(0, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // "This" network
-	{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // Private-use networks
-	{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}, // Carrier-grade NAT
-	{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // Loopback
-	{IP: net.IPv4(127, 0, 53, 53), Mask: net.CIDRMask(32, 32)}, // Name collision occurrence
-	{IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32)}, // Link-local
-	{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // Private-use networks
-	{IP: net.IPv4(192, 0, 0, 0), Mask: net.CIDRMask(24, 32)}, // IETF protocol assignments
-	{IP: net.IPv4(192, 0, 2, 0), Mask: net.CIDRMask(24, 32)}, // TEST-NET-1
-	{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // Private-use networks
-	{IP: net.IPv4(198, 18, 0, 0), Mask: net.CIDRMask(15, 32)}, // Network interconnect device benchmark testing
-	{IP: net.IPv4(198, 51, 100, 0), Mask: net.CIDRMask(24, 32)}, // TEST-NET-2
-	{IP: net.IPv4(203, 0, 113, 0), Mask: net.CIDRMask(24, 32)}, // TEST-NET-3
-	{IP: net.IPv4(224, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // Multicast
-	{IP: net.IPv4(240, 0, 0, 0), Mask: net.CIDRMask(4, 32)}, // Reserved for future use
-	{IP: net.IPv4(255, 255, 255, 255), Mask: net.CIDRMask(32, 32)}, // Limited broadcast
-	// IPv6
-	{IP: net.ParseIP("::/128"), Mask: net.CIDRMask(128, 128)}, // Node-scope unicast unspecified address
-	{IP: net.ParseIP("::1/128"), Mask: net.CIDRMask(128, 128)}, // Node-scope unicast loopback address
-	{IP: net.ParseIP("::ffff:0:0/96"), Mask: net.CIDRMask(96, 128)}, // IPv4-mapped addresses
-	{IP: net.ParseIP("::/96"), Mask: net.CIDRMask(96, 128)}, // IPv4-compatible addresses
-	{IP: net.ParseIP("100::/64"), Mask: net.CIDRMask(64, 128)}, // Remotely triggered black hole addresses
-	{IP: net.ParseIP("2001:10::/28"), Mask: net.CIDRMask(28, 128)}, // Overlay routable cryptographic hash identifiers (ORCHID)
-	{IP: net.ParseIP("2001:db8::/32"), Mask: net.CIDRMask(32, 128)}, // Documentation prefix
-	{IP: net.ParseIP("fc00::/7"), Mask: net.CIDRMask(7, 128)}, // Unique local addresses (ULA)
-	{IP: net.ParseIP("fe80::/10"), Mask: net.CIDRMask(10, 128)}, // Link-local unicast
-	{IP: net.ParseIP("fec0::/10"), Mask: net.CIDRMask(10, 128)}, // Site-local unicast (deprecated)
-	{IP: net.ParseIP("ff00::/8"), Mask: net.CIDRMask(8, 128)}, // Multicast
-	// Additional Bogon Ranges
-	{IP: net.ParseIP("2002::/24"), Mask: net.CIDRMask(24, 128)}, // 6to4 bogon (0.0.0.0/8)
-	{IP: net.ParseIP("2002:a00::/24"), Mask: net.CIDRMask(24, 128)}, // 6to4 bogon (10.0.0.0/8)
-	{IP: net.ParseIP("2002:7f00::/24"), Mask: net.CIDRMask(24, 128)}, // 6to4 bogon (127.0.0.0/8)
-	{IP: net.ParseIP("2002:a9fe::/32"), Mask: net.CIDRMask(32, 128)}, // 6to4 bogon (169.254.0.0/16)
-	{IP: net.ParseIP("2002:ac10::/28"), Mask: net.CIDRMask(28, 128)}, // 6to4 bogon (172.16.0.0/12)
-	{IP: net.ParseIP("2002:c000::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (192.0.0.0/24)
-	{IP: net.ParseIP("2002:c000:200::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (192.0.2.0/24)
-	{IP: net.ParseIP("2002:c0a8::/32"), Mask: net.CIDRMask(32, 128)}, // 6to4 bogon (192.168.0.0/16)
-	{IP: net.ParseIP("2002:c612::/31"), Mask: net.CIDRMask(31, 128)}, // 6to4 bogon (198.18.0.0/15)
-	{IP: net.ParseIP("2002:c633:6400::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (198.51.100.0/24)
-	{IP: net.ParseIP("2002:cb00:7100::/40"), Mask: net.CIDRMask(40, 128)}, // 6to4 bogon (203.0.113.0/24)
-	{IP: net.ParseIP("2002:e000::/20"), Mask: net.CIDRMask(20, 128)}, // 6to4 bogon (224.0.0.0/4)
-	{IP: net.ParseIP("2002:f000::/20"), Mask: net.CIDRMask(20, 128)}, // 6to4 bogon (240.0.0.0/4)
-	{IP: net.ParseIP("2002:ffff:ffff::/48"), Mask: net.CIDRMask(48, 128)}, // 6to4 bogon (255.255.255.255/32)
-	{IP: net.ParseIP("2001::/40"), Mask: net.CIDRMask(40, 128)}, // Teredo bogon (0.0.0.0/8)
-	{IP: net.ParseIP("2001:0:a00::/40"), Mask: net.CIDRMask(40, 128)}, // Teredo bogon (10.0.0.0/8)
-	{IP: net.ParseIP("2001:0:7f00::/40"), Mask: net.CIDRMask(40, 128)}, // Teredo bogon (127.0.0.0/8)
-	{IP: net.ParseIP("2001:0:a9fe::/48"), Mask: net.CIDRMask(48, 128)}, // Teredo bogon (169.254.0.0/16)
-	{IP: net.ParseIP("2001:0:ac10::/44"), Mask: net.CIDRMask(44, 128)}, // Teredo bogon (172.16.0.0/12)
-	{IP: net.ParseIP("2001:0:c000::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (192.0.0.0/24)
-	{IP: net.ParseIP("2001:0:c000:200::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (192.0.2.0/24)
-	{IP: net.ParseIP("2001:0:c0a8::/48"), Mask: net.CIDRMask(48, 128)}, // Teredo bogon (192.168.0.0/16)
-	{IP: net.ParseIP("2001:0:c612::/47"), Mask: net.CIDRMask(47, 128)}, // Teredo bogon (198.18.0.0/15)
-	{IP: net.ParseIP("2001:0:c633:6400::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (198.51.100.0/24)
-	{IP: net.ParseIP("2001:0:cb00:7100::/56"), Mask: net.CIDRMask(56, 128)}, // Teredo bogon (203.0.113.0/24)
-	{IP: net.ParseIP("2001:0:e000::/36"), Mask: net.CIDRMask(36, 128)}, // Teredo bogon (224.0.0.0/4)
-	{IP: net.ParseIP("2001:0:f000::/36"), Mask: net.CIDRMask(36, 128)}, // Teredo bogon (240.0.0.0/4)
-	{IP: net.ParseIP("2001:0:ffff:ffff::/64"), Mask: net.CIDRMask(64, 128)}, // Teredo bogon (255.255.255.255/32)
-}
-
-// Check if the IP is a bogon IP
-func isBogon(ip net.IP) bool {
-	for _, net := range bogonNets {
-		if net.Contains(ip) {
-			return true
-		}
-	}
-	return false
-}
-
-// HTTP handler
-func handler(w http.ResponseWriter, r *http.Request) {
-	requestedThings := strings.Split(r.URL.Path, "/")
-
-	var IPAddress, field string
-	if len(requestedThings) > 1 && net.ParseIP(requestedThings[1]) != nil {
-		IPAddress = requestedThings[1]
-		if len(requestedThings) > 2 {
-			field = requestedThings[2]
-		}
-	} else if len(requestedThings) > 1 {
-		field = requestedThings[1]
-	}
-
-	if IPAddress == "" || IPAddress == "self" {
-		IPAddress = getRealIP(r)
-	}
-	ip := net.ParseIP(IPAddress)
-	if ip == nil {
-		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-		w.Write(invalidIPBytes)
-		return
-	}
-
-	// Check if the IP is a bogon
-	if isBogon(ip) {
-		w.Header().Set("Content-Type", "application/json; charset=utf-8")
-		bogonData := bogonDataStruct{
-			IP:    ip.String(),
-			Bogon: true,
-		}
-		json.NewEncoder(w).Encode(bogonData)
-		return
-	}
-
-	data := lookupIPData(ip)
-	if data == nil {
-		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-		w.Write(invalidIPBytes)
-		return
-	}
-
-	if field != "" {
-		value := getField(data, field)
-		if value != nil {
-			w.Header().Set("Content-Type", "application/json; charset=utf-8")
-			json.NewEncoder(w).Encode(map[string]*string{field: value})
-			return
-		} else {
-			w.Header().Set("Content-Type", "application/json; charset=utf-8")
-			json.NewEncoder(w).Encode(map[string]*string{field: nil})
-			return
-		}
-	}
-
-	w.Header().Set("Content-Type", "application/json; charset=utf-8")
-	callback := r.URL.Query().Get("callback")
-	enableJSONP := callback != "" && len(callback) < 2000 && callbackJSONP.MatchString(callback)
-	if enableJSONP {
-		jsonData, _ := json.MarshalIndent(data, "", "  ")
-		response := fmt.Sprintf("/**/ typeof %s === 'function' && %s(%s);", callback, callback, jsonData)
-		w.Write([]byte(response))
-	} else {
-		enc := json.NewEncoder(w)
-		enc.SetIndent("", "  ")
-		if r.URL.Query().Get("compact") == "true" {
-			enc.SetIndent("", "")
-		}
-		enc.Encode(data)
-	}
-}
-
-// Get specific field from dataStruct
-func getField(data *dataStruct, field string) *string {
-	switch field {
-	case "ip":
-		return data.IP
-	case "hostname":
-		return data.Hostname
-	case "asn":
-		return data.ASN
-	case "org":
-		return data.Org
-	case "city":
-		return data.City
-	case "region":
-		return data.Region
-	case "country":
-		return data.Country
-	case "continent":
-		return data.Continent
-	case "loc":
-		return data.Loc
-	default:
-		return nil
-	}
-}
-
-// Get the real IP address from the request headers
-func getRealIP(r *http.Request) string {
-	if realIP := r.Header.Get("CF-Connecting-IP"); realIP != "" {
-		return realIP
-	} else if realIP := r.Header.Get("X-Forwarded-For"); realIP != "" {
-		return strings.Split(realIP, ",")[0]
-	} else {
-		return extractIP(r.RemoteAddr)
-	}
-}
-
-// Lookup IP data in the databases
-func lookupIPData(ip net.IP) *dataStruct {
-	dbMtx.RLock()
-	defer dbMtx.RUnlock()
-
-	var cityRecord struct {
-		City struct {
-			Names map[string]string `maxminddb:"names"`
-		} `maxminddb:"city"`
-		Subdivisions []struct {
-			Names map[string]string `maxminddb:"names"`
-		} `maxminddb:"subdivisions"`
-		Country struct {
-			IsoCode string            `maxminddb:"iso_code"`
-			Names   map[string]string `maxminddb:"names"`
-		} `maxminddb:"country"`
-		Continent struct {
-			Code  string            `maxminddb:"code"`
-			Names map[string]string `maxminddb:"names"`
-		} `maxminddb:"continent"`
-		Location struct {
-			Latitude  float64 `maxminddb:"latitude"`
-			Longitude float64 `maxminddb:"longitude"`
-		} `maxminddb:"location"`
-	}
-	err := cityDB.Lookup(ip, &cityRecord)
-	if err != nil {
-		log.Printf("Error looking up city data: %v\n", err)
-		return nil
-	}
-
-	var asnRecord struct {
-		AutonomousSystemNumber       uint   `maxminddb:"autonomous_system_number"`
-		AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
-	}
-	err = asnDB.Lookup(ip, &asnRecord)
-	if err != nil {
-		log.Printf("Error looking up ASN data: %v\n", err)
-		return nil
-	}
-
-	hostname, err := net.LookupAddr(ip.String())
-	if err != nil || len(hostname) == 0 {
-		hostname = []string{""}
-	}
-
-	var sd *string
-	if len(cityRecord.Subdivisions) > 0 {
-		name := cityRecord.Subdivisions[0].Names["en"]
-		sd = &name
-	}
-
-	return &dataStruct{
-		IP:            toPtr(ip.String()),
-		Hostname:      toPtr(strings.TrimSuffix(hostname[0], ".")),
-		ASN:           toPtr(fmt.Sprintf("%d", asnRecord.AutonomousSystemNumber)),
-		Org:           toPtr(asnRecord.AutonomousSystemOrganization),
-		City:          toPtr(cityRecord.City.Names["en"]),
-		Region:        sd,
-		Country:       toPtr(cityRecord.Country.Names["en"]),
-		Continent:     toPtr(cityRecord.Continent.Names["en"]),
-		Loc:           toPtr(fmt.Sprintf("%.4f,%.4f", cityRecord.Location.Latitude, cityRecord.Location.Longitude)),
-	}
-}
-
-// Convert string to pointer
-func toPtr(s string) *string {
-	if s == "" {
-		return nil
-	}
-	return &s
-}
-
-// Validate JSONP callback name
-var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`)
-
-// Extract the IP address from a string, removing unwanted characters
-func extractIP(ip string) string {
-	ip = strings.ReplaceAll(ip, "[", "")
-	ip = strings.ReplaceAll(ip, "]", "")
-	ss := strings.Split(ip, ":")
-	return strings.Join(ss[:len(ss)-1], ":")
-}
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..ec004e7
--- /dev/null
+++ b/server.go
@@ -0,0 +1,132 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"strings"
+)
+
+var invalidIPBytes = []byte("Please provide a valid IP address.")
+
+// Struct to hold IP data
+type dataStruct struct {
+	IP        *string `json:"ip"`
+	Hostname  *string `json:"hostname"`
+	ASN       *string `json:"asn"`
+	Org       *string `json:"org"`
+	City      *string `json:"city"`
+	Region    *string `json:"region"`
+	Country   *string `json:"country"`
+	Continent *string `json:"continent"`
+	Loc       *string `json:"loc"`
+}
+
+type bogonDataStruct struct {
+	IP    string `json:"ip"`
+	Bogon bool   `json:"bogon"`
+}
+
+func startServer() {
+	log.Println("Server listening on :3000")
+	http.HandleFunc("/", handler)
+	log.Fatal(http.ListenAndServe(":3000", nil))
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	requestedThings := strings.Split(r.URL.Path, "/")
+
+	var IPAddress, field string
+	if len(requestedThings) > 1 && net.ParseIP(requestedThings[1]) != nil {
+		IPAddress = requestedThings[1]
+		if len(requestedThings) > 2 {
+			field = requestedThings[2]
+		}
+	} else if len(requestedThings) > 1 {
+		field = requestedThings[1]
+	}
+
+	if IPAddress == "" || IPAddress == "self" {
+		IPAddress = getRealIP(r)
+	}
+	ip := net.ParseIP(IPAddress)
+	if ip == nil {
+		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+		w.Write(invalidIPBytes)
+		return
+	}
+
+	if isBogon(ip) {
+		w.Header().Set("Content-Type", "application/json; charset=utf-8")
+		bogonData := bogonDataStruct{
+			IP:    ip.String(),
+			Bogon: true,
+		}
+		json.NewEncoder(w).Encode(bogonData)
+		return
+	}
+
+	data := lookupIPData(ip)
+	if data == nil {
+		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+		w.Write(invalidIPBytes)
+		return
+	}
+
+	if field != "" {
+		value := getField(data, field)
+		if value != nil {
+			w.Header().Set("Content-Type", "application/json; charset=utf-8")
+			json.NewEncoder(w).Encode(map[string]*string{field: value})
+			return
+		} else {
+			w.Header().Set("Content-Type", "application/json; charset=utf-8")
+			json.NewEncoder(w).Encode(map[string]*string{field: nil})
+			return
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	callback := r.URL.Query().Get("callback")
+	enableJSONP := callback != "" && len(callback) < 2000 && callbackJSONP.MatchString(callback)
+	if enableJSONP {
+		jsonData, _ := json.MarshalIndent(data, "", "  ")
+		response := fmt.Sprintf("/**/ typeof %s === 'function' && %s(%s);", callback, callback, jsonData)
+		w.Write([]byte(response))
+	} else {
+		enc := json.NewEncoder(w)
+		enc.SetIndent("", "  ")
+		if r.URL.Query().Get("compact") == "true" {
+			enc.SetIndent("", "")
+		}
+		enc.Encode(data)
+	}
+}
+
+// Get specific field from dataStruct
+func getField(data *dataStruct, field string) *string {
+	switch field {
+	case "ip":
+		return data.IP
+	case "hostname":
+		return data.Hostname
+	case "asn":
+		return data.ASN
+	case "org":
+		return data.Org
+	case "city":
+		return data.City
+	case "region":
+		return data.Region
+	case "country":
+		return data.Country
+	case "continent":
+		return data.Continent
+	case "loc":
+		return data.Loc
+	default:
+		return nil
+	}
+}

From 7e14ec4d76de7928f06b42fa1458fdd772d28928 Mon Sep 17 00:00:00 2001
From: skidoodle <contact@albert.lol>
Date: Sat, 7 Sep 2024 03:22:37 +0200
Subject: [PATCH 2/4] action

---
 .github/workflows/docker-publish.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index f0a9d5c..5d9be79 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -2,7 +2,7 @@ name: Docker
 
 on:
   push:
-    branches: [ "main" ]
+    branches: [ "testing" ]
 
 env:
   REGISTRY: ghcr.io

From d4ac1c0dd8ac28a1f54e6e53c689e3013acf2ac6 Mon Sep 17 00:00:00 2001
From: skidoodle <contact@albert.lol>
Date: Sat, 7 Sep 2024 18:59:47 +0200
Subject: [PATCH 3/4] test updates

---
 Dockerfile             | 6 +++++-
 docker-compose.dev.yml | 2 --
 docker-compose.yml     | 2 --
 iputils.go             | 2 ++
 server.go              | 3 +++
 5 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index efa2c2a..4e7c097 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,8 @@ RUN go install github.com/maxmind/geoipupdate/v7/cmd/geoipupdate@latest
 RUN mkdir -p /build/data
 
 FROM alpine:latest
-RUN apk add --no-cache curl
+RUN apk add --no-cache curl tzdata busybox-suid
+
 WORKDIR /app
 COPY --from=builder /build/ipinfo .
 COPY --from=builder /go/bin/geoipupdate /usr/local/bin/geoipupdate
@@ -23,6 +24,9 @@ RUN echo "AccountID ${GEOIPUPDATE_ACCOUNT_ID}" > /etc/GeoIP.conf && \
     echo "EditionIDs ${GEOIPUPDATE_EDITION_IDS}" >> /etc/GeoIP.conf && \
     echo "DatabaseDirectory ${GEOIPUPDATE_DB_DIR}" >> /etc/GeoIP.conf
 
+RUN echo "0 0 * * * geoipupdate >> /var/log/geoipupdate.log 2>&1" > /etc/crontabs/root
+RUN cat /etc/crontabs/root
+
 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s CMD curl --fail http://localhost:3000/ || exit 1
 
 EXPOSE 3000
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 6da0029..ef73764 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,5 +1,3 @@
-version: '3.9'
-
 services:
   ipinfo:
     build: .
diff --git a/docker-compose.yml b/docker-compose.yml
index ccafcfc..bacf78a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.9'
-
 services:
   ipinfo:
     image: ghcr.io/skidoodle/ipinfo:main
diff --git a/iputils.go b/iputils.go
index 27c6e73..10c42c2 100644
--- a/iputils.go
+++ b/iputils.go
@@ -114,6 +114,7 @@ func lookupIPData(ip net.IP) *dataStruct {
 		Location struct {
 			Latitude  float64 `maxminddb:"latitude"`
 			Longitude float64 `maxminddb:"longitude"`
+			Timezone  string `maxminddb:"time_zone"`
 		} `maxminddb:"location"`
 	}
 	err := cityDB.Lookup(ip, &cityRecord)
@@ -152,6 +153,7 @@ func lookupIPData(ip net.IP) *dataStruct {
 		Region:    sd,
 		Country:   toPtr(cityRecord.Country.Names["en"]),
 		Continent: toPtr(cityRecord.Continent.Names["en"]),
+		Timezone:  toPtr(cityRecord.Location.Timezone),
 		Loc:       toPtr(fmt.Sprintf("%.4f,%.4f", cityRecord.Location.Latitude, cityRecord.Location.Longitude)),
 	}
 }
diff --git a/server.go b/server.go
index ec004e7..239604e 100644
--- a/server.go
+++ b/server.go
@@ -21,6 +21,7 @@ type dataStruct struct {
 	Region    *string `json:"region"`
 	Country   *string `json:"country"`
 	Continent *string `json:"continent"`
+	Timezone  *string `json:"timezone"`
 	Loc       *string `json:"loc"`
 }
 
@@ -124,6 +125,8 @@ func getField(data *dataStruct, field string) *string {
 		return data.Country
 	case "continent":
 		return data.Continent
+	case "timezone":
+		return data.Timezone
 	case "loc":
 		return data.Loc
 	default:

From d9e21d04f07666ab439a082a65d359994e424ecd Mon Sep 17 00:00:00 2001
From: skidoodle <contact@albert.lol>
Date: Sat, 7 Sep 2024 19:14:47 +0200
Subject: [PATCH 4/4] cache

---
 .github/workflows/go.yml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 3851826..b7a491a 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -18,6 +18,14 @@ jobs:
       with:
         go-version: '1.22.4'
 
+    - name: Cache Go Modules
+      uses: actions/cache@v3
+      with:
+        path: ~/go/pkg/mod
+        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        restore-keys: |
+          ${{ runner.os }}-go-
+
     - name: Build
       run: go build -v ./...