mirror of
https://github.com/skidoodle/ipinfo.git
synced 2025-02-15 08:29:17 +01:00
Initial commit
This commit is contained in:
commit
228b37f9fd
6 changed files with 347 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.mmdb
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
@ -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"]
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
@ -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:
|
7
go.mod
Normal file
7
go.mod
Normal file
|
@ -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
|
10
go.sum
Normal file
10
go.sum
Normal file
|
@ -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=
|
301
main.go
Normal file
301
main.go
Normal file
|
@ -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], ":")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue