123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- package asndb
- import (
- "archive/zip"
- "bytes"
- "crypto/md5"
- "encoding/csv"
- "encoding/hex"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net"
- "net/http"
- "os"
- "regexp"
- "strings"
- "sync"
- "git.scraperwall.com/scw/ip"
- privip "git.scraperwall.com/scw/ip"
- "github.com/google/btree"
- )
- const (
- asnFile = "GeoLite2-ASN-CSV.zip"
- asnMd5File = "GeoLite2-ASN-CSV.zip.md5"
- )
- // DB contains a b-tree of ASNs
- type DB struct {
- db *btree.BTree
- mutex sync.Mutex
- privIPs *ip.IP
- }
- // Lookup returns the ASN struct of the network that contains ip
- func (a *DB) Lookup(ip net.IP) *ASN {
- var asn *ASN
- privNet := a.privIPs.Network(ip)
- if privNet != nil {
- pasn, _ := NewASN(privNet.String(), "-1", "Private Network")
- return pasn
- }
- ipNorm := ip.To16()
- dummy := ASN{
- To: &ipNorm,
- }
- a.mutex.Lock()
- defer a.mutex.Unlock()
- a.db.AscendGreaterOrEqual(&dummy, func(item btree.Item) bool {
- asn = item.(*ASN)
- if !asn.Network.Contains(ip) {
- asn, _ = NewASN("0.0.0.0/32", "-1", "Unknown Network")
- }
- return false
- })
- return asn
- }
- // Size returns the number of networks in the database
- func (a *DB) Size() int {
- return a.db.Len()
- }
- // Each iterates over each element in the database
- func (a *DB) Each(f func(a *ASN) bool) {
- a.db.Ascend(func(item btree.Item) bool {
- return f(item.(*ASN))
- })
- }
- // load pulls fresh data from maxmind
- func (a *DB) load(baseURL string) error {
- asndb, err := fromURL(baseURL)
- if err != nil {
- return err
- }
- if asndb == nil {
- return errors.New("asndb is nil")
- }
- a.mutex.Lock()
- defer a.mutex.Unlock()
- a.db = asndb
- return nil
- }
- // fromURL loads data from maxmind and creates an ASNDB with this fresh data
- func fromURL(baseURL string) (*btree.BTree, error) {
- // Get MD5 sum for tar.gz file
- asnMd5URL := baseURL + "/" + asnMd5File
- resp, err := http.Get(asnMd5URL)
- if err != nil {
- return nil, err
- }
- md5Sum, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
- resp.Body.Close()
- asnURL := baseURL + "/" + asnFile
- // Load the tar.gz file
- resp, err = http.Get(asnURL)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("%s status %d", asnURL, resp.StatusCode)
- }
- bodyData, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
- // Build the MD5 sum of the downloaded tar.gz
- hash := md5.New()
- if _, err := io.Copy(hash, bytes.NewReader(bodyData)); err != nil {
- return nil, err
- }
- if string(md5Sum) != hex.EncodeToString(hash.Sum(nil)) {
- log.Println("asndb checksum mismatch")
- return nil, fmt.Errorf("checksum mismatch: %s != %s", md5Sum, hash.Sum(nil))
- }
- // Copy the data to a temporary file for zip to be able to open it
- tmpF, err := ioutil.TempFile("/tmp", "asndb-")
- if err != nil {
- return nil, err
- }
- defer os.Remove(tmpF.Name())
- io.Copy(tmpF, bytes.NewReader(bodyData))
- tmpF.Close()
- return fromFile(tmpF.Name())
- }
- func parseCSV(reader io.Reader) (*btree.BTree, error) {
- csvr := csv.NewReader(reader)
- numMatch := regexp.MustCompile(`^[0-9a-fA-F]+[\.:]`)
- tree := btree.New(8)
- for {
- record, err := csvr.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- log.Fatal(err)
- }
- // ignore the header and anything that doesn't look like an IP
- if !numMatch.MatchString(record[0]) {
- continue
- }
- a, err := NewASN(record[0], record[1], record[2])
- if err != nil {
- return nil, err
- }
- tree.ReplaceOrInsert(a)
- }
- return tree, nil
- }
- func fromFile(filename string) (*btree.BTree, error) {
- zipReader, err := zip.OpenReader(filename)
- if err != nil {
- return nil, err
- }
- defer zipReader.Close()
- buf := bytes.NewBufferString("")
- // find the data in the zip file
- for _, f := range zipReader.File {
- if strings.HasSuffix(f.Name, "GeoLite2-ASN-Blocks-IPv4.csv") || strings.HasSuffix(f.Name, "GeoLite2-ASN-Blocks-IPv6.csv") {
- asn, err := f.Open()
- if err != nil {
- return nil, err
- }
- io.Copy(buf, asn)
- }
- }
- if buf.Len() <= 0 {
- return nil, fmt.Errorf("not enough data")
- }
- // generate the tree
- tree, err := parseCSV(buf)
- if err != nil {
- return nil, err
- }
- return tree, nil
- }
- // New creates a new ASN database. fname denotes the path to the Maxmind ASN CSV file
- func New(baseURLOrFile string) (*DB, error) {
- db := &DB{
- mutex: sync.Mutex{},
- privIPs: privip.NewIP(),
- }
- if strings.HasPrefix(baseURLOrFile, "https://") || strings.HasPrefix(baseURLOrFile, "http://") {
- err := db.load(baseURLOrFile)
- if err != nil {
- return nil, err
- }
- } else {
- var err error
- db.db, err = fromFile(baseURLOrFile)
- if err != nil {
- return nil, err
- }
- }
- return db, nil
- }
|