db.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. // Copyright 2009 The freegeoip authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. package freegeoip
  5. import (
  6. "compress/gzip"
  7. "crypto/md5"
  8. "encoding/hex"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "io/ioutil"
  13. "math"
  14. "net"
  15. "net/http"
  16. "net/url"
  17. "os"
  18. "path/filepath"
  19. "sync"
  20. "time"
  21. "github.com/howeyc/fsnotify"
  22. "github.com/oschwald/maxminddb-golang"
  23. )
  24. var (
  25. // ErrUnavailable may be returned by DB.Lookup when the database
  26. // points to a URL and is not yet available because it's being
  27. // downloaded in background.
  28. ErrUnavailable = errors.New("no database available")
  29. // Local cached copy of a database downloaded from a URL.
  30. defaultDB = filepath.Join(os.TempDir(), "freegeoip", "db.gz")
  31. // MaxMindDB is the URL of the free MaxMind GeoLite2 database.
  32. MaxMindDB = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz"
  33. )
  34. // DB is the IP geolocation database.
  35. type DB struct {
  36. file string // Database file name.
  37. checksum string // MD5 of the unzipped database file
  38. reader *maxminddb.Reader // Actual db object.
  39. notifyQuit chan struct{} // Stop auto-update and watch goroutines.
  40. notifyOpen chan string // Notify when a db file is open.
  41. notifyError chan error // Notify when an error occurs.
  42. notifyInfo chan string // Notify random actions for logging
  43. closed bool // Mark this db as closed.
  44. lastUpdated time.Time // Last time the db was updated.
  45. mu sync.RWMutex // Protects all the above.
  46. updateInterval time.Duration // Update interval.
  47. maxRetryInterval time.Duration // Max retry interval in case of failure.
  48. }
  49. // Open creates and initializes a DB from a local file.
  50. //
  51. // The database file is monitored by fsnotify and automatically
  52. // reloads when the file is updated or overwritten.
  53. func Open(dsn string) (*DB, error) {
  54. db := &DB{
  55. file: dsn,
  56. notifyQuit: make(chan struct{}),
  57. notifyOpen: make(chan string, 1),
  58. notifyError: make(chan error, 1),
  59. notifyInfo: make(chan string, 1),
  60. }
  61. err := db.openFile()
  62. if err != nil {
  63. db.Close()
  64. return nil, err
  65. }
  66. err = db.watchFile()
  67. if err != nil {
  68. db.Close()
  69. return nil, fmt.Errorf("fsnotify failed for %s: %s", dsn, err)
  70. }
  71. return db, nil
  72. }
  73. // MaxMindUpdateURL generates the URL for MaxMind paid databases.
  74. func MaxMindUpdateURL(hostname, productID, userID, licenseKey string) (string, error) {
  75. limiter := func(r io.Reader) *io.LimitedReader {
  76. return &io.LimitedReader{R: r, N: 1 << 30}
  77. }
  78. baseurl := "https://" + hostname + "/app/"
  79. // Get the file name for the product ID.
  80. u := baseurl + "update_getfilename?product_id=" + productID
  81. resp, err := http.Get(u)
  82. if err != nil {
  83. return "", err
  84. }
  85. defer resp.Body.Close()
  86. md5hash := md5.New()
  87. _, err = io.Copy(md5hash, limiter(resp.Body))
  88. if err != nil {
  89. return "", err
  90. }
  91. sum := md5hash.Sum(nil)
  92. hexdigest1 := hex.EncodeToString(sum[:])
  93. // Get our client IP address.
  94. resp, err = http.Get(baseurl + "update_getipaddr")
  95. if err != nil {
  96. return "", err
  97. }
  98. defer resp.Body.Close()
  99. md5hash = md5.New()
  100. io.WriteString(md5hash, licenseKey)
  101. _, err = io.Copy(md5hash, limiter(resp.Body))
  102. if err != nil {
  103. return "", err
  104. }
  105. sum = md5hash.Sum(nil)
  106. hexdigest2 := hex.EncodeToString(sum[:])
  107. // Generate the URL.
  108. params := url.Values{
  109. "db_md5": {hexdigest1},
  110. "challenge_md5": {hexdigest2},
  111. "user_id": {userID},
  112. "edition_id": {productID},
  113. }
  114. u = baseurl + "update_secure?" + params.Encode()
  115. return u, nil
  116. }
  117. // OpenURL creates and initializes a DB from a URL.
  118. // It automatically downloads and updates the file in background, and
  119. // keeps a local copy on $TMPDIR.
  120. func OpenURL(url string, updateInterval, maxRetryInterval time.Duration) (*DB, error) {
  121. db := &DB{
  122. file: defaultDB,
  123. notifyQuit: make(chan struct{}),
  124. notifyOpen: make(chan string, 1),
  125. notifyError: make(chan error, 1),
  126. notifyInfo: make(chan string, 1),
  127. updateInterval: updateInterval,
  128. maxRetryInterval: maxRetryInterval,
  129. }
  130. db.openFile() // Optional, might fail.
  131. go db.autoUpdate(url)
  132. err := db.watchFile()
  133. if err != nil {
  134. db.Close()
  135. return nil, fmt.Errorf("fsnotify failed for %s: %s", db.file, err)
  136. }
  137. return db, nil
  138. }
  139. func (db *DB) watchFile() error {
  140. watcher, err := fsnotify.NewWatcher()
  141. if err != nil {
  142. return err
  143. }
  144. dbdir, err := db.makeDir()
  145. if err != nil {
  146. return err
  147. }
  148. go db.watchEvents(watcher)
  149. return watcher.Watch(dbdir)
  150. }
  151. func (db *DB) watchEvents(watcher *fsnotify.Watcher) {
  152. for {
  153. select {
  154. case ev := <-watcher.Event:
  155. if ev.Name == db.file && (ev.IsCreate() || ev.IsModify()) {
  156. db.openFile()
  157. }
  158. case <-watcher.Error:
  159. case <-db.notifyQuit:
  160. watcher.Close()
  161. return
  162. }
  163. time.Sleep(time.Second) // Suppress high-rate events.
  164. }
  165. }
  166. func (db *DB) openFile() error {
  167. reader, checksum, err := db.newReader(db.file)
  168. if err != nil {
  169. return err
  170. }
  171. stat, err := os.Stat(db.file)
  172. if err != nil {
  173. return err
  174. }
  175. db.setReader(reader, stat.ModTime(), checksum)
  176. return nil
  177. }
  178. func (db *DB) newReader(dbfile string) (*maxminddb.Reader, string, error) {
  179. f, err := os.Open(dbfile)
  180. if err != nil {
  181. return nil, "", err
  182. }
  183. defer f.Close()
  184. gzf, err := gzip.NewReader(f)
  185. if err != nil {
  186. return nil, "", err
  187. }
  188. defer gzf.Close()
  189. b, err := ioutil.ReadAll(gzf)
  190. if err != nil {
  191. return nil, "", err
  192. }
  193. checksum := fmt.Sprintf("%x", md5.Sum(b))
  194. mmdb, err := maxminddb.FromBytes(b)
  195. return mmdb, checksum, err
  196. }
  197. func (db *DB) setReader(reader *maxminddb.Reader, modtime time.Time, checksum string) {
  198. db.mu.Lock()
  199. defer db.mu.Unlock()
  200. if db.closed {
  201. reader.Close()
  202. return
  203. }
  204. if db.reader != nil {
  205. db.reader.Close()
  206. }
  207. db.reader = reader
  208. db.lastUpdated = modtime.UTC()
  209. db.checksum = checksum
  210. select {
  211. case db.notifyOpen <- db.file:
  212. default:
  213. }
  214. }
  215. func (db *DB) autoUpdate(url string) {
  216. backoff := time.Second
  217. for {
  218. db.sendInfo("starting update")
  219. err := db.runUpdate(url)
  220. if err != nil {
  221. bs := backoff.Seconds()
  222. ms := db.maxRetryInterval.Seconds()
  223. backoff = time.Duration(math.Min(bs*math.E, ms)) * time.Second
  224. db.sendError(fmt.Errorf("download failed (will retry in %s): %s", backoff, err))
  225. } else {
  226. backoff = db.updateInterval
  227. }
  228. db.sendInfo("finished update")
  229. select {
  230. case <-db.notifyQuit:
  231. return
  232. case <-time.After(backoff):
  233. // Sleep till time for the next update attempt.
  234. }
  235. }
  236. }
  237. func (db *DB) runUpdate(url string) error {
  238. yes, err := db.needUpdate(url)
  239. if err != nil {
  240. return err
  241. }
  242. if !yes {
  243. return nil
  244. }
  245. tmpfile, err := db.download(url)
  246. if err != nil {
  247. return err
  248. }
  249. err = db.renameFile(tmpfile)
  250. if err != nil {
  251. // Cleanup the tempfile if renaming failed.
  252. os.RemoveAll(tmpfile)
  253. }
  254. return err
  255. }
  256. func (db *DB) needUpdate(url string) (bool, error) {
  257. stat, err := os.Stat(db.file)
  258. if err != nil {
  259. return true, nil // Local db is missing, must be downloaded.
  260. }
  261. resp, err := http.Head(url)
  262. if err != nil {
  263. return false, err
  264. }
  265. defer resp.Body.Close()
  266. // Check X-Database-MD5 if it exists
  267. headerMd5 := resp.Header.Get("X-Database-MD5")
  268. if len(headerMd5) > 0 && db.checksum != headerMd5 {
  269. return true, nil
  270. }
  271. if stat.Size() != resp.ContentLength {
  272. return true, nil
  273. }
  274. return false, nil
  275. }
  276. func (db *DB) download(url string) (tmpfile string, err error) {
  277. resp, err := http.Get(url)
  278. if err != nil {
  279. return "", err
  280. }
  281. defer resp.Body.Close()
  282. tmpfile = filepath.Join(os.TempDir(),
  283. fmt.Sprintf("_freegeoip.%d.db.gz", time.Now().UnixNano()))
  284. f, err := os.Create(tmpfile)
  285. if err != nil {
  286. return "", err
  287. }
  288. defer f.Close()
  289. _, err = io.Copy(f, resp.Body)
  290. if err != nil {
  291. return "", err
  292. }
  293. return tmpfile, nil
  294. }
  295. func (db *DB) makeDir() (dbdir string, err error) {
  296. dbdir = filepath.Dir(db.file)
  297. _, err = os.Stat(dbdir)
  298. if err != nil {
  299. err = os.MkdirAll(dbdir, 0755)
  300. if err != nil {
  301. return "", err
  302. }
  303. }
  304. return dbdir, nil
  305. }
  306. func (db *DB) renameFile(name string) error {
  307. os.Rename(db.file, db.file+".bak") // Optional, might fail.
  308. _, err := db.makeDir()
  309. if err != nil {
  310. return err
  311. }
  312. return os.Rename(name, db.file)
  313. }
  314. // Date returns the UTC date the database file was last modified.
  315. // If no database file has been opened the behaviour of Date is undefined.
  316. func (db *DB) Date() time.Time {
  317. db.mu.RLock()
  318. defer db.mu.RUnlock()
  319. return db.lastUpdated
  320. }
  321. // NotifyClose returns a channel that is closed when the database is closed.
  322. func (db *DB) NotifyClose() <-chan struct{} {
  323. return db.notifyQuit
  324. }
  325. // NotifyOpen returns a channel that notifies when a new database is
  326. // loaded or reloaded. This can be used to monitor background updates
  327. // when the DB points to a URL.
  328. func (db *DB) NotifyOpen() (filename <-chan string) {
  329. return db.notifyOpen
  330. }
  331. // NotifyError returns a channel that notifies when an error occurs
  332. // while downloading or reloading a DB that points to a URL.
  333. func (db *DB) NotifyError() (errChan <-chan error) {
  334. return db.notifyError
  335. }
  336. // NotifyInfo returns a channel that notifies informational messages
  337. // while downloading or reloading.
  338. func (db *DB) NotifyInfo() <-chan string {
  339. return db.notifyInfo
  340. }
  341. func (db *DB) sendError(err error) {
  342. db.mu.RLock()
  343. defer db.mu.RUnlock()
  344. if db.closed {
  345. return
  346. }
  347. select {
  348. case db.notifyError <- err:
  349. default:
  350. }
  351. }
  352. func (db *DB) sendInfo(message string) {
  353. db.mu.RLock()
  354. defer db.mu.RUnlock()
  355. if db.closed {
  356. return
  357. }
  358. select {
  359. case db.notifyInfo <- message:
  360. default:
  361. }
  362. }
  363. // Lookup performs a database lookup of the given IP address, and stores
  364. // the response into the result value. The result value must be a struct
  365. // with specific fields and tags as described here:
  366. // https://godoc.org/github.com/oschwald/maxminddb-golang#Reader.Lookup
  367. //
  368. // See the DefaultQuery for an example of the result struct.
  369. func (db *DB) Lookup(addr net.IP, result interface{}) error {
  370. db.mu.RLock()
  371. defer db.mu.RUnlock()
  372. if db.reader != nil {
  373. return db.reader.Lookup(addr, result)
  374. }
  375. return ErrUnavailable
  376. }
  377. // DefaultQuery is the default query used for database lookups.
  378. type DefaultQuery struct {
  379. Continent struct {
  380. Names map[string]string `maxminddb:"names"`
  381. } `maxminddb:"continent"`
  382. Country struct {
  383. ISOCode string `maxminddb:"iso_code"`
  384. Names map[string]string `maxminddb:"names"`
  385. } `maxminddb:"country"`
  386. Region []struct {
  387. ISOCode string `maxminddb:"iso_code"`
  388. Names map[string]string `maxminddb:"names"`
  389. } `maxminddb:"subdivisions"`
  390. City struct {
  391. Names map[string]string `maxminddb:"names"`
  392. } `maxminddb:"city"`
  393. Location struct {
  394. Latitude float64 `maxminddb:"latitude"`
  395. Longitude float64 `maxminddb:"longitude"`
  396. MetroCode uint `maxminddb:"metro_code"`
  397. TimeZone string `maxminddb:"time_zone"`
  398. } `maxminddb:"location"`
  399. Postal struct {
  400. Code string `maxminddb:"code"`
  401. } `maxminddb:"postal"`
  402. }
  403. // Close closes the database.
  404. func (db *DB) Close() {
  405. db.mu.Lock()
  406. defer db.mu.Unlock()
  407. if !db.closed {
  408. db.closed = true
  409. close(db.notifyQuit)
  410. close(db.notifyOpen)
  411. close(db.notifyError)
  412. close(db.notifyInfo)
  413. }
  414. if db.reader != nil {
  415. db.reader.Close()
  416. db.reader = nil
  417. }
  418. }