github.com/m-lab/locate@v0.17.6/clientgeo/maxmind.go (about) 1 package clientgeo 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "log" 8 "net" 9 "net/http" 10 "strings" 11 "sync" 12 13 "github.com/m-lab/go/content" 14 "github.com/m-lab/go/rtx" 15 "github.com/oschwald/geoip2-golang" 16 17 "github.com/m-lab/uuid-annotator/tarreader" 18 ) 19 20 // NewMaxmindLocator creates a new MaxmindLocator and loads the current copy of 21 // MaxMind data stored in GCS. 22 func NewMaxmindLocator(ctx context.Context, mm content.Provider) *MaxmindLocator { 23 mml := &MaxmindLocator{ 24 dataSource: mm, 25 } 26 var err error 27 mml.maxmind, err = mml.load(ctx) 28 rtx.Must(err, "Could not load annotation db") 29 return mml 30 } 31 32 // MaxmindLocator manages access to the maxmind database. 33 type MaxmindLocator struct { 34 mut sync.RWMutex 35 dataSource content.Provider 36 maxmind *geoip2.Reader 37 } 38 39 var emptyResult = geoip2.City{} 40 41 // Locate finds the Location of the given request using client's remote IP or IP 42 // from X-Forwarded-For header. 43 func (mml *MaxmindLocator) Locate(req *http.Request) (*Location, error) { 44 mml.mut.RLock() 45 defer mml.mut.RUnlock() 46 47 ip, err := ipFromRequest(req) 48 if err != nil { 49 return nil, err 50 } 51 if ip == nil { 52 return nil, errors.New("cannot locate nil IP") 53 } 54 if mml.maxmind == nil { 55 log.Println("No maxmind DB present. This should only occur during testing.") 56 return nil, errors.New("no maxmind db loaded") 57 } 58 record, err := mml.maxmind.City(ip) 59 if err != nil { 60 return nil, err 61 } 62 63 // Check for empty results because "not found" is not an error. Instead the 64 // geoip2 package returns an empty result. May be fixed in a future version: 65 // https://github.com/oschwald/geoip2-golang/issues/32 66 // 67 // "Not found" in a well-functioning database should not be an error. 68 // Instead, it is an accurate reflection of data that is missing. 69 if isEmpty(record) { 70 return nil, errors.New("unknown location; empty result") 71 } 72 73 lat := fmt.Sprintf("%f", record.Location.Latitude) 74 lon := fmt.Sprintf("%f", record.Location.Longitude) 75 tmp := &Location{ 76 Latitude: lat, 77 Longitude: lon, 78 Headers: http.Header{ 79 hLocateClientlatlon: []string{lat + "," + lon}, 80 hLocateClientlatlonMethod: []string{"maxmind-remoteip"}, 81 }, 82 } 83 return tmp, nil 84 } 85 86 func ipFromRequest(req *http.Request) (net.IP, error) { 87 fwdIPs := strings.Split(req.Header.Get("X-Forwarded-For"), ", ") 88 var ip net.IP 89 if fwdIPs[0] != "" { 90 ip = net.ParseIP(fwdIPs[0]) 91 } else { 92 h, _, err := net.SplitHostPort(req.RemoteAddr) 93 if err != nil { 94 return nil, errors.New("failed to parse remote addr") 95 } 96 ip = net.ParseIP(h) 97 } 98 return ip, nil 99 } 100 101 // Reload is intended to be regularly called in a loop. It should check whether 102 // the data in GCS is newer than the local data, and, if it is, then download 103 // and load that new data into memory and then replace it in the annotator. 104 func (mml *MaxmindLocator) Reload(ctx context.Context) { 105 mm, err := mml.load(ctx) 106 if err != nil { 107 log.Println("Could not reload maxmind dataset:", err) 108 return 109 } 110 // Don't acquire the lock until after the data is in RAM. 111 mml.mut.Lock() 112 defer mml.mut.Unlock() 113 mml.maxmind = mm 114 } 115 116 func isEmpty(r *geoip2.City) bool { 117 // The record has no associated city, country, or continent. 118 return r.City.GeoNameID == 0 && r.Country.GeoNameID == 0 && r.Continent.GeoNameID == 0 119 } 120 121 // load unconditionally loads datasets and returns them. 122 func (mml *MaxmindLocator) load(ctx context.Context) (*geoip2.Reader, error) { 123 tgz, err := mml.dataSource.Get(ctx) 124 if err == content.ErrNoChange { 125 return mml.maxmind, nil 126 } 127 if err != nil { 128 return nil, err 129 } 130 data, err := tarreader.FromTarGZ(tgz, "GeoLite2-City.mmdb") 131 if err != nil { 132 return nil, err 133 } 134 return geoip2.FromBytes(data) 135 }