forked from nullitics/nullitics
-
Notifications
You must be signed in to change notification settings - Fork 0
/
geo.go
150 lines (137 loc) · 2.99 KB
/
geo.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package nullitics
import (
"archive/zip"
"bytes"
"encoding/csv"
"errors"
"io"
"net"
"os"
"sort"
"strings"
)
// GeoDB is a file location for a GeoLite CSV database. By default the location
// is set to $GEODB environment variable, but can be changed if needed.
var GeoDB GeoFinder
func init() {
if GeoDB, _ = NewGeoDB(os.Getenv("GEODB")); GeoDB == nil {
GeoDB = &geodb{}
}
}
type geodb []ipRange
// GeoFinder is an interface, that can find the country ISO code by the IP
// address.
type GeoFinder interface {
Find(ip string) string
}
type ipRange struct {
Net *net.IPNet
Country string
}
// NewGeoDB reads a GeoLite CSV zip file and returns the geo database, or an error.
func NewGeoDB(zipfile string) (GeoFinder, error) {
f, err := os.Open(zipfile)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
zf, err := zip.NewReader(f, fi.Size())
if err != nil {
return nil, err
}
var blocks, countries *zip.File
for _, f := range zf.File {
if strings.HasSuffix(f.Name, "-Blocks-IPv4.csv") {
blocks = f
} else if strings.HasSuffix(f.Name, "-Country-Locations-en.csv") {
countries = f
}
}
if blocks == nil || countries == nil {
return nil, errors.New("ZIP does not contains blocks or countries")
}
db := geodb{}
cn := map[string]string{}
if err := readCSV(countries, []string{"geoname_id", "country_iso_code"}, func(row []string) error {
cn[row[0]] = row[1]
return nil
}); err != nil {
return nil, err
}
if err := readCSV(blocks, []string{"network", "geoname_id"}, func(row []string) error {
network, id := row[0], row[1]
country, ok := cn[id]
if !ok {
// Some ranges may not contain country data
return nil
}
_, ipnet, err := net.ParseCIDR(network)
if err != nil {
return err
}
db = append(db, ipRange{Net: ipnet, Country: country})
return nil
}); err != nil {
return nil, err
}
return db, nil
}
func readCSV(file *zip.File, fields []string, f func([]string) error) error {
r, err := file.Open()
if err != nil {
return err
}
defer r.Close()
rows := csv.NewReader(r)
header, err := rows.Read()
if err != nil {
return err
}
indices := make([]int, len(fields))
for i, f := range fields {
found := false
for j, h := range header {
if h == f {
indices[i] = j
found = true
}
}
if !found {
return errors.New("missing field: " + f)
}
}
cols := make([]string, len(fields))
for {
row, err := rows.Read()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
for i, j := range indices {
cols[i] = row[j]
}
if err := f(cols); err != nil {
return err
}
}
}
// Find returns the country ISO code for the provided ipv4 address
func (db geodb) Find(ipv4 string) string {
ip := net.ParseIP(ipv4).To4()
if ip == nil {
return ""
}
i := sort.Search(len(db), func(i int) bool {
return bytes.Compare(db[i].Net.IP, ip) > 0 || db[i].Net.Contains(ip)
})
if i < len(db) && db[i].Net.Contains(ip) {
return db[i].Country
}
return ""
}