diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index f0a9d5c..041fd21 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -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 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 72e3776..1615822 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 ./... diff --git a/Dockerfile b/Dockerfile index 6c8031d..2a02d54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/go.mod b/go.mod index 21be2b2..38589cf 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 027367d..e55c807 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/common/lookup.go b/internal/common/lookup.go index c99d99d..9d93289 100644 --- a/internal/common/lookup.go +++ b/internal/common/lookup.go @@ -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 { diff --git a/internal/common/whois.go b/internal/common/whois.go index 9013039..a69c3c5 100644 --- a/internal/common/whois.go +++ b/internal/common/whois.go @@ -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. diff --git a/internal/db/manager.go b/internal/db/manager.go index b4186ca..02d4f7f 100644 --- a/internal/db/manager.go +++ b/internal/db/manager.go @@ -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 { diff --git a/internal/db/types.go b/internal/db/types.go index 56c1a99..a1edb15 100644 --- a/internal/db/types.go +++ b/internal/db/types.go @@ -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 diff --git a/internal/db/updater.go b/internal/db/updater.go index ab9b33d..6d74ee7 100644 --- a/internal/db/updater.go +++ b/internal/db/updater.go @@ -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 } diff --git a/internal/db/utils.go b/internal/db/utils.go deleted file mode 100644 index 928051d..0000000 --- a/internal/db/utils.go +++ /dev/null @@ -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 -} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 07a8e93..bb8b875 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -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/ or /AS.", 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] diff --git a/readme.md b/readme.md index 7defe49..07dee6f 100644 --- a/readme.md +++ b/readme.md @@ -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 ```