geoip2.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package geoip2
  2. import (
  3. "fmt"
  4. "io/fs"
  5. "log"
  6. "net"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/abh/geodns/countries"
  13. "github.com/abh/geodns/targeting/geo"
  14. gdb "github.com/oschwald/geoip2-golang"
  15. )
  16. // GeoIP2 contains the geoip implementation of the GeoDNS geo
  17. // targeting interface
  18. type GeoIP2 struct {
  19. dir string
  20. country geodb
  21. city geodb
  22. asn geodb
  23. }
  24. type geodb struct {
  25. active bool
  26. lastModified int64 // Epoch time
  27. fp string // FilePath
  28. db *gdb.Reader // Database reader
  29. l sync.RWMutex // Individual lock for separate DB access and reload -- Future?
  30. }
  31. // FindDB returns a guess at a directory path for GeoIP data files
  32. func FindDB() string {
  33. dirs := []string{
  34. "/usr/share/GeoIP/", // Linux default
  35. "/usr/share/local/GeoIP/", // source install?
  36. "/usr/local/share/GeoIP/", // FreeBSD
  37. "/opt/local/share/GeoIP/", // MacPorts
  38. }
  39. for _, dir := range dirs {
  40. if _, err := os.Stat(dir); err != nil {
  41. if os.IsExist(err) {
  42. log.Println(err)
  43. }
  44. continue
  45. }
  46. return dir
  47. }
  48. return ""
  49. }
  50. // open will create a filehandle for the provided GeoIP2 database. If opened once before and a newer modification time is present, the function will reopen the file with its new contents
  51. func (g *GeoIP2) open(v *geodb, fns ...string) error {
  52. var fi fs.FileInfo
  53. var err error
  54. if v.fp == "" {
  55. // We're opening this file for the first time
  56. for _, i := range fns {
  57. fp := filepath.Join(g.dir, i)
  58. fi, err = os.Stat(fp)
  59. if err != nil {
  60. continue
  61. }
  62. v.fp = fp
  63. }
  64. }
  65. if v.fp == "" { // Recheck for empty string in case none of the DB files are found
  66. return fmt.Errorf("no files found for db")
  67. }
  68. if fi == nil { // We have not set fileInfo and v.fp is set
  69. fi, err = os.Stat(v.fp)
  70. }
  71. if err != nil {
  72. return err
  73. }
  74. if v.lastModified >= fi.ModTime().UTC().Unix() { // No update to existing file
  75. return nil
  76. }
  77. // Delay the lock to here because we're only
  78. v.l.Lock()
  79. defer v.l.Unlock()
  80. o, e := gdb.Open(v.fp)
  81. if e != nil {
  82. return e
  83. }
  84. v.db = o
  85. v.active = true
  86. v.lastModified = fi.ModTime().UTC().Unix()
  87. return nil
  88. }
  89. // watchFiles spawns a goroutine to check for new files every minute, reloading if the modtime is newer than the original file's modtime
  90. func (g *GeoIP2) watchFiles() {
  91. // Not worried about goroutines leaking because only one geoip2.New call is made in main (outside of testing)
  92. ticker := time.NewTicker(1 * time.Minute)
  93. for { // We forever-loop here because we only run this function in a separate goroutine
  94. select {
  95. case <-ticker.C:
  96. // Iterate through each db, check modtime. If new, reload file
  97. cityErr := g.open(&g.city, "GeoIP2-City.mmdb", "GeoLite2-City.mmdb")
  98. if cityErr != nil {
  99. log.Printf("Failed to update City: %v\n", cityErr)
  100. }
  101. countryErr := g.open(&g.country, "GeoIP2-Country.mmdb", "GeoLite2-Country.mmdb")
  102. if countryErr != nil {
  103. log.Printf("failed to update Country: %v\n", countryErr)
  104. }
  105. asnErr := g.open(&g.asn, "GeoIP2-ASN.mmdb", "GeoLite2-ASN.mmdb")
  106. if asnErr != nil {
  107. log.Printf("failed to update ASN: %v\n", asnErr)
  108. }
  109. }
  110. }
  111. }
  112. func (g *GeoIP2) anyActive() bool {
  113. return g.country.active || g.city.active || g.asn.active
  114. }
  115. // New returns a new GeoIP2 provider
  116. func New(dir string) (g *GeoIP2, err error) {
  117. g = &GeoIP2{
  118. dir: dir,
  119. }
  120. // This routine MUST load the database files at least once.
  121. cityErr := g.open(&g.city, "GeoIP2-City.mmdb", "GeoLite2-City.mmdb")
  122. if cityErr != nil {
  123. log.Printf("failed to load City DB: %v\n", cityErr)
  124. err = cityErr
  125. }
  126. countryErr := g.open(&g.country, "GeoIP2-Country.mmdb", "GeoLite2-Country.mmdb")
  127. if countryErr != nil {
  128. log.Printf("failed to load Country DB: %v\n", countryErr)
  129. err = countryErr
  130. }
  131. asnErr := g.open(&g.asn, "GeoIP2-ASN.mmdb", "GeoLite2-ASN.mmdb")
  132. if asnErr != nil {
  133. log.Printf("failed to load ASN DB: %v\n", asnErr)
  134. err = asnErr
  135. }
  136. if !g.anyActive() {
  137. return nil, err
  138. }
  139. go g.watchFiles() // Launch goroutine to load and monitor
  140. return
  141. }
  142. // HasASN returns if we can do ASN lookups
  143. func (g *GeoIP2) HasASN() (bool, error) {
  144. return g.asn.active, nil
  145. }
  146. // GetASN returns the ASN for the IP (as a "as123" string) and the netmask
  147. func (g *GeoIP2) GetASN(ip net.IP) (string, int, error) {
  148. g.asn.l.RLock()
  149. defer g.asn.l.RUnlock()
  150. if !g.asn.active {
  151. return "", 0, fmt.Errorf("ASN db not active")
  152. }
  153. c, err := g.asn.db.ASN(ip)
  154. if err != nil {
  155. return "", 0, fmt.Errorf("lookup ASN for '%s': %s", ip.String(), err)
  156. }
  157. asn := c.AutonomousSystemNumber
  158. netmask := 24
  159. if ip.To4() != nil {
  160. netmask = 48
  161. }
  162. return fmt.Sprintf("as%d", asn), netmask, nil
  163. }
  164. // HasCountry checks if the GeoIP country database is available
  165. func (g *GeoIP2) HasCountry() (bool, error) {
  166. return g.country.active, nil
  167. }
  168. // GetCountry returns the country, continent and netmask for the given IP
  169. func (g *GeoIP2) GetCountry(ip net.IP) (country, continent string, netmask int) {
  170. // Need a read-lock because return value of Country is a pointer, not copy of the struct/object
  171. g.country.l.RLock()
  172. defer g.country.l.RUnlock()
  173. if !g.country.active {
  174. return "", "", 0
  175. }
  176. c, err := g.country.db.Country(ip)
  177. if err != nil {
  178. log.Printf("Could not lookup country for '%s': %s", ip.String(), err)
  179. return "", "", 0
  180. }
  181. country = c.Country.IsoCode
  182. if len(country) > 0 {
  183. country = strings.ToLower(country)
  184. continent = countries.CountryContinent[country]
  185. }
  186. return country, continent, 0
  187. }
  188. // HasLocation returns if the city database is available to return lat/lon information for an IP
  189. func (g *GeoIP2) HasLocation() (bool, error) {
  190. return g.city.active, nil
  191. }
  192. // GetLocation returns a geo.Location object for the given IP
  193. func (g *GeoIP2) GetLocation(ip net.IP) (l *geo.Location, err error) {
  194. // Need a read-lock because return value of City is a pointer, not copy of the struct/object
  195. g.city.l.RLock()
  196. defer g.city.l.RUnlock()
  197. if !g.city.active {
  198. return nil, fmt.Errorf("city db not active")
  199. }
  200. c, err := g.city.db.City(ip)
  201. if err != nil {
  202. log.Printf("Could not lookup CountryRegion for '%s': %s", ip.String(), err)
  203. return
  204. }
  205. l = &geo.Location{
  206. Latitude: float64(c.Location.Latitude),
  207. Longitude: float64(c.Location.Longitude),
  208. Country: strings.ToLower(c.Country.IsoCode),
  209. }
  210. if len(c.Subdivisions) > 0 {
  211. l.Region = strings.ToLower(c.Subdivisions[0].IsoCode)
  212. }
  213. if len(l.Country) > 0 {
  214. l.Continent = countries.CountryContinent[l.Country]
  215. if len(l.Region) > 0 {
  216. l.Region = l.Country + "-" + l.Region
  217. l.RegionGroup = countries.CountryRegionGroup(l.Country, l.Region)
  218. }
  219. }
  220. return
  221. }