package geo import ( "context" "fmt" "net" "strings" "sync" "github.com/lionsoul2014/ip2region/binding/golang/xdb" recordstore "speedtest/internal/store" ) type XDBResolver struct { mu sync.Mutex searcher *xdb.Searcher } func NewXDBResolver(db []byte) (*XDBResolver, error) { if len(db) == 0 { return nil, fmt.Errorf("empty ip2region xdb") } searcher, err := xdb.NewWithBuffer(xdb.IPv4, db) if err != nil { return nil, fmt.Errorf("create ip2region searcher: %w", err) } return &XDBResolver{searcher: searcher}, nil } func (r *XDBResolver) Resolve(ctx context.Context, ip string) (recordstore.ClientInfo, error) { if err := ctx.Err(); err != nil { return recordstore.ClientInfo{}, err } parsed := net.ParseIP(strings.TrimSpace(ip)) if parsed == nil || parsed.To4() == nil { return recordstore.ClientInfo{}, fmt.Errorf("ip2region supports IPv4 only in embedded database") } r.mu.Lock() region, err := r.searcher.Search(parsed.String()) r.mu.Unlock() if err != nil { return recordstore.ClientInfo{}, err } if strings.TrimSpace(region) == "" { return recordstore.ClientInfo{}, fmt.Errorf("ip2region returned empty region") } info := clientInfoFromIP2Region(ip, region) info.Source = "ip2region" return info, nil } func clientInfoFromIP2Region(ip string, region string) recordstore.ClientInfo { parts := strings.Split(region, "|") country := field(parts, 0) regionName := field(parts, 1) city := field(parts, 2) isp := field(parts, 3) return recordstore.ClientInfo{ IP: ip, ISP: isp, Region: regionName, Country: country, City: city, } } func field(parts []string, index int) string { if index >= len(parts) { return "" } value := strings.TrimSpace(parts[index]) if value == "0" || value == "未知" || strings.EqualFold(value, "unknown") { return "" } return value }