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