├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── circle.yml ├── cmd └── qip │ └── qip.go ├── datx ├── 17monipdb.dat ├── datx.go └── datx_test.go ├── go.mod ├── internal └── proto │ └── proto.go ├── ip17mon.go ├── ip17mon_test.go └── ipdb ├── city.free.ipdb ├── ipdb.go └── ipdb_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 wangtuanjie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test build 2 | 3 | test: 4 | go test -v -race ./... 5 | 6 | build: 7 | go build ./... 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [17mon](http://www.ipip.net/) IP location data for Golang 2 | === 3 | 4 | [![Circle CI](https://circleci.com/gh/wangtuanjie/ip17mon.svg?style=svg)](https://circleci.com/gh/wangtuanjie/ip17mon) 5 | 6 | ## 特性 7 | 8 | * dat/datx 只支持 ipv4 9 | * ipdb 支持 ipv4/ipv6 10 | 11 | ## 安装 12 | 13 | go get github.com/wangtuanjie/ip17mon@latest 14 | 15 | 16 | ## 使用 17 | import ( 18 | "fmt" 19 | "github.com/wangtuanjie/ip17mon" 20 | ) 21 | 22 | func init() { 23 | ip17mon.Init("your data file") 24 | } 25 | 26 | func main() { 27 | loc, err := ip17mon.Find("116.228.111.18") 28 | if err != nil { 29 | fmt.Println("err:", err) 30 | return 31 | } 32 | fmt.Println(loc) 33 | } 34 | 35 | 更多请参考[example](https://github.com/wangtuanjie/ip17mon/tree/master/cmd/qip) 36 | 37 | 38 | 39 | ## 许可证 40 | 41 | 基于 [MIT](https://github.com/wangtuanjie/ip17mon/blob/master/LICENSE) 协议发布 42 | 43 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: golang:1.13 7 | steps: 8 | - checkout 9 | - run: make test 10 | -------------------------------------------------------------------------------- /cmd/qip/qip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/wangtuanjie/ip17mon" 10 | ) 11 | 12 | func stdin() { 13 | scanner := bufio.NewScanner(os.Stdin) 14 | 15 | for scanner.Scan() { 16 | ip := scanner.Text() 17 | if loc, err := ip17mon.Find(ip); err != nil { 18 | fmt.Fprintf(os.Stderr, "%s: %v\n", ip, err) 19 | } else { 20 | fmt.Println(ip, loc.Country, loc.Region, loc.City, loc.Isp) 21 | } 22 | } 23 | 24 | if err := scanner.Err(); err != nil { 25 | fmt.Fprintln(os.Stderr, "Failed:", err) 26 | os.Exit(1) 27 | } 28 | 29 | return 30 | } 31 | 32 | func main() { 33 | 34 | f := flag.String("f", "ipip_12_7.datx", "ip data file support dat/datx/ipdb format") 35 | flag.Parse() 36 | 37 | ip17mon.Init(*f) 38 | 39 | if args := flag.Args(); len(args) > 0 { 40 | if loc, err := ip17mon.Find(args[0]); err != nil { 41 | fmt.Fprintf(os.Stderr, "%s: %v\n", args[0], err) 42 | os.Exit(1) 43 | } else { 44 | fmt.Println(loc.Country, loc.Region, loc.City, loc.Isp) 45 | } 46 | } else { 47 | stdin() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /datx/17monipdb.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtuanjie/ip17mon/5ebb9094c7eab71d18789e15424e352b818a1506/datx/17monipdb.dat -------------------------------------------------------------------------------- /datx/datx.go: -------------------------------------------------------------------------------- 1 | package datx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io/ioutil" 7 | "net" 8 | "strings" 9 | 10 | . "github.com/wangtuanjie/ip17mon/internal/proto" 11 | ) 12 | 13 | func New(dataFile string) (loc Locator, err error) { 14 | 15 | data, err := ioutil.ReadFile(dataFile) 16 | if err != nil { 17 | return 18 | } 19 | if strings.HasSuffix(dataFile, ".datx") { 20 | loc = NewWithDatx(data) 21 | } else { 22 | loc = NewWithData(data) 23 | } 24 | return 25 | } 26 | 27 | func NewWithData(data []byte) Locator { 28 | loc := new(locator) 29 | loc.init(data) 30 | return loc 31 | } 32 | 33 | func NewWithDatx(data []byte) Locator { 34 | loc := new(locator) 35 | loc.initX(data) 36 | return loc 37 | } 38 | 39 | type locator struct { 40 | index [256]int 41 | indexData []uint32 42 | textStartIndex []int 43 | textLengthIndex []int 44 | textData []byte 45 | } 46 | 47 | type Range struct { 48 | Start net.IP 49 | End net.IP 50 | } 51 | 52 | // Find locationInfo by ip string 53 | // It will return err when ipstr is not a valid format 54 | func (loc *locator) Find(ipstr string) (info *LocationInfo, err error) { 55 | ip := net.ParseIP(ipstr) 56 | if ip == nil || ip.To4() == nil { 57 | err = ErrUnsupportedIP 58 | return 59 | } 60 | info = loc.FindByUint(binary.BigEndian.Uint32([]byte(ip.To4()))) 61 | return 62 | } 63 | 64 | // Find locationInfo by uint32 65 | func (loc *locator) FindByUint(ip uint32) (info *LocationInfo) { 66 | 67 | idx := loc.findTextIndex(ip, loc.index[ip>>24]) 68 | start := loc.textStartIndex[idx] 69 | return newLocationInfo(loc.textData[start : start+loc.textLengthIndex[idx]]) 70 | } 71 | 72 | func (loc *locator) Dump() (rs []Range, locs []*LocationInfo) { 73 | 74 | rs = make([]Range, 0, len(loc.indexData)) 75 | locs = make([]*LocationInfo, 0, len(loc.indexData)) 76 | 77 | for i := 1; i < len(loc.indexData); i++ { 78 | s, e := loc.indexData[i-1], loc.indexData[i] 79 | off := loc.textStartIndex[i] 80 | l := newLocationInfo(loc.textData[off : off+loc.textLengthIndex[i]]) 81 | rs = append(rs, Range{ipOf(s), ipOf(e)}) 82 | locs = append(locs, l) 83 | } 84 | return 85 | } 86 | 87 | func ipOf(n uint32) net.IP { 88 | b := make([]byte, 4) 89 | binary.BigEndian.PutUint32(b, n) 90 | return net.IP(b) 91 | } 92 | 93 | // binary search 94 | func (loc *locator) findTextIndex(ip uint32, start int) int { 95 | 96 | end := len(loc.indexData) - 1 97 | for start < end { 98 | mid := (start + end) / 2 99 | if ip > loc.indexData[mid] { 100 | start = mid + 1 101 | } else { 102 | end = mid 103 | } 104 | } 105 | 106 | if loc.indexData[end] >= ip { 107 | return end 108 | } else { 109 | return start 110 | } 111 | 112 | } 113 | 114 | func (loc *locator) init(data []byte) { 115 | 116 | offset := int(binary.BigEndian.Uint32(data[:4])) 117 | textOff := offset - 1024 118 | 119 | loc.textData = data[textOff:] 120 | for i := 0; i < 256; i++ { 121 | off := 4 + i*4 122 | loc.index[i] = int(binary.LittleEndian.Uint32(data[off : off+4])) 123 | } 124 | 125 | nidx := (textOff - 4 - 1024) / 8 126 | 127 | loc.indexData = make([]uint32, nidx) 128 | loc.textStartIndex = make([]int, nidx) 129 | loc.textLengthIndex = make([]int, nidx) 130 | 131 | for i := 0; i < nidx; i++ { 132 | off := 4 + 1024 + i*8 133 | loc.indexData[i] = binary.BigEndian.Uint32(data[off : off+4]) 134 | loc.textStartIndex[i] = int(uint32(data[off+4]) | uint32(data[off+5])<<8 | uint32(data[off+6])<<16) 135 | loc.textLengthIndex[i] = int(data[off+7]) 136 | } 137 | return 138 | } 139 | 140 | // datx format 141 | func (loc *locator) initX(data []byte) { 142 | 143 | offset := int(binary.BigEndian.Uint32(data[:4])) 144 | textOff := offset - 256*256*4 145 | loc.textData = data[textOff:] 146 | for i := 0; i < 256; i++ { 147 | // datx格式使用了ipv4的前两个字节做为索引字段,出于对data格式兼容考虑这里只使用首字节做为索引字段 148 | // 由于我们使用二分查找, 这个点上认为对性能不会有任何影响 149 | off := 4 + i*256*4 150 | loc.index[i] = int(binary.LittleEndian.Uint32(data[off : off+4])) 151 | } 152 | 153 | nidx := (textOff - 4 - 256*256*4) / 9 154 | 155 | loc.indexData = make([]uint32, nidx) 156 | loc.textStartIndex = make([]int, nidx) 157 | loc.textLengthIndex = make([]int, nidx) 158 | 159 | for i := 0; i < nidx; i++ { 160 | off := 4 + 256*256*4 + i*9 161 | loc.indexData[i] = binary.BigEndian.Uint32(data[off : off+4]) 162 | loc.textStartIndex[i] = int(uint32(data[off+4]) | uint32(data[off+5])<<8 | uint32(data[off+6])<<16) 163 | loc.textLengthIndex[i] = int(uint32(data[off+8]) | uint32(data[off+7])<<8) 164 | } 165 | return 166 | } 167 | 168 | func newLocationInfo(str []byte) *LocationInfo { 169 | 170 | var info *LocationInfo 171 | 172 | fields := bytes.Split(str, []byte("\t")) 173 | if len(fields) < 4 { 174 | panic("unexpected ip info:" + string(str)) 175 | } 176 | info = &LocationInfo{ 177 | Country: string(fields[0]), 178 | Region: string(fields[1]), 179 | City: string(fields[2]), 180 | } 181 | if len(fields) >= 5 { 182 | info.Isp = string(fields[4]) 183 | } 184 | 185 | { 186 | if len(info.Country) == 0 { 187 | info.Country = Null 188 | } 189 | if len(info.Region) == 0 { 190 | info.Region = Null 191 | } 192 | if len(info.City) == 0 { 193 | info.City = Null 194 | } 195 | if len(info.Isp) == 0 { 196 | info.Isp = Null 197 | } 198 | } 199 | 200 | return info 201 | } 202 | -------------------------------------------------------------------------------- /datx/datx_test.go: -------------------------------------------------------------------------------- 1 | package datx 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/wangtuanjie/ip17mon/internal/proto" 7 | ) 8 | 9 | const data = "17monipdb.dat" 10 | 11 | func TestFind(t *testing.T) { 12 | l, err := New(data) 13 | if err != nil { 14 | t.Fatal("New failed:", err) 15 | } 16 | info, err := l.Find("115.231.237.124") 17 | 18 | if err != nil { 19 | t.Fatal("Find failed:", err) 20 | } 21 | 22 | if info.Country != "中国" { 23 | t.Fatal("country expect = 中国, but actual =", info.Country) 24 | } 25 | 26 | if info.Region != "浙江" { 27 | t.Fatal("region expect = 浙江, but actual =", info.Region) 28 | } 29 | 30 | if info.City != "嘉兴" { 31 | t.Fatal("city expect = 嘉兴, but actual =", info.City) 32 | } 33 | 34 | if info.Isp != Null { 35 | t.Fatal("isp expect = Null, but actual =", info.Isp) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wangtuanjie/ip17mon 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /internal/proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import "errors" 4 | 5 | type LocationInfo struct { 6 | Country string 7 | Region string 8 | City string 9 | Isp string 10 | } 11 | 12 | type Locator interface { 13 | Find(ipstr string) (*LocationInfo, error) 14 | } 15 | 16 | var ErrUnsupportedIP = errors.New("unsupported ip format") 17 | 18 | const Null = "N/A" 19 | -------------------------------------------------------------------------------- /ip17mon.go: -------------------------------------------------------------------------------- 1 | package ip17mon 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/wangtuanjie/ip17mon/datx" 9 | "github.com/wangtuanjie/ip17mon/ipdb" 10 | "github.com/wangtuanjie/ip17mon/internal/proto" 11 | ) 12 | 13 | var def Locator 14 | 15 | type ( 16 | Locator = proto.Locator 17 | LocationInfo = proto.LocationInfo 18 | ) 19 | 20 | func Init(dataFile string) { 21 | var err error 22 | def, err = New(dataFile) 23 | if err != nil { 24 | panic(err) 25 | } 26 | } 27 | 28 | func InitWithDatx(b []byte) { 29 | def = datx.NewWithDatx(b) 30 | } 31 | 32 | func InitWithIpdb(b []byte) { 33 | var err error 34 | def, err = ipdb.NewWith(b) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | 40 | func Find(ipstr string) (*LocationInfo, error) { 41 | return def.Find(ipstr) 42 | } 43 | 44 | func New(dataFile string) (loc Locator, err error) { 45 | 46 | switch strings.ToLower(filepath.Ext(dataFile)) { 47 | case ".dat", ".datx": 48 | return datx.New(dataFile) 49 | case ".ipdb": 50 | return ipdb.New(dataFile) 51 | default: 52 | return nil, errors.New("unsupported file") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ip17mon_test.go: -------------------------------------------------------------------------------- 1 | package ip17mon 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAll(t *testing.T) { 8 | 9 | l1, err := New("datx/17monipdb.dat") 10 | if err != nil { 11 | t.Fatal("New failed:", err) 12 | } 13 | 14 | l2, err := New("ipdb/city.free.ipdb") 15 | if err != nil { 16 | t.Fatal("New failed:", err) 17 | } 18 | 19 | ip := "115.231.237.124" 20 | 21 | info1, err := l1.Find(ip) 22 | if err != nil { 23 | t.Fatal("l1.Find failed:", err) 24 | } 25 | 26 | info2, err := l2.Find(ip) 27 | if err != nil { 28 | t.Fatal("l2.Find failed:", err) 29 | } 30 | 31 | if *info1 != *info2 { 32 | t.Fatalf("info: %v != %v", *info1, *info2) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ipdb/city.free.ipdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtuanjie/ip17mon/5ebb9094c7eab71d18789e15424e352b818a1506/ipdb/city.free.ipdb -------------------------------------------------------------------------------- /ipdb/ipdb.go: -------------------------------------------------------------------------------- 1 | package ipdb 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net" 8 | "strings" 9 | 10 | . "github.com/wangtuanjie/ip17mon/internal/proto" 11 | ) 12 | 13 | func New(dataFile string) (Locator, error) { 14 | 15 | b, err := ioutil.ReadFile(dataFile) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return NewWith(b) 20 | } 21 | 22 | func NewWith(b []byte) (Locator, error) { 23 | 24 | loc := new(locator) 25 | if err := loc.init(b); err != nil { 26 | return nil, err 27 | } 28 | return loc, nil 29 | } 30 | 31 | type locator struct { 32 | data []byte 33 | md metadata 34 | 35 | v4Offset uint32 36 | 37 | countryField int 38 | regionField int 39 | cityField int 40 | ispField int 41 | } 42 | 43 | type metadata struct { 44 | Build int64 `json:"build"` 45 | IPVersion uint16 `json:"ip_version"` 46 | Languages map[string]int `json:"languages"` 47 | NodeCount uint32 `json:"node_count"` 48 | TotalSize int `json:"total_size"` 49 | Fields []string `json:"fields"` 50 | } 51 | 52 | func (l *locator) Find(ipstr string) (*LocationInfo, error) { 53 | 54 | ip := net.ParseIP(ipstr) 55 | if ip == nil { 56 | return nil, ErrUnsupportedIP 57 | } 58 | 59 | var node uint32 60 | bitCount := 128 61 | 62 | if ipv4 := ip.To4(); ipv4 != nil { 63 | if l.md.IPVersion&0x01 == 0 { 64 | return nil, ErrUnsupportedIP 65 | } 66 | node = l.v4Offset 67 | bitCount = 32 68 | ip = ipv4 69 | } else if l.md.IPVersion&0x02 == 0 { 70 | return nil, ErrUnsupportedIP 71 | } 72 | 73 | for i := 0; i < bitCount; i++ { 74 | if node > l.md.NodeCount { 75 | return l.newLocationInfo(node), nil 76 | } 77 | node = l.nextNode(node, ((0xFF&int(ip[i>>3]))>>uint(7-(i%8)))&1 == 1) 78 | } 79 | 80 | return nil, ErrUnsupportedIP 81 | } 82 | 83 | func (l *locator) init(b []byte) error { 84 | 85 | metaSize := int(binary.BigEndian.Uint32(b[:4])) 86 | b = b[4:] 87 | 88 | if err := json.Unmarshal(b[:metaSize], &l.md); err != nil { 89 | return err 90 | } 91 | l.data = b[metaSize:] 92 | 93 | var node uint32 94 | for i := 0; i < 96 && node < l.md.NodeCount; i++ { 95 | if i >= 80 { 96 | node = l.nextNode(node, true) 97 | } else { 98 | node = l.nextNode(node, false) 99 | } 100 | } 101 | l.v4Offset = node 102 | 103 | l.countryField = -1 104 | l.regionField = -1 105 | l.cityField = -1 106 | l.ispField = -1 107 | 108 | for i, f := range l.md.Fields { 109 | switch f { 110 | case "country_name": 111 | l.countryField = i 112 | case "region_name": 113 | l.regionField = i 114 | case "city_name": 115 | l.cityField = i 116 | case "isp_domain": 117 | l.ispField = i 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (l *locator) nextNode(node uint32, right bool) uint32 { 125 | 126 | off := node * 8 127 | if right { 128 | off += 4 129 | } 130 | return binary.BigEndian.Uint32(l.data[off : off+4]) 131 | } 132 | 133 | func (l *locator) newLocationInfo(node uint32) *LocationInfo { 134 | 135 | off := node - l.md.NodeCount + l.md.NodeCount*8 136 | size := uint32(binary.BigEndian.Uint16(l.data[off : off+2])) 137 | text := l.data[off+2 : off+2+size] 138 | fields := strings.Split(string(text), "\t") 139 | 140 | fieldOf := func(i int) string { 141 | if i >= 0 && i < len(fields) && fields[i] != "" { 142 | return fields[i] 143 | } 144 | return Null 145 | } 146 | 147 | return &LocationInfo{ 148 | Country: fieldOf(l.countryField), 149 | Region: fieldOf(l.regionField), 150 | City: fieldOf(l.cityField), 151 | Isp: fieldOf(l.ispField), 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /ipdb/ipdb_test.go: -------------------------------------------------------------------------------- 1 | package ipdb 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIpdb(t *testing.T) { 8 | 9 | l, err := New("city.free.ipdb") 10 | info, err := l.Find("115.231.237.124") 11 | if err != nil { 12 | t.Fatal("Find failed:", err) 13 | } 14 | 15 | if info.Country != "中国" { 16 | t.Fatal("country expect = 中国, but actual =", info.Country) 17 | } 18 | 19 | if info.Region != "浙江" { 20 | t.Fatal("region expect = 浙江, but actual =", info.Region) 21 | } 22 | 23 | if info.City != "嘉兴" { 24 | t.Fatal("city expect = 嘉兴, but actual =", info.City) 25 | } 26 | 27 | if info.Isp != "N/A" { 28 | t.Fatal("isp expect = N/A, but actual =", info.Isp) 29 | } 30 | } 31 | --------------------------------------------------------------------------------