commit 228b37f9fd60b5cbce3a3b264718fd60afe9346f Author: skidoodle Date: Thu Jun 13 11:43:30 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dca10b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.mmdb diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..584431a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:alpine AS builder +WORKDIR /build +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o ipinfo . +RUN mkdir -p /build/data + +FROM alpine:latest +WORKDIR /app +COPY --from=builder /build/ipinfo . +COPY --from=builder /build/data /app/data +EXPOSE 3000 +CMD ["./ipinfo"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7d04131 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3' + +services: + ipzxqco: + image: skidoodle/ipinfo:main + build: . + restart: unless-stopped + volumes: + - data:/app/data + ports: + - 3000:3000 + +volumes: + data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fa19869 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/skidoodle/ipinfo + +go 1.22.4 + +require github.com/oschwald/maxminddb-golang v1.6.0 + +require golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fee63ce --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= +github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..bec4f18 --- /dev/null +++ b/main.go @@ -0,0 +1,301 @@ +package main + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "io" + "log" + "net" + "net/http" + "os" + "regexp" + "strings" + "sync" + "time" + + "github.com/oschwald/maxminddb-golang" +) + +// GeoIP database readers +var cityDB *maxminddb.Reader +var asnDB *maxminddb.Reader + +var ( + currCityFilename = time.Now().Format("2006-01") + "-city.mmdb" + currASNFilename = time.Now().Format("2006-01") + "-asn.mmdb" + dbMtx = new(sync.RWMutex) +) + +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" +) + +// Fetch and update the GeoIP databases. +func doUpdate() { + fmt.Fprintln(os.Stderr, "Fetching updates...") + currMonth := time.Now().Format("2006-01") + newCityFilename := currMonth + "-city.mmdb" + newASNFilename := currMonth + "-asn.mmdb" + + updateDatabase(cityDBURL, newCityFilename, func(newDB *maxminddb.Reader) { + dbMtx.Lock() + defer dbMtx.Unlock() + if cityDB != nil { + cityDB.Close() + } + cityDB = newDB + currCityFilename = newCityFilename + fmt.Fprintf(os.Stderr, "City GeoIP database updated to %s\n", currMonth) + }) + + updateDatabase(asnDBURL, newASNFilename, func(newDB *maxminddb.Reader) { + dbMtx.Lock() + defer dbMtx.Unlock() + if asnDB != nil { + asnDB.Close() + } + asnDB = newDB + currASNFilename = newASNFilename + fmt.Fprintf(os.Stderr, "ASN GeoIP database updated to %s\n", currMonth) + }) +} + +// Download and update the database file. +func updateDatabase(urlTemplate, dstFilename string, updateFunc func(*maxminddb.Reader)) { + resp, err := http.Get(fmt.Sprintf(urlTemplate, time.Now().Format("2006-01"))) + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching the updated DB: %v\n", err) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + fmt.Fprintf(os.Stderr, "Non-200 status code (%d), retry later...\n", resp.StatusCode) + return + } + + dst, err := os.Create(dstFilename) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating file: %v\n", err) + return + } + defer dst.Close() + + r, err := gzip.NewReader(resp.Body) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating gzip reader: %v\n", err) + return + } + defer r.Close() + + fmt.Fprintln(os.Stderr, "Copying new database...") + if _, err = io.Copy(dst, r); err != nil { + fmt.Fprintf(os.Stderr, "Error copying file: %v\n", err) + return + } + + newDB, err := maxminddb.Open(dstFilename) + if err != nil { + fmt.Fprintf(os.Stderr, "Error opening new DB: %v\n", err) + return + } + + updateFunc(newDB) +} + +// Periodically update the GeoIP databases every week. +func updater() { + for range time.Tick(time.Hour * 24 * 7) { + doUpdate() + } +} + +func main() { + var err error + cityDB, err = maxminddb.Open(currCityFilename) + if err != nil { + if os.IsNotExist(err) { + currCityFilename = "" + doUpdate() + if cityDB == nil { + os.Exit(1) + } + } else { + log.Fatal(err) + } + } + + asnDB, err = maxminddb.Open(currASNFilename) + if err != nil { + if os.IsNotExist(err) { + currASNFilename = "" + doUpdate() + if asnDB == nil { + os.Exit(1) + } + } else { + log.Fatal(err) + } + } + + go updater() + + log.Println("Server listening on :3000") + http.ListenAndServe(":3000", http.HandlerFunc(handler)) +} + +var invalidIPBytes = []byte("Please provide a valid IP address.") + +type dataStruct struct { + IP string `json:"ip"` + Hostname string `json:"hostname"` + ASN string `json:"asn"` + Organization string `json:"organization"` + City string `json:"city"` + Region string `json:"region"` + Country string `json:"country"` + CountryFull string `json:"country_full"` + Continent string `json:"continent"` + ContinentFull string `json:"continent_full"` + Loc string `json:"loc"` +} + +var nameToField = map[string]func(dataStruct) string{ + "ip": func(d dataStruct) string { return d.IP }, + "hostname": func(d dataStruct) string { return d.Hostname }, + "asn": func(d dataStruct) string { return d.ASN }, + "organization": func(d dataStruct) string { return d.Organization }, + "city": func(d dataStruct) string { return d.City }, + "region": func(d dataStruct) string { return d.Region }, + "country": func(d dataStruct) string { return d.Country }, + "country_full": func(d dataStruct) string { return d.CountryFull }, + "continent": func(d dataStruct) string { return d.Continent }, + "continent_full": func(d dataStruct) string { return d.ContinentFull }, + "loc": func(d dataStruct) string { return d.Loc }, +} + +func handler(w http.ResponseWriter, r *http.Request) { + requestedThings := strings.Split(r.URL.Path, "/") + + var IPAddress, Which string + switch len(requestedThings) { + case 3: + Which = requestedThings[2] + fallthrough + case 2: + IPAddress = requestedThings[1] + } + + if IPAddress == "" || IPAddress == "self" { + if realIP, ok := r.Header["X-Real-Ip"]; ok && len(realIP) > 0 { + IPAddress = realIP[0] + } else { + IPAddress = extractIP(r.RemoteAddr) + } + } + + ip := net.ParseIP(IPAddress) + if ip == nil { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write(invalidIPBytes) + return + } + + dbMtx.RLock() + var cityRecord struct { + Country struct { + IsoCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"country"` + City struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"city"` + Subdivisions []struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"subdivisions"` + 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) + dbMtx.RUnlock() + if err != nil { + log.Fatal(err) + } + + dbMtx.RLock() + var asnRecord struct { + AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"` + AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` + } + err = asnDB.Lookup(ip, &asnRecord) + dbMtx.RUnlock() + if err != nil { + log.Fatal(err) + } + + hostname, err := net.LookupAddr(ip.String()) + if err != nil || len(hostname) == 0 { + hostname = []string{""} + } + + var sd string + if len(cityRecord.Subdivisions) > 0 { + sd = cityRecord.Subdivisions[0].Names["en"] + } + + d := dataStruct{ + IP: ip.String(), + Hostname: hostname[0], + ASN: fmt.Sprintf("%d", asnRecord.AutonomousSystemNumber), + Organization: asnRecord.AutonomousSystemOrganization, + Country: cityRecord.Country.IsoCode, + CountryFull: cityRecord.Country.Names["en"], + City: cityRecord.City.Names["en"], + Region: sd, + Continent: cityRecord.Continent.Code, + ContinentFull: cityRecord.Continent.Names["en"], + Loc: fmt.Sprintf("%.4f,%.4f", cityRecord.Location.Latitude, cityRecord.Location.Longitude), + } + + if Which == "" { + 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 { + w.Write([]byte(fmt.Sprintf("/**/ typeof %s === 'function' && %s(", callback, callback))) + } + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if r.URL.Query().Get("compact") == "true" { + enc.SetIndent("", "") + } + enc.Encode(d) + if enableJSONP { + w.Write([]byte(");")) + } + } else { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + if val := nameToField[Which]; val != nil { + w.Write([]byte(val(d))) + } else { + w.Write([]byte("undefined")) + } + } +} + +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], ":") +}