This commit is contained in:
2026-02-03 02:23:23 +01:00
parent cc0a3e38ac
commit d8755b7f07
13 changed files with 196 additions and 228 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ name: Docker
on:
push:
branches: [ "main" ]
branches: ["main"]
env:
REGISTRY: ghcr.io
@@ -24,7 +24,7 @@ jobs:
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
with:
cosign-release: 'v2.1.1'
cosign-release: "v2.1.1"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+18 -19
View File
@@ -2,32 +2,31 @@ name: Go
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25.1'
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.25.6"
- 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: 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 ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
- name: Test
run: go test -v ./...
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.25.1-alpine AS builder
FROM golang:1.25.6-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
+11 -14
View File
@@ -1,24 +1,21 @@
module ipinfo
go 1.25.1
go 1.25.6
require (
github.com/joho/godotenv v1.5.1
github.com/likexian/whois v1.15.6
github.com/likexian/whois-parser v1.24.20
github.com/miekg/dns v1.1.68
github.com/likexian/whois v1.15.7
github.com/likexian/whois-parser v1.24.21
github.com/miekg/dns v1.1.72
github.com/oschwald/maxminddb-golang v1.13.1
golang.org/x/net v0.44.0
golang.org/x/net v0.49.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/likexian/gokit v0.25.15 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
github.com/likexian/gokit v0.25.16 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/tools v0.40.0 // indirect
)
+26 -26
View File
@@ -1,34 +1,34 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/likexian/gokit v0.25.15 h1:QjospM1eXhdMMHwZRpMKKAHY/Wig9wgcREmLtf9NslY=
github.com/likexian/gokit v0.25.15/go.mod h1:S2QisdsxLEHWeD/XI0QMVeggp+jbxYqUxMvSBil7MRg=
github.com/likexian/whois v1.15.6 h1:hizngFHJTNQDlhwhU+FEGyPGxy8bRnf25gHDNrSB4Ag=
github.com/likexian/whois v1.15.6/go.mod h1:vx3kt3sZ4mx4XFgpaNp3GXQCZQIzAoyrUAkRtJwoM2I=
github.com/likexian/whois-parser v1.24.20 h1:oxEkRi0GxgqWQRLDMJpXU1EhgWmLmkqEFZ2ChXTeQLE=
github.com/likexian/whois-parser v1.24.20/go.mod h1:rAtaofg2luol09H+ogDzGIfcG8ig1NtM5R16uQADDz4=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/likexian/gokit v0.25.16 h1:wwBeUIN/OdoPp6t00xTnZE8Di/+s969Bl5N2Kw6bzP8=
github.com/likexian/gokit v0.25.16/go.mod h1:Wqd4f+iifV0qxA1N3MqePJTUsmRy/lpst9/yXriDx/4=
github.com/likexian/whois v1.15.7 h1:sajjDhi2bVD71AHJhjV7jLYxN92H4AWhTwxM8hmj7c0=
github.com/likexian/whois v1.15.7/go.mod h1:kdPQtYb+7SQVftBEbCblDadUkycN7Mg1k1/Li/rwvmc=
github.com/likexian/whois-parser v1.24.21 h1:MxsrGRxDOiZIVp7q7N/yAIbKuN4QAkGjCpOtTDA5OsM=
github.com/likexian/whois-parser v1.24.21/go.mod h1:o3DUruO65Pb8WXCJCTlSVkTbwuYVrBCeoMTw2q0mxY4=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+6 -7
View File
@@ -10,7 +10,7 @@ import (
"ipinfo/internal/db"
"github.com/likexian/whois-parser"
whoisparser "github.com/likexian/whois-parser"
"github.com/miekg/dns"
"golang.org/x/net/publicsuffix"
)
@@ -95,7 +95,6 @@ func LookupASNData(geoIP *db.GeoIPManager, targetASN uint) (*ASNDataResponse, er
var ipv4Prefixes, ipv6Prefixes []string
for _, prefix := range prefixes {
// Filter out bogon prefixes before adding them to the list.
if !IsBogon(prefix.IP) {
prefixStr := prefix.String()
if strings.Contains(prefixStr, ":") {
@@ -130,13 +129,13 @@ func queryDns(domain string, recordType uint16) ([]dns.RR, error) {
m.SetQuestion(dns.Fqdn(domain), recordType)
m.RecursionDesired = true
r, _, err := c.Exchange(m, "1.1.1.1:53") // Using Cloudflare's public resolver
r, _, err := c.Exchange(m, "1.1.1.1:53")
if err != nil {
return nil, err
}
if r.Rcode != dns.RcodeSuccess {
return nil, nil // No error, just no records found
return nil, nil
}
return r.Answer, nil
@@ -154,10 +153,10 @@ func LookupDomainData(domain string) (*DomainDataResponse, error) {
}
whoisRaw, err := performWhoisWithFallback(eTLD)
var whoisResult interface{}
var whoisResult any
if err != nil {
slog.Error("whois lookup failed after fallback", "domain", eTLD, "err", err)
whoisResult = fmt.Sprintf("whois lookup failed: %v", err)
slog.Error("whois lookup failed completely", "domain", eTLD, "err", err)
whoisResult = nil
} else {
parsed, parseErr := whoisparser.Parse(whoisRaw)
if parseErr != nil {
+41 -26
View File
@@ -10,42 +10,57 @@ import (
"time"
"github.com/likexian/whois"
"github.com/likexian/whois-parser"
whoisparser "github.com/likexian/whois-parser"
)
// performWhoisWithFallback attempts a WHOIS query and falls back to IPv4 if it suspects an IPv6 issue.
// performWhoisWithFallback attempts a WHOIS query and falls back to manual lookup if the default fails.
func performWhoisWithFallback(domain string) (string, error) {
result, err := whois.Whois(domain)
c := whois.NewClient()
c.SetTimeout(5 * time.Second)
result, err := c.Whois(domain)
if err == nil {
return result, nil
}
if strings.Contains(err.Error(), "dial tcp [") && strings.Contains(err.Error(), "]:43") {
slog.Warn("whois failed with potential ipv6 issue, falling back to ipv4", "domain", domain, "err", err)
slog.Warn("standard whois lookup failed, attempting fallback", "domain", domain, "err", err)
serverHost, serverErr := getWhoisServerForDomain(domain)
if serverErr != nil {
slog.Error("could not find whois server during fallback", "domain", domain, "err", serverErr)
return "", err
}
ips, resolveErr := net.LookupIP(serverHost)
if resolveErr != nil {
slog.Error("could not resolve whois server hostname during fallback", "server", serverHost, "err", resolveErr)
return "", err
}
for _, ip := range ips {
if ip.To4() != nil {
ipv4Server := ip.String()
slog.Info("retrying whois query with explicit ipv4 address", "domain", domain, "server", ipv4Server)
return queryWhoisServer(domain, ipv4Server)
}
}
slog.Warn("no ipv4 address found for whois server during fallback", "server", serverHost)
serverHost, serverErr := getWhoisServerForDomain(domain)
if serverErr != nil {
slog.Error("could not find whois server during fallback", "domain", domain, "err", serverErr)
return "", err
}
return "", err
ips, resolveErr := net.LookupIP(serverHost)
if resolveErr != nil {
slog.Error("could not resolve whois server hostname during fallback", "server", serverHost, "err", resolveErr)
return "", err
}
for _, ip := range ips {
if ip.To4() != nil {
ipv4Server := ip.String()
slog.Info("retrying whois query with explicit ipv4 address", "domain", domain, "server", ipv4Server)
res, err := queryWhoisServer(domain, ipv4Server)
if err == nil {
return res, nil
}
slog.Warn("fallback query to ipv4 server failed", "server", ipv4Server, "err", err)
}
}
for _, ip := range ips {
if ip.To4() == nil {
ipv6Server := ip.String()
slog.Info("retrying whois query with ipv6 address", "domain", domain, "server", ipv6Server)
res, err := queryWhoisServer(domain, ipv6Server)
if err == nil {
return res, nil
}
}
}
return "", fmt.Errorf("all whois attempts failed: %w", err)
}
// getWhoisServerForDomain finds the authoritative WHOIS server for a domain by querying IANA.
+16 -32
View File
@@ -1,12 +1,10 @@
package db
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"sync"
"time"
@@ -25,7 +23,7 @@ type GeoIPManager struct {
// NewGeoIPManager creates a new GeoIPManager
func NewGeoIPManager() (*GeoIPManager, error) {
manager := &GeoIPManager{
httpClient: &http.Client{Timeout: 2 * time.Minute},
httpClient: &http.Client{Timeout: 5 * time.Minute},
}
if err := manager.Initialize(); err != nil {
return nil, fmt.Errorf("initializing geoip manager: %w", err)
@@ -33,19 +31,24 @@ func NewGeoIPManager() (*GeoIPManager, error) {
return manager, nil
}
// Initialize initializes the GeoIPManager by opening the database files
// Initialize initializes the GeoIPManager by opening the database files.
func (g *GeoIPManager) Initialize() error {
g.mu.Lock()
defer g.mu.Unlock()
cityErr := g.openDB(CityDBPath)
asnErr := g.openDB(ASNDBPath)
g.mu.Unlock()
if err := g.openDB(CityDBPath); err != nil {
return err
}
if err := g.openDB(ASNDBPath); err != nil {
return err
if cityErr != nil || asnErr != nil {
slog.Info("databases missing or invalid, performing initial update")
if err := g.UpdateDatabases(); err != nil {
return fmt.Errorf("initial update failed: %w", err)
}
} else {
g.mu.Lock()
g.buildASNPrefixMap()
g.mu.Unlock()
}
g.buildASNPrefixMap()
return nil
}
@@ -65,30 +68,11 @@ func (g *GeoIPManager) Close() {
}
}
// openDB opens a MaxMind DB file, downloading it if it doesn't exist.
// openDB opens a MaxMind DB file.
func (g *GeoIPManager) openDB(path string) error {
db, err := maxminddb.Open(path)
if err == nil {
if path == CityDBPath {
g.cityDB = db
} else {
g.asnDB = db
}
return nil
}
if !os.IsNotExist(err) {
return fmt.Errorf("%w: failed to open %s: %v", ErrDatabaseOpen, path, err)
}
slog.Warn("database not found, attempting initial download", "path", path)
if err := g.DownloadDatabases(context.Background()); err != nil {
return fmt.Errorf("%w: %v", ErrDownloadFailed, err)
}
db, err = maxminddb.Open(path)
if err != nil {
return fmt.Errorf("%w: failed to open %s after download: %v", ErrDatabaseOpen, path, err)
return err
}
if path == CityDBPath {
+2 -2
View File
@@ -4,8 +4,8 @@ import "errors"
// Constants for database names and paths
const (
CityDBName = "GeoLite2-City"
ASNDBName = "GeoLite2-ASN"
CityDBName = "dbip-city-lite"
ASNDBName = "dbip-asn-lite"
DBExtension = ".mmdb"
CityDBPath = CityDBName + DBExtension
ASNDBPath = ASNDBName + DBExtension
+57 -48
View File
@@ -8,7 +8,6 @@ import (
"log/slog"
"net/http"
"os"
"strings"
"time"
"github.com/oschwald/maxminddb-golang"
@@ -37,7 +36,8 @@ func (g *GeoIPManager) StartUpdater(ctx context.Context, updateInterval time.Dur
// UpdateDatabases downloads new databases and reloads them into the manager.
func (g *GeoIPManager) UpdateDatabases() error {
if err := g.DownloadDatabases(context.Background()); err != nil {
tmpFiles, err := g.downloadToTemp(context.Background())
if err != nil {
return err
}
@@ -46,68 +46,80 @@ func (g *GeoIPManager) UpdateDatabases() error {
if g.cityDB != nil {
_ = g.cityDB.Close()
g.cityDB = nil
}
if g.asnDB != nil {
_ = g.asnDB.Close()
g.asnDB = nil
}
for targetPath, tmpPath := range tmpFiles {
if err := os.Rename(tmpPath, targetPath); err != nil {
slog.Error("failed to replace database file", "target", targetPath, "tmp", tmpPath, "err", err)
}
}
var openErr error
g.cityDB, openErr = maxminddb.Open(CityDBPath)
if openErr != nil {
return fmt.Errorf("reopening city database: %w", openErr)
slog.Error("failed to reopen city database", "err", openErr)
}
g.asnDB, openErr = maxminddb.Open(ASNDBPath)
if openErr != nil {
return fmt.Errorf("reopening asn database: %w", openErr)
slog.Error("failed to reopen asn database", "err", openErr)
}
g.buildASNPrefixMap()
slog.Info("successfully reloaded databases")
slog.Info("successfully updated and reloaded databases")
return nil
}
// DownloadDatabases downloads all configured GeoIP database editions.
func (g *GeoIPManager) DownloadDatabases(ctx context.Context) error {
accountID := os.Getenv("GEOIPUPDATE_ACCOUNT_ID")
licenseKey := os.Getenv("GEOIPUPDATE_LICENSE_KEY")
if accountID == "" || licenseKey == "" {
return fmt.Errorf("GEOIPUPDATE_ACCOUNT_ID and GEOIPUPDATE_LICENSE_KEY must be set")
}
editionIDs := os.Getenv("GEOIPUPDATE_EDITION_IDS")
if editionIDs == "" {
editionIDs = "GeoLite2-City GeoLite2-ASN"
// downloadToTemp downloads the current month's DB-IP databases to temporary files.
func (g *GeoIPManager) downloadToTemp(ctx context.Context) (map[string]string, error) {
now := time.Now()
dateStr := now.Format("2006-01")
targets := map[string]string{
CityDBPath: fmt.Sprintf("dbip-city-lite-%s", dateStr),
ASNDBPath: fmt.Sprintf("dbip-asn-lite-%s", dateStr),
}
results := make(map[string]string)
var firstError error
for _, editionID := range strings.Fields(editionIDs) {
if err := g.downloadEdition(ctx, accountID, licenseKey, editionID); err != nil {
slog.Error("failed to download edition", "edition", editionID, "err", err)
for localPath, urlName := range targets {
downloadURL := fmt.Sprintf("https://download.db-ip.com/free/%s.mmdb.gz", urlName)
tmpPath := localPath + ".tmp"
if err := g.downloadFile(ctx, downloadURL, tmpPath); err != nil {
slog.Error("failed to download database", "url", downloadURL, "err", err)
if firstError == nil {
firstError = err
}
continue
}
results[localPath] = tmpPath
}
return firstError
if firstError != nil {
for _, tmp := range results {
_ = os.Remove(tmp)
}
return nil, firstError
}
return results, nil
}
// downloadEdition downloads a specific GeoIP database edition.
func (g *GeoIPManager) downloadEdition(ctx context.Context, accountID, licenseKey, editionID string) error {
dbPath := editionID + DBExtension
slog.Info("checking for updates", "database", dbPath)
// downloadFile downloads a file from a URL, decompresses it, and saves it to destPath.
func (g *GeoIPManager) downloadFile(ctx context.Context, url, destPath string) error {
slog.Info("checking for updates", "url", url)
hash, err := fileMD5(dbPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("could not calculate md5 for %s: %w", dbPath, err)
}
downloadURL := fmt.Sprintf("https://updates.maxmind.com/geoip/databases/%s/update?db_md5=%s", editionID, hash)
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("could not create request: %w", err)
}
req.SetBasicAuth(accountID, licenseKey)
resp, err := g.httpClient.Do(req)
if err != nil {
@@ -119,16 +131,11 @@ func (g *GeoIPManager) downloadEdition(ctx context.Context, accountID, licenseKe
}
}()
if resp.StatusCode == http.StatusNotModified {
slog.Info("database is already up to date", "database", dbPath)
return nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("received non-200 status code: %d - %s", resp.StatusCode, string(body))
return fmt.Errorf("received non-200 status code: %d", resp.StatusCode)
}
slog.Info("downloading and decompressing new version", "database", dbPath)
slog.Info("downloading and decompressing", "destination", destPath)
gzr, err := gzip.NewReader(resp.Body)
if err != nil {
@@ -140,26 +147,28 @@ func (g *GeoIPManager) downloadEdition(ctx context.Context, accountID, licenseKe
}
}()
tmpPath := dbPath + ".tmp"
outFile, err := os.Create(tmpPath)
outFile, err := os.Create(destPath)
if err != nil {
return fmt.Errorf("could not create temporary file: %w", err)
}
defer func() {
closeFile := func() error {
if err := outFile.Close(); err != nil {
slog.Error("failed to close output file", "err", err)
return fmt.Errorf("failed to close output file: %w", err)
}
}()
return nil
}
if _, err := io.Copy(outFile, gzr); err != nil {
_ = os.Remove(tmpPath)
_ = closeFile()
_ = os.Remove(destPath)
return fmt.Errorf("could not decompress and write db file: %w", err)
}
if err := os.Rename(tmpPath, dbPath); err != nil {
return fmt.Errorf("could not replace database file: %w", err)
if err := closeFile(); err != nil {
return err
}
slog.Info("successfully downloaded and updated", "database", dbPath)
slog.Info("successfully downloaded", "file", destPath)
return nil
}
-27
View File
@@ -1,27 +0,0 @@
package db
import (
"crypto/md5"
"fmt"
"io"
"os"
)
// fileMD5 calculates the MD5 hash of a file.
func fileMD5(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer func() {
if err := file.Close(); err != nil {
fmt.Printf("Error closing file: %v\n", err)
}
}()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}
+11 -12
View File
@@ -47,18 +47,17 @@ func handleDomainLookup(w http.ResponseWriter, _ *http.Request, domain string) {
// handleASNLookup handles ASN lookup requests.
func handleASNLookup(w http.ResponseWriter, _ *http.Request, path string, geoIP *db.GeoIPManager) {
var asnStr string
lowerPath := strings.ToLower(path)
upperPath := strings.ToUpper(path)
cleanPath := path
if strings.HasPrefix(lowerPath, "asn/") {
asnStr = path[4:]
} else if strings.HasPrefix(lowerPath, "as") {
asnStr = path[2:]
} else {
sendJSONError(w, "Invalid ASN query format. Use /asn/<number> or /AS<number>.", http.StatusBadRequest)
return
if strings.HasPrefix(upperPath, "ASN") {
cleanPath = path[3:]
} else if strings.HasPrefix(upperPath, "AS") {
cleanPath = path[2:]
}
asnStr := strings.Trim(cleanPath, "/ ")
asn, err := strconv.ParseUint(asnStr, 10, 32)
if err != nil || asn == 0 {
sendJSONError(w, "Invalid ASN: must be a positive number.", http.StatusBadRequest)
@@ -86,12 +85,12 @@ func handleIPLookup(w http.ResponseWriter, r *http.Request, path string, geoIP *
switch len(parts) {
case 0:
ipAddress = GetRealIP(r) // No more "common." prefix
ipAddress = GetRealIP(r)
case 1:
if parts[0] == "" {
ipAddress = GetRealIP(r) // No more "common." prefix
ipAddress = GetRealIP(r)
} else if _, ok := fieldMap[parts[0]]; ok {
ipAddress = GetRealIP(r) // No more "common." prefix
ipAddress = GetRealIP(r)
field = parts[0]
} else {
ipAddress = parts[0]
+5 -12
View File
@@ -1,6 +1,6 @@
# ipinfo
`ipinfo` is a powerful and efficient IP information service written in Go. It fetches GeoIP data to provide detailed information about an IP address, including geographical location, ASN, and related network details. The service automatically updates its GeoIP databases to ensure accuracy and reliability.
`ipinfo` is a powerful and efficient IP information service written in Go. It fetches GeoIP data to provide detailed information about an IP address, including geographical location, ASN, and related network details. The service automatically updates its GeoIP databases (provided by DB-IP) to ensure accuracy and reliability.
## Features
@@ -9,7 +9,7 @@
- **Hostname Lookup**: Retrieves the hostname associated with the IP address.
- **Domain WHOIS**: Fetches structured WHOIS data for any domain.
- **Domain DNS Records**: Retrieves common DNS records (A, AAAA, CNAME, MX, TXT, NS).
- **Automatic Database Updates**: Keeps GeoIP databases up-to-date daily.
- **Automatic Database Updates**: Keeps GeoIP databases up-to-date monthly.
## Example Endpoints
@@ -41,6 +41,7 @@ $ curl https://ip.albert.lol/9.9.9.9/city
```
### Get details about an ASN
```sh
$ curl https://ip.albert.lol/AS19281
{
@@ -75,6 +76,7 @@ $ curl https://ip.albert.lol/AS19281
```
### Get WHOIS and DNS records for a domain
```sh
$ curl https://ip.albert.lol/example.com
{
@@ -121,11 +123,7 @@ $ curl https://ip.albert.lol/example.com
git clone https://github.com/skidoodle/ipinfo
cd ipinfo
docker build -t ipinfo:main .
docker run \
-p 3000:3000
-e GEOIPUPDATE_ACCOUNT_ID=${GEOIPUPDATE_ACCOUNT_ID} \
-e GEOIPUPDATE_LICENSE_KEY=${GEOIPUPDATE_LICENSE_KEY} \
ipinfo:main
docker run -p 3000:3000 ipinfo:main
```
### Without Docker
@@ -148,9 +146,6 @@ services:
restart: unless-stopped
ports:
- "3000:3000"
environment:
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
```
### Docker Run
@@ -161,8 +156,6 @@ docker run \
--name=ipinfo \
--restart=unless-stopped \
-p 3000:3000 \
-e GEOIPUPDATE_ACCOUNT_ID=${GEOIPUPDATE_ACCOUNT_ID} \
-e GEOIPUPDATE_LICENSE_KEY=${GEOIPUPDATE_LICENSE_KEY} \
ghcr.io/skidoodle/ipinfo:main
```