github.com/jimpick/sp-kyc-checks@v0.0.0-20230201194251-fa84fca72da8/checks/geoip/check_geo.go (about)

     1  package geoip
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/jftuga/geodist"
    11  	"github.com/savaki/geoip2"
    12  	"googlemaps.github.io/maps"
    13  )
    14  
    15  const MAX_DISTANCE = 600
    16  
    17  type GeoData struct {
    18  	MultiaddrsIPs []MultiaddrsIPsRecord
    19  	IPsGeolite2   map[string]IPsGeolite2Record
    20  	IPsBaidu      map[string]IPsBaiduRecord
    21  	IPsGeoIP2     map[string]geoip2.Response
    22  }
    23  
    24  func LoadGeoData() (*GeoData, error) {
    25  	multiaddrsIPs, err := LoadMultiAddrsIPs()
    26  	if err != nil {
    27  		return nil, err
    28  	}
    29  
    30  	ipsGeolite2, err := LoadIPsGeolite2()
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  
    35  	ipsBaidu, err := LoadIPsBaidu()
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	return &GeoData{
    41  		multiaddrsIPs,
    42  		ipsGeolite2,
    43  		ipsBaidu,
    44  		make(map[string]geoip2.Response),
    45  	}, nil
    46  }
    47  
    48  func (g *GeoData) filterByMinerID(ctx context.Context, minerID string,
    49  	currentEpoch int64) (*GeoData, error) {
    50  	minEpoch := currentEpoch - 14*24*60*2 // 2 weeks
    51  	multiaddrsIPs := []MultiaddrsIPsRecord{}
    52  	ipsGeoLite2 := make(map[string]IPsGeolite2Record)
    53  	ipsBaidu := make(map[string]IPsBaiduRecord)
    54  	ipsGeoIP2 := make(map[string]geoip2.Response)
    55  	for _, m := range g.MultiaddrsIPs {
    56  		if m.Miner == minerID {
    57  			if int64(m.Epoch) < minEpoch {
    58  				log.Printf("IP address %s rejected, too old: %d < %d\n",
    59  					m.IP, m.Epoch, minEpoch)
    60  			} else {
    61  				multiaddrsIPs = append(multiaddrsIPs, m)
    62  				if r, ok := g.IPsGeolite2[m.IP]; ok {
    63  					ipsGeoLite2[m.IP] = r
    64  				}
    65  				if r, ok := g.IPsBaidu[m.IP]; ok {
    66  					ipsBaidu[m.IP] = r
    67  				}
    68  				r, err := getGeoIP2(ctx, m.IP)
    69  				if err != nil {
    70  					return &GeoData{}, err
    71  				}
    72  				ipsGeoIP2[m.IP] = r
    73  			}
    74  		}
    75  	}
    76  
    77  	return &GeoData{
    78  		multiaddrsIPs,
    79  		ipsGeoLite2,
    80  		ipsBaidu,
    81  		ipsGeoIP2,
    82  	}, nil
    83  }
    84  
    85  type ExtraArtifacts struct {
    86  	GeoData           *GeoData
    87  	GeocodeLocations  []geodist.Coord
    88  	GoogleGeocodeData []maps.GeocodingResult
    89  }
    90  
    91  func findMatchGeoLite2(g *GeoData, minerID string, city string,
    92  	countryCode string, locations []geodist.Coord) bool {
    93  	var match_found bool = false
    94  	for ip, geolite2 := range g.IPsGeolite2 {
    95  		// Match country
    96  		if geolite2.Country != countryCode {
    97  			log.Printf("No Geolite2 country match for %s (%s != GeoLite2:%s), IP: %s\n",
    98  				minerID, countryCode, geolite2.Country, ip)
    99  			continue
   100  		}
   101  		log.Printf("Matching Geolite2 country for %s (%s) found, IP: %s\n",
   102  			minerID, countryCode, ip)
   103  
   104  		// Try to match city
   105  		if geolite2.City == city {
   106  			log.Printf("Match found! %s matches Geolite2 city name (%s), IP: %s\n",
   107  				minerID, city, ip)
   108  			match_found = true
   109  			continue
   110  		}
   111  		log.Printf("No Geolite2 city match for %s (%s != GeoLite2:%s), IP: %s\n",
   112  			minerID, city, geolite2.City, ip)
   113  
   114  		// Try to match based on Lat/Lng
   115  		l := geolite2.Geolite2["location"].(map[string]interface{})
   116  		geolite2Location := geodist.Coord{
   117  			Lat: l["latitude"].(float64),
   118  			Lon: l["longitude"].(float64),
   119  		}
   120  		log.Printf("Geolite2 Lat/Lng: %v for IP %s\n", geolite2Location, ip)
   121  
   122  		// Distance based matching
   123  		for i, location := range locations {
   124  			log.Printf("Geocoded via Google %s, %s #%d Lat/Long %v", city,
   125  				countryCode, i+1, location)
   126  			_, distance, err := geodist.VincentyDistance(location, geolite2Location)
   127  			if err != nil {
   128  				log.Println("Unable to compute Vincenty Distance.")
   129  				continue
   130  			} else {
   131  				if distance <= MAX_DISTANCE {
   132  					log.Printf("Match found! Distance %f km\n", distance)
   133  					match_found = true
   134  					continue
   135  				}
   136  				log.Printf("No match, distance %f km > %d km\n", distance, MAX_DISTANCE)
   137  			}
   138  		}
   139  	}
   140  	return match_found
   141  }
   142  
   143  func findMatchGeoIP2(g *GeoData, minerID string, city string,
   144  	countryCode string, locations []geodist.Coord) bool {
   145  	provisional_match := false
   146  	match_found := false
   147  GEOIP2_LOOP:
   148  	for ip, geoip2 := range g.IPsGeoIP2 {
   149  		// Match country
   150  		if geoip2.Country.IsoCode != countryCode {
   151  			log.Printf("No GeoIP2 country match for %s (%s != GeoIP2:%s), IP: %s\n",
   152  				minerID, countryCode, geoip2.Country.IsoCode, ip)
   153  			continue
   154  		}
   155  		log.Printf("Matching GeoIP2 country for %s (%s) found, IP: %s\n",
   156  			minerID, countryCode, ip)
   157  
   158  		// Try to match city
   159  		for _, cityName := range geoip2.City.Names {
   160  			if cityName == city {
   161  				log.Printf("Match found! %s matches GeoIP2 city name (%s), IP: %s\n",
   162  					minerID, city, ip)
   163  				match_found = true
   164  				continue GEOIP2_LOOP
   165  			}
   166  		}
   167  		log.Printf("No GeoIP2 city match for %s (%s != GeoIP2:%s), IP: %s\n",
   168  			minerID, city, geoip2.City.Names["en"], ip)
   169  		if geoip2.City.Names["en"] == "" {
   170  			provisional_match = true
   171  		}
   172  
   173  		// Try to match based on Lat/Lng
   174  		geoip2Location := geodist.Coord{
   175  			Lat: geoip2.Location.Latitude,
   176  			Lon: geoip2.Location.Longitude,
   177  		}
   178  		log.Printf("GeoIP2 Lat/Lng: %v for IP %s\n", geoip2Location, ip)
   179  
   180  		// Distance based matching
   181  		for i, location := range locations {
   182  			log.Printf("Geocoded via Google %s, %s #%d Lat/Long %v", city,
   183  				countryCode, i+1, location)
   184  			_, distance, err := geodist.VincentyDistance(location, geoip2Location)
   185  			if err != nil {
   186  				log.Println("Unable to compute Vincenty Distance.")
   187  				continue
   188  			} else {
   189  				if distance <= MAX_DISTANCE {
   190  					log.Printf("Match found! Distance %f km\n", distance)
   191  					match_found = true
   192  					continue
   193  				}
   194  				log.Printf("No match, distance %f km > %d km\n", distance, MAX_DISTANCE)
   195  			}
   196  		}
   197  	}
   198  	if provisional_match {
   199  		log.Printf("Match found! %s had GeoIP2 entries that matched country, "+
   200  			"all with no city data.\n",
   201  			minerID)
   202  		match_found = true
   203  	}
   204  	return match_found
   205  }
   206  
   207  func findMatchBaidu(g *GeoData, minerID string, city string,
   208  	countryCode string, locations []geodist.Coord) bool {
   209  	match_found := false
   210  	for ip, baidu := range g.IPsBaidu {
   211  		// Try to match city
   212  		if baidu.City == city {
   213  			log.Printf("Match found! %s matches city name (%s), IP: %s\n",
   214  				minerID, city, ip)
   215  			match_found = true
   216  			continue
   217  		}
   218  		log.Printf("No city match for %s (%s != Baidu:%s), IP: %s\n",
   219  			minerID, city, baidu.City, ip)
   220  
   221  		baiduContent := baidu.Baidu["content"].(map[string]interface{})
   222  		baiduPoint := baiduContent["point"].(map[string]interface{})
   223  		lon, err := strconv.ParseFloat(baiduPoint["x"].(string), 64)
   224  		if err != nil {
   225  			log.Println("Error parsing baidu longitude (x)", err)
   226  			continue
   227  		}
   228  		lat, err := strconv.ParseFloat(baiduPoint["y"].(string), 64)
   229  		if err != nil {
   230  			log.Println("Error parsing baidu latitude (y)", err)
   231  			continue
   232  		}
   233  		baiduLocation := geodist.Coord{
   234  			Lat: lat,
   235  			Lon: lon,
   236  		}
   237  		log.Printf("Baidu Lat/Lng: %v for IP %s\n", baiduLocation, ip)
   238  		// Distance based matching
   239  		for i, location := range locations {
   240  			log.Printf("Geocoded via Google %s, %s #%d Lat/Long %v", city,
   241  				countryCode, i+1, location)
   242  			_, distance, err := geodist.VincentyDistance(location, baiduLocation)
   243  			if err != nil {
   244  				log.Println("Unable to compute Vincenty Distance.")
   245  				continue
   246  			} else {
   247  				if distance <= MAX_DISTANCE {
   248  					log.Printf("Match found! Distance %f km\n", distance)
   249  					match_found = true
   250  					continue
   251  				}
   252  				log.Printf("No match, distance %f km > %d km\n", distance, MAX_DISTANCE)
   253  			}
   254  		}
   255  	}
   256  	return match_found
   257  }
   258  
   259  // GeoMatchExists checks if the miner has an IP address with a location close to the city/country
   260  func GeoMatchExists(ctx context.Context, geodata *GeoData,
   261  	geocodeClient *maps.Client, currentEpoch int64, minerID string, city string,
   262  	countryCode string) (bool, ExtraArtifacts, error) {
   263  
   264  	// Quick fixes for bad input data
   265  	if countryCode == "United States" || countryCode == "San Jose, CA" {
   266  		countryCode = "US"
   267  	}
   268  	if countryCode == "Canada" {
   269  		countryCode = "CA"
   270  	}
   271  	countryCode = strings.ToUpper(countryCode)
   272  
   273  	log.Printf("Searching for geo matches for %s (%s, %s)", minerID,
   274  		city, countryCode)
   275  	g, err := geodata.filterByMinerID(ctx, minerID, currentEpoch)
   276  	extraArtifacts := ExtraArtifacts{GeoData: g}
   277  	if err != nil {
   278  		return false, extraArtifacts, err
   279  	}
   280  
   281  	if len(g.MultiaddrsIPs) == 0 {
   282  		log.Printf("No Multiaddrs/IPs found for %s\n", minerID)
   283  		return false, extraArtifacts, nil
   284  	}
   285  
   286  	locations, googleResponse, err := geocodeAddress(ctx, geocodeClient,
   287  		fmt.Sprintf("%s, %s", city, countryCode))
   288  	if err != nil {
   289  		log.Fatalf("Geocode error: %s", err)
   290  	}
   291  	extraArtifacts.GeocodeLocations = locations
   292  	extraArtifacts.GoogleGeocodeData = googleResponse
   293  
   294  	match_found := false
   295  
   296  	// First, try with Baidu data
   297  	if countryCode == "CN" {
   298  		match_found = findMatchBaidu(g, minerID, city, countryCode, locations)
   299  	}
   300  
   301  	// Next, try with Geolite2 data
   302  	if !match_found {
   303  		match_found = findMatchGeoLite2(g, minerID, city, countryCode, locations)
   304  	}
   305  
   306  	// last, try with GeoIP2 API data
   307  	if !match_found {
   308  		match_found = findMatchGeoIP2(g, minerID, city, countryCode, locations)
   309  	}
   310  
   311  	if !match_found {
   312  		log.Println("No match found.")
   313  	}
   314  	return match_found, extraArtifacts, nil
   315  }