mirror of
https://github.com/skidoodle/ipinfo.git
synced 2026-04-28 09:27:35 +02:00
256 lines
6.6 KiB
Go
256 lines
6.6 KiB
Go
package common
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"ipinfo/internal/db"
|
|
|
|
whoisparser "github.com/likexian/whois-parser"
|
|
"github.com/miekg/dns"
|
|
"github.com/ringsaturn/tzf"
|
|
"golang.org/x/net/publicsuffix"
|
|
)
|
|
|
|
var tzFinder tzf.F
|
|
|
|
func init() {
|
|
var err error
|
|
tzFinder, err = tzf.NewDefaultFinder()
|
|
if err != nil {
|
|
slog.Error("failed to initialize timezone finder", "err", err)
|
|
}
|
|
}
|
|
|
|
// LookupIPData looks up IP data in the databases with caching.
|
|
func LookupIPData(geoIP *db.GeoIPManager, ip net.IP) *DataStruct {
|
|
ipStr := ip.String()
|
|
if data, found := cache.Get(ipStr); found {
|
|
return data.(*DataStruct)
|
|
}
|
|
|
|
var cityRecord struct {
|
|
City struct {
|
|
Names map[string]string `maxminddb:"names"`
|
|
} `maxminddb:"city"`
|
|
Subdivisions []struct {
|
|
Names map[string]string `maxminddb:"names"`
|
|
} `maxminddb:"subdivisions"`
|
|
Country struct {
|
|
IsoCode string `maxminddb:"iso_code"`
|
|
} `maxminddb:"country"`
|
|
Location struct {
|
|
Latitude float64 `maxminddb:"latitude"`
|
|
Longitude float64 `maxminddb:"longitude"`
|
|
} `maxminddb:"location"`
|
|
}
|
|
|
|
if err := geoIP.GetCityDB().Lookup(ip, &cityRecord); err != nil {
|
|
slog.Error("failed to look up city data", "err", err)
|
|
return nil
|
|
}
|
|
|
|
var asnRecord db.ASNRecord
|
|
if err := geoIP.GetASNDB().Lookup(ip, &asnRecord); err != nil {
|
|
slog.Error("failed to look up asn data", "err", err)
|
|
return nil
|
|
}
|
|
|
|
hostname, _ := net.LookupAddr(ipStr)
|
|
hostnameStr := ""
|
|
if len(hostname) > 0 {
|
|
hostnameStr = strings.TrimSuffix(hostname[0], ".")
|
|
}
|
|
|
|
var region *string
|
|
if len(cityRecord.Subdivisions) > 0 {
|
|
region = ToPtr(cityRecord.Subdivisions[0].Names["en"])
|
|
}
|
|
|
|
var timezone string
|
|
if tzFinder != nil && (cityRecord.Location.Latitude != 0 || cityRecord.Location.Longitude != 0) {
|
|
timezone = tzFinder.GetTimezoneName(cityRecord.Location.Longitude, cityRecord.Location.Latitude)
|
|
}
|
|
|
|
data := &DataStruct{
|
|
IP: ToPtr(ipStr),
|
|
Hostname: ToPtr(hostnameStr),
|
|
Org: ToPtr(fmt.Sprintf("AS%d %s", asnRecord.AutonomousSystemNumber, asnRecord.AutonomousSystemOrganization)),
|
|
City: ToPtr(cityRecord.City.Names["en"]),
|
|
Region: region,
|
|
Country: ToPtr(cityRecord.Country.IsoCode),
|
|
Timezone: ToPtr(timezone),
|
|
Loc: ToPtr(fmt.Sprintf("%.4f,%.4f", cityRecord.Location.Latitude, cityRecord.Location.Longitude)),
|
|
}
|
|
|
|
cache.Set(ipStr, data)
|
|
return data
|
|
}
|
|
|
|
// LookupASNData looks up ASN data in the databases with caching.
|
|
func LookupASNData(geoIP *db.GeoIPManager, targetASN uint) (*ASNDataResponse, error) {
|
|
if data, found := cache.Get(targetASN); found {
|
|
return data.(*ASNDataResponse), nil
|
|
}
|
|
|
|
prefixes := geoIP.GetASNPrefixes(targetASN)
|
|
if len(prefixes) == 0 {
|
|
return nil, fmt.Errorf("no prefixes found for as%d in the database", targetASN)
|
|
}
|
|
|
|
var orgName string
|
|
var record db.ASNRecord
|
|
if err := geoIP.GetASNDB().Lookup(prefixes[0].IP, &record); err == nil {
|
|
orgName = record.AutonomousSystemOrganization
|
|
}
|
|
|
|
var ipv4Prefixes, ipv6Prefixes []string
|
|
for _, prefix := range prefixes {
|
|
if !IsBogon(prefix.IP) {
|
|
prefixStr := prefix.String()
|
|
if strings.Contains(prefixStr, ":") {
|
|
ipv6Prefixes = append(ipv6Prefixes, prefixStr)
|
|
} else {
|
|
ipv4Prefixes = append(ipv4Prefixes, prefixStr)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(ipv4Prefixes)
|
|
sort.Strings(ipv6Prefixes)
|
|
|
|
response := &ASNDataResponse{
|
|
Details: ASNDetails{
|
|
ASN: targetASN,
|
|
Name: orgName,
|
|
},
|
|
Prefixes: ASNPrefixInfo{
|
|
IPv4: ipv4Prefixes,
|
|
IPv6: ipv6Prefixes,
|
|
},
|
|
}
|
|
|
|
cache.Set(targetASN, response)
|
|
return response, nil
|
|
}
|
|
|
|
// queryDns performs a DNS query for a specific type against a public resolver.
|
|
func queryDns(domain string, recordType uint16) ([]dns.RR, error) {
|
|
c := new(dns.Client)
|
|
m := new(dns.Msg)
|
|
m.SetQuestion(dns.Fqdn(domain), recordType)
|
|
m.RecursionDesired = true
|
|
|
|
r, _, err := c.Exchange(m, "1.1.1.1:53")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if r.Rcode != dns.RcodeSuccess {
|
|
return nil, nil
|
|
}
|
|
|
|
return r.Answer, nil
|
|
}
|
|
|
|
// LookupDomainData looks up domain data with caching.
|
|
func LookupDomainData(domain string) (*DomainDataResponse, error) {
|
|
if data, found := cache.Get(domain); found {
|
|
return data.(*DomainDataResponse), nil
|
|
}
|
|
|
|
eTLD, err := publicsuffix.EffectiveTLDPlusOne(domain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid domain: %w", err)
|
|
}
|
|
|
|
whoisRaw, err := performWhoisWithFallback(eTLD)
|
|
var whoisResult any
|
|
if err != nil {
|
|
slog.Error("whois lookup failed completely", "domain", eTLD, "err", err)
|
|
whoisResult = nil
|
|
} else {
|
|
parsed, parseErr := whoisparser.Parse(whoisRaw)
|
|
if parseErr != nil {
|
|
slog.Warn("failed to parse whois data, returning raw text", "domain", eTLD, "err", parseErr)
|
|
whoisResult = whoisRaw
|
|
} else {
|
|
whoisResult = formatWhois(parsed)
|
|
}
|
|
}
|
|
|
|
dnsData := DNSData{}
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
|
|
recordTypes := map[string]uint16{
|
|
"A": dns.TypeA,
|
|
"AAAA": dns.TypeAAAA,
|
|
"CNAME": dns.TypeCNAME,
|
|
"MX": dns.TypeMX,
|
|
"TXT": dns.TypeTXT,
|
|
"NS": dns.TypeNS,
|
|
"SOA": dns.TypeSOA,
|
|
"CAA": dns.TypeCAA,
|
|
}
|
|
|
|
for key, rType := range recordTypes {
|
|
wg.Add(1)
|
|
go func(name string, recordType uint16) {
|
|
defer wg.Done()
|
|
answers, err := queryDns(domain, recordType)
|
|
if err != nil {
|
|
slog.Debug("dns lookup failed for type", "type", name, "domain", domain, "err", err)
|
|
return
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for _, ans := range answers {
|
|
switch rr := ans.(type) {
|
|
case *dns.A:
|
|
dnsData.A = append(dnsData.A, rr.A.String())
|
|
case *dns.AAAA:
|
|
dnsData.AAAA = append(dnsData.AAAA, rr.AAAA.String())
|
|
case *dns.CNAME:
|
|
dnsData.CNAME = strings.TrimSuffix(rr.Target, ".")
|
|
case *dns.MX:
|
|
dnsData.MX = append(dnsData.MX, fmt.Sprintf("%d %s", rr.Preference, strings.TrimSuffix(rr.Mx, ".")))
|
|
case *dns.TXT:
|
|
dnsData.TXT = append(dnsData.TXT, strings.Join(rr.Txt, " "))
|
|
case *dns.NS:
|
|
dnsData.NS = append(dnsData.NS, strings.TrimSuffix(rr.Ns, "."))
|
|
case *dns.SOA:
|
|
soaStr := fmt.Sprintf("%s %s %d %d %d %d %d",
|
|
strings.TrimSuffix(rr.Ns, "."), strings.TrimSuffix(rr.Mbox, "."),
|
|
rr.Serial, rr.Refresh, rr.Retry, rr.Expire, rr.Minttl)
|
|
dnsData.SOA = append(dnsData.SOA, soaStr)
|
|
case *dns.CAA:
|
|
dnsData.CAA = append(dnsData.CAA, fmt.Sprintf(`%d %s "%s"`, rr.Flag, rr.Tag, rr.Value))
|
|
}
|
|
}
|
|
}(key, rType)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Sort MX records for consistent output
|
|
sort.Slice(dnsData.MX, func(i, j int) bool {
|
|
var prefI, prefJ int
|
|
_, _ = fmt.Sscanf(dnsData.MX[i], "%d", &prefI)
|
|
_, _ = fmt.Sscanf(dnsData.MX[j], "%d", &prefJ)
|
|
return prefI < prefJ
|
|
})
|
|
|
|
response := &DomainDataResponse{
|
|
Whois: whoisResult,
|
|
DNS: dnsData,
|
|
}
|
|
|
|
cache.Set(domain, response)
|
|
return response, nil
|
|
}
|