mirror of
https://github.com/skidoodle/ipinfo.git
synced 2026-04-28 01:27:34 +02:00
272 lines
6.4 KiB
Go
272 lines
6.4 KiB
Go
package internal
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
db "skidoodle/ipinfo/internal/db"
|
|
iputils "skidoodle/ipinfo/utils/iputils"
|
|
)
|
|
|
|
// DataStruct represents the structure of the IP data returned by the API.
|
|
type DataStruct struct {
|
|
IP *string `json:"ip"`
|
|
Hostname *string `json:"hostname"`
|
|
Org *string `json:"org"`
|
|
City *string `json:"city"`
|
|
Region *string `json:"region"`
|
|
Country *string `json:"country"`
|
|
Timezone *string `json:"timezone"`
|
|
Loc *string `json:"loc"`
|
|
}
|
|
|
|
// ASNDataResponse represents the structure of the ASN data returned by the API.
|
|
type ASNDataResponse struct {
|
|
Details Details `json:"details"`
|
|
Prefixes PrefixInfo `json:"prefixes"`
|
|
}
|
|
|
|
// Details represents the structure of the ASN details returned by the API.
|
|
type Details struct {
|
|
ASN uint `json:"asn"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// PrefixInfo represents the structure of the ASN prefix information returned by the API.
|
|
type PrefixInfo struct {
|
|
IPv4 []string `json:"ipv4"`
|
|
IPv6 []string `json:"ipv6"`
|
|
}
|
|
|
|
// Global caches with 10 minute TTL
|
|
var ipCache = NewIPCache(10 * time.Minute)
|
|
var asnCache = NewASNCache(10 * time.Minute)
|
|
|
|
// cachedIPData represents a cached IP lookup result.
|
|
type cachedIPData struct {
|
|
data *DataStruct
|
|
time time.Time
|
|
}
|
|
|
|
// cachedASNData represents a cached ASN lookup result.
|
|
type cachedASNData struct {
|
|
data *ASNDataResponse
|
|
time time.Time
|
|
}
|
|
|
|
// IPCache provides thread-safe caching of IP lookup results
|
|
type IPCache struct {
|
|
cache sync.Map
|
|
ttl time.Duration
|
|
}
|
|
|
|
// NewIPCache creates a new IP cache with the specified TTL
|
|
func NewIPCache(ttl time.Duration) *IPCache {
|
|
return &IPCache{
|
|
ttl: ttl,
|
|
}
|
|
}
|
|
|
|
// Set adds a new entry to the IP cache
|
|
func (c *IPCache) Set(ipStr string, data *DataStruct) {
|
|
c.cache.Store(ipStr, cachedIPData{
|
|
data: data,
|
|
time: time.Now(),
|
|
})
|
|
}
|
|
|
|
// Get retrieves an entry from the IP cache
|
|
func (c *IPCache) Get(ipStr string) (*DataStruct, bool) {
|
|
if cachedData, ok := c.cache.Load(ipStr); ok {
|
|
cached := cachedData.(cachedIPData)
|
|
if time.Since(cached.time) < c.ttl {
|
|
return cached.data, true
|
|
}
|
|
c.cache.Delete(ipStr)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// ASNCache provides thread-safe caching of ASN lookup results
|
|
type ASNCache struct {
|
|
cache sync.Map
|
|
ttl time.Duration
|
|
}
|
|
|
|
// NewASNCache creates a new ASN cache with the specified TTL
|
|
func NewASNCache(ttl time.Duration) *ASNCache {
|
|
return &ASNCache{
|
|
ttl: ttl,
|
|
}
|
|
}
|
|
|
|
// Set adds a new entry to the ASN cache
|
|
func (c *ASNCache) Set(asn uint, data *ASNDataResponse) {
|
|
c.cache.Store(asn, cachedASNData{
|
|
data: data,
|
|
time: time.Now(),
|
|
})
|
|
}
|
|
|
|
// Get retrieves an entry from the ASN cache
|
|
func (c *ASNCache) Get(asn uint) (*ASNDataResponse, bool) {
|
|
if cachedData, ok := c.cache.Load(asn); ok {
|
|
cached := cachedData.(cachedASNData)
|
|
if time.Since(cached.time) < c.ttl {
|
|
return cached.data, true
|
|
}
|
|
c.cache.Delete(asn)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// LookupIPData looks up IP data in the databases with caching
|
|
func LookupIPData(geoIP *db.GeoIPManager, ip net.IP) *DataStruct {
|
|
if data, found := ipCache.Get(ip.String()); found {
|
|
return data
|
|
}
|
|
|
|
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"`
|
|
Names map[string]string `maxminddb:"names"`
|
|
} `maxminddb:"country"`
|
|
Location struct {
|
|
Latitude float64 `maxminddb:"latitude"`
|
|
Longitude float64 `maxminddb:"longitude"`
|
|
Timezone string `maxminddb:"time_zone"`
|
|
} `maxminddb:"location"`
|
|
}
|
|
|
|
cityDB := geoIP.GetCityDB()
|
|
err := cityDB.Lookup(ip, &cityRecord)
|
|
if err != nil {
|
|
log.Printf("Error looking up city data: %v", err)
|
|
return nil
|
|
}
|
|
|
|
var asnRecord db.ASNRecord
|
|
asnDB := geoIP.GetASNDB()
|
|
err = asnDB.Lookup(ip, &asnRecord)
|
|
if err != nil {
|
|
log.Printf("Error looking up ASN data: %v", err)
|
|
return nil
|
|
}
|
|
|
|
hostname, err := net.LookupAddr(ip.String())
|
|
if err != nil || len(hostname) == 0 {
|
|
hostname = []string{""}
|
|
}
|
|
|
|
var sd *string
|
|
if len(cityRecord.Subdivisions) > 0 {
|
|
sd = ToPtr(cityRecord.Subdivisions[0].Names["en"])
|
|
}
|
|
|
|
data := &DataStruct{
|
|
IP: ToPtr(ip.String()),
|
|
Hostname: ToPtr(strings.TrimSuffix(hostname[0], ".")),
|
|
Org: ToPtr(fmt.Sprintf("AS%d %s", asnRecord.AutonomousSystemNumber, asnRecord.AutonomousSystemOrganization)),
|
|
City: ToPtr(cityRecord.City.Names["en"]),
|
|
Region: sd,
|
|
Country: ToPtr(cityRecord.Country.IsoCode),
|
|
Timezone: ToPtr(cityRecord.Location.Timezone),
|
|
Loc: ToPtr(fmt.Sprintf("%.4f,%.4f", cityRecord.Location.Latitude, cityRecord.Location.Longitude)),
|
|
}
|
|
|
|
ipCache.Set(ip.String(), 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 := asnCache.Get(targetASN); found {
|
|
return data, nil
|
|
}
|
|
|
|
prefixes := geoIP.GetASNPrefixes(targetASN)
|
|
if len(prefixes) == 0 {
|
|
return nil, fmt.Errorf("no prefixes found for ASN %d in the database", targetASN)
|
|
}
|
|
|
|
var orgName string
|
|
var ipv4Prefixes, ipv6Prefixes []string
|
|
|
|
var record db.ASNRecord
|
|
if err := geoIP.GetASNDB().Lookup(prefixes[0].IP, &record); err == nil {
|
|
orgName = record.AutonomousSystemOrganization
|
|
}
|
|
|
|
for _, prefix := range prefixes {
|
|
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: Details{
|
|
ASN: targetASN,
|
|
Name: orgName,
|
|
},
|
|
Prefixes: PrefixInfo{
|
|
IPv4: ipv4Prefixes,
|
|
IPv6: ipv6Prefixes,
|
|
},
|
|
}
|
|
|
|
asnCache.Set(targetASN, response)
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// ToPtr converts string to pointer
|
|
func ToPtr(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|
|
|
|
// IsBogon checks if the IP is a bogon IP
|
|
func IsBogon(ip net.IP) bool {
|
|
for _, net := range iputils.BogonNets {
|
|
if net.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetRealIP extracts the client's real IP address from request headers
|
|
func GetRealIP(r *http.Request) string {
|
|
for _, header := range []string{"CF-Connecting-IP", "X-Real-IP", "X-Forwarded-For"} {
|
|
if ip := r.Header.Get(header); ip != "" {
|
|
return strings.TrimSpace(strings.Split(ip, ",")[0])
|
|
}
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
return r.RemoteAddr
|
|
}
|
|
return host
|
|
}
|