100 lines
2.5 KiB
Go
100 lines
2.5 KiB
Go
|
|
// Package location resolves the geographic position of this node via IP
|
||
|
|
// geolocation and applies a privacy-preserving random offset proportional
|
||
|
|
// to the chosen granularity level before publishing the result.
|
||
|
|
package location
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"math/rand"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
peer "cloud.o-forge.io/core/oc-lib/models/peer"
|
||
|
|
)
|
||
|
|
|
||
|
|
// fuzzRadius returns the maximum random offset (in degrees) for each axis
|
||
|
|
// given a granularity level.
|
||
|
|
//
|
||
|
|
// 0 → no location
|
||
|
|
// 1 → continent ±15° lat / ±20° lng
|
||
|
|
// 2 → country ±3° lat / ±4° lng (default)
|
||
|
|
// 3 → region ±0.5° lat / ±0.7° lng
|
||
|
|
// 4 → city ±0.05° lat / ±0.07° lng
|
||
|
|
func fuzzRadius(granularity int) (latR, lngR float64) {
|
||
|
|
switch granularity {
|
||
|
|
case 1:
|
||
|
|
return 15.0, 20.0
|
||
|
|
case 2:
|
||
|
|
return 3.0, 4.0
|
||
|
|
case 3:
|
||
|
|
return 0.5, 0.7
|
||
|
|
case 4:
|
||
|
|
return 0.05, 0.07
|
||
|
|
default:
|
||
|
|
return 3.0, 4.0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// clamp keeps a value inside [min, max].
|
||
|
|
func clamp(v, min, max float64) float64 {
|
||
|
|
if v < min {
|
||
|
|
return min
|
||
|
|
}
|
||
|
|
if v > max {
|
||
|
|
return max
|
||
|
|
}
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
|
||
|
|
// ipAPIResponse is the subset of fields returned by ip-api.com/json.
|
||
|
|
type ipAPIResponse struct {
|
||
|
|
Status string `json:"status"`
|
||
|
|
Lat float64 `json:"lat"`
|
||
|
|
Lon float64 `json:"lon"`
|
||
|
|
Country string `json:"country"`
|
||
|
|
Region string `json:"regionName"`
|
||
|
|
City string `json:"city"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Geolocate resolves the current public IP location via ip-api.com (free,
|
||
|
|
// no key required for non-commercial use), then fuzzes the result according
|
||
|
|
// to granularity.
|
||
|
|
//
|
||
|
|
// Returns nil if granularity == 0 (opt-out) or if the lookup fails.
|
||
|
|
func Geolocate(granularity int) *peer.PeerLocation {
|
||
|
|
if granularity == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
||
|
|
resp, err := client.Get("http://ip-api.com/json?fields=status,lat,lon,country,regionName,city")
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
var result ipAPIResponse
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || result.Status != "success" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
latR, lngR := fuzzRadius(granularity)
|
||
|
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
|
|
||
|
|
fuzzedLat := result.Lat + (rng.Float64()*2-1)*latR
|
||
|
|
fuzzedLng := result.Lon + (rng.Float64()*2-1)*lngR
|
||
|
|
|
||
|
|
fuzzedLat = clamp(fuzzedLat, -85.0, 85.0)
|
||
|
|
fuzzedLng = clamp(fuzzedLng, -180.0, 180.0)
|
||
|
|
|
||
|
|
fmt.Printf("[location] granularity=%d raw=(%.4f,%.4f) fuzzed=(%.4f,%.4f)\n",
|
||
|
|
granularity, result.Lat, result.Lon, fuzzedLat, fuzzedLng)
|
||
|
|
|
||
|
|
return &peer.PeerLocation{
|
||
|
|
Latitude: fuzzedLat,
|
||
|
|
Longitude: fuzzedLng,
|
||
|
|
Granularity: granularity,
|
||
|
|
}
|
||
|
|
}
|