Files
IPDATA/ipstore.go
Lizard ca4db18896 init
2025-12-26 18:50:42 +03:00

414 lines
9.9 KiB
Go

package main
import (
"context"
"encoding/csv"
"errors"
"fmt"
"io"
"log"
"math/big"
"net"
"os"
"sort"
"strconv"
"strings"
"time"
)
// ASNInfo represents data from the ASN CSV file
type ASNInfo struct {
IPRangeStart *big.Int
IPRangeEnd *big.Int
CIDR string
ASN string
ASName string
}
// GeoInfo represents data from the geolocation CSV file
type GeoInfo struct {
IPRangeStart *big.Int
IPRangeEnd *big.Int
CountryCode string
Country string
Region string
City string
Latitude float64
Longitude float64
ZipCode string
TimeZone string
}
// IPInfo combines ASN and geolocation information
type IPInfo struct {
Source string `json:"source"`
IP string `json:"ip"`
Hostname string `json:"hostname,omitempty"`
ASN string `json:"asn,omitempty"`
ASName string `json:"as_name,omitempty"`
CountryCode string `json:"country_code,omitempty"`
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
City string `json:"city,omitempty"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
ZipCode string `json:"zip_code,omitempty"`
TimeZone string `json:"time_zone,omitempty"`
}
type IPsInfo struct {
Items []IPInfo `json:"items"`
}
// IPStore holds the IP data in memory
type IPStore struct {
asnDataIPv4 []ASNInfo
asnDataIPv6 []ASNInfo
geoDataIPv4 []GeoInfo
geoDataIPv6 []GeoInfo
}
// NewIPStore creates and initializes a new IPStore
func NewIPStore(asnFileIPv6, geoFileIPv6, asnFileIPv4, geoFileIPv4 string) (*IPStore, error) {
store := &IPStore{
asnDataIPv4: []ASNInfo{},
asnDataIPv6: []ASNInfo{},
geoDataIPv4: []GeoInfo{},
geoDataIPv6: []GeoInfo{},
}
// Load IPv6 ASN data
if err := store.loadASNData(asnFileIPv6, &store.asnDataIPv6); err != nil {
return nil, fmt.Errorf("failed to load IPv6 ASN data: %w", err)
}
// Load IPv6 geolocation data
if err := store.loadGeoData(geoFileIPv6, &store.geoDataIPv6); err != nil {
return nil, fmt.Errorf("failed to load IPv6 geolocation data: %w", err)
}
// Load IPv4 ASN data
if err := store.loadASNData(asnFileIPv4, &store.asnDataIPv4); err != nil {
return nil, fmt.Errorf("failed to load IPv4 ASN data: %w", err)
}
// Load IPv4 geolocation data
if err := store.loadGeoData(geoFileIPv4, &store.geoDataIPv4); err != nil {
return nil, fmt.Errorf("failed to load IPv4 geolocation data: %w", err)
}
// Sort data for binary search (IPv6)
sort.Slice(store.asnDataIPv6, func(i, j int) bool {
return store.asnDataIPv6[i].IPRangeStart.Cmp(store.asnDataIPv6[j].IPRangeStart) < 0
})
sort.Slice(store.geoDataIPv6, func(i, j int) bool {
return store.geoDataIPv6[i].IPRangeStart.Cmp(store.geoDataIPv6[j].IPRangeStart) < 0
})
// Sort data for binary search (IPv4)
sort.Slice(store.asnDataIPv4, func(i, j int) bool {
return store.asnDataIPv4[i].IPRangeStart.Cmp(store.asnDataIPv4[j].IPRangeStart) < 0
})
sort.Slice(store.geoDataIPv4, func(i, j int) bool {
return store.geoDataIPv4[i].IPRangeStart.Cmp(store.geoDataIPv4[j].IPRangeStart) < 0
})
return store, nil
}
// loadASNData parses the ASN CSV file
func (s *IPStore) loadASNData(filename string, data *[]ASNInfo) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
reader := csv.NewReader(file)
reader.Comma = ','
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
if len(record) < 5 {
continue // Skip malformed records
}
// Parse IP range
ipStart := new(big.Int)
ipStart.SetString(record[0], 10)
ipEnd := new(big.Int)
ipEnd.SetString(record[1], 10)
*data = append(*data, ASNInfo{
IPRangeStart: ipStart,
IPRangeEnd: ipEnd,
CIDR: record[2],
ASN: record[3],
ASName: record[4],
})
}
return nil
}
// loadGeoData parses the geolocation CSV file
func (s *IPStore) loadGeoData(filename string, data *[]GeoInfo) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
reader := csv.NewReader(file)
reader.Comma = ','
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
if len(record) < 10 {
continue // Skip malformed records
}
// Parse IP range
ipStart := new(big.Int)
ipStart.SetString(record[0], 10)
ipEnd := new(big.Int)
ipEnd.SetString(record[1], 10)
// Parse coordinates
lat, _ := strconv.ParseFloat(record[6], 64)
long, _ := strconv.ParseFloat(record[7], 64)
*data = append(*data, GeoInfo{
IPRangeStart: ipStart,
IPRangeEnd: ipEnd,
CountryCode: record[2],
Country: record[3],
Region: record[4],
City: record[5],
Latitude: lat,
Longitude: long,
ZipCode: record[8],
TimeZone: record[9],
})
}
return nil
}
// lookupHostname performs a reverse DNS lookup to find the hostname for an IP
// with a timeout to avoid long waiting times
func lookupHostname(ipStr string, timeout int) string {
// Check if DNS lookup is disabled
if disableDNS := os.Getenv("DISABLE_DNS_LOOKUP"); disableDNS != "" {
if strings.ToLower(disableDNS) == "true" || disableDNS == "1" {
return ""
}
}
startTime := time.Now()
// Get timeout from environment or use default
timeoutMs := timeout
if envTimeout := os.Getenv("DNS_TIMEOUT_MS"); envTimeout != "" {
if t, err := strconv.Atoi(envTimeout); err == nil && t > 0 {
timeoutMs = t
}
}
// Create a context with the configured timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMs)*time.Millisecond)
defer cancel()
// Use a channel to handle the timeout in parallel with the DNS lookup
resultChan := make(chan string, 1)
go func() {
// Perform the DNS lookup in a goroutine
names, err := net.DefaultResolver.LookupAddr(ctx, ipStr)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Printf("DNS lookup timeout for IP %s (limit: %dms)", ipStr, timeoutMs)
} else {
log.Printf("DNS lookup error for IP %s: %v", ipStr, err)
}
resultChan <- ""
return
}
if len(names) == 0 {
resultChan <- ""
return
}
resultChan <- names[0]
}()
// Wait for the result or timeout
select {
case result := <-resultChan:
elapsed := time.Since(startTime)
if result != "" {
log.Printf("DNS lookup for IP %s took %dms: found hostname %s",
ipStr, elapsed.Milliseconds(), result)
}
return result
case <-ctx.Done():
// Timeout or context cancelled
log.Printf("DNS lookup for IP %s timed out after %dms",
ipStr, time.Since(startTime).Milliseconds())
return ""
}
}
// ipToBigInt converts an IP address to a big.Int
func ipToBigInt(ipStr string) (*big.Int, bool, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, false, errors.New("invalid IP address")
}
// Check if it's IPv4 or IPv6
isIPv4 := ip.To4() != nil
// For IPv4, ensure it's in IPv4 format (not mapped)
if isIPv4 {
ip = ip.To4()
}
ipInt := new(big.Int)
ipBytes := []byte(ip)
ipInt.SetBytes(ipBytes)
return ipInt, isIPv4, nil
}
// findASNInfo uses binary search to find ASN info for an IP
func (s *IPStore) findASNInfo(ipInt *big.Int, isIPv4 bool) *ASNInfo {
var data []ASNInfo
if isIPv4 {
data = s.asnDataIPv4
} else {
data = s.asnDataIPv6
}
idx := sort.Search(len(data), func(i int) bool {
return data[i].IPRangeEnd.Cmp(ipInt) >= 0
})
if idx < len(data) &&
data[idx].IPRangeStart.Cmp(ipInt) <= 0 &&
data[idx].IPRangeEnd.Cmp(ipInt) >= 0 {
return &data[idx]
}
return nil
}
// findGeoInfo uses binary search to find geolocation info for an IP
func (s *IPStore) findGeoInfo(ipInt *big.Int, isIPv4 bool) *GeoInfo {
var data []GeoInfo
if isIPv4 {
data = s.geoDataIPv4
} else {
data = s.geoDataIPv6
}
idx := sort.Search(len(data), func(i int) bool {
return data[i].IPRangeEnd.Cmp(ipInt) >= 0
})
if idx < len(data) &&
data[idx].IPRangeStart.Cmp(ipInt) <= 0 &&
data[idx].IPRangeEnd.Cmp(ipInt) >= 0 {
return &data[idx]
}
return nil
}
// GetIPInfo retrieves information about an IP address
func (s *IPStore) GetIPInfo(ipStr string, timeout int, source string) (*IPInfo, error) {
ipInt, isIPv4, err := ipToBigInt(ipStr)
if err != nil {
return nil, err
}
info := &IPInfo{IP: ipStr, Source: source}
// Add hostname via reverse DNS lookup
info.Hostname = lookupHostname(ipStr, timeout)
// Find ASN info using binary search
if asnInfo := s.findASNInfo(ipInt, isIPv4); asnInfo != nil {
info.ASN = asnInfo.ASN
info.ASName = asnInfo.ASName
}
// Find geolocation info using binary search
if geoInfo := s.findGeoInfo(ipInt, isIPv4); geoInfo != nil {
info.CountryCode = geoInfo.CountryCode
info.Country = geoInfo.Country
info.Region = geoInfo.Region
info.City = geoInfo.City
info.Latitude = geoInfo.Latitude
info.Longitude = geoInfo.Longitude
info.ZipCode = geoInfo.ZipCode
info.TimeZone = geoInfo.TimeZone
}
return info, nil
}
// IsValidIP checks if the provided string is a valid IP address
func (s *IPStore) IsValidIP(input string) bool {
return net.ParseIP(input) != nil
}
// ResolveDomain resolves a domain name to an IP address
func (s *IPStore) ResolveDomain(domain string, timeout int) (string, error) {
if envTimeout := os.Getenv("DNS_TIMEOUT_MS"); envTimeout != "" {
if t, err := strconv.Atoi(envTimeout); err == nil && t > 0 {
timeout = t
}
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Millisecond)
defer cancel()
// Use net.DefaultResolver to perform the lookup
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain)
if err != nil {
return "", fmt.Errorf("failed to resolve domain %s: %w", domain, err)
}
if len(ips) == 0 {
return "", fmt.Errorf("no IP addresses found for domain %s", domain)
}
// Prefer IPv4 addresses if available
for _, ip := range ips {
if ip.To4() != nil {
return ip.String(), nil
}
}
// Return the first IP address if no IPv4 available
return ips[0].String(), nil
}