├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── ipdb ├── README.md ├── ip_seeker.go ├── ip_seeker_test.go ├── metadata.go └── record.go ├── qqwry ├── README.md ├── ip_seeker.go ├── ip_seeker_test.go ├── metadata.go ├── update.go └── utils.go ├── shared ├── errors.go ├── ip_seeker.go ├── record.go └── utils.go └── testdata ├── 17monipdb.datx ├── ipiptest.ipdb └── qqwry.dat /.gitattributes: -------------------------------------------------------------------------------- 1 | testdata/* filter=lfs diff=lfs merge=lfs -binary 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | *.DS_Sotre 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Septs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoIP Seeker 2 | 3 | > WARNING: due to **IPIP.NET** acquired **CZ88.NET**, this library ends maintenance. 4 | 5 | # Supported 6 | 7 | - [x] [CZ88.NET/QQWry](qqwry) 8 | - [x] [IPIP.NET/IPDB](ipdb) 9 | 10 | # Packer References 11 | 12 | - 13 | - 14 | - 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/NiceLabs/geoip-seeker 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiceLabs/geoip-seeker/f4422b327de0f23f6c7b1d6b941579de15429874/go.sum -------------------------------------------------------------------------------- /ipdb/README.md: -------------------------------------------------------------------------------- 1 | # IPIP-IPDB ip database seeker 2 | 3 | ## Notes 4 | 5 | 1. thread safe implementation 6 | 2. no cache (cache to be managed by yourself) 7 | 8 | ## Example 9 | 10 | ```go 11 | package main 12 | 13 | import ( 14 | "encoding/json" 15 | "fmt" 16 | "io/ioutil" 17 | "net" 18 | 19 | "github.com/NiceLabs/geoip-seeker/ipdb" 20 | ) 21 | 22 | func main() { 23 | data, _ := ioutil.ReadFile("testdata/ipiptest.ipdb") 24 | seeker, _ := ipdb.New(data) 25 | 26 | record, _ := seeker.LookupByIP(net.ParseIP("114.114.114.114")) 27 | 28 | encodedRecord, _ := json.MarshalIndent(record, "", " ") 29 | 30 | fmt.Println(seeker.String()) 31 | // IPIP(IPDB) 2018-08-31 385083 [IPv4] 32 | fmt.Println(seeker.RecordCount()) 33 | // 385083 34 | fmt.Println(seeker.BuildTime()) 35 | // 2018-08-31 00:00:00 +0800 CST 36 | fmt.Println(string(encodedRecord)) 37 | // { 38 | // "IP": "114.114.114.114", 39 | // "CountryName": "114DNS.COM", 40 | // "RegionName": "114DNS.COM" 41 | // } 42 | } 43 | ``` 44 | 45 | ## Benchmark 46 | 47 | ``` 48 | $ go test --bench . 49 | goos: darwin 50 | goarch: amd64 51 | pkg: github.com/NiceLabs/geoip-seeker/ipdb 52 | BenchmarkIPSeeker_LookupByIP-12 3452234 354 ns/op 53 | PASS 54 | ok github.com/NiceLabs/geoip-seeker/ipdb 2.952s 55 | ``` 56 | 57 | # References 58 | 59 | 1. https://ipip.net 60 | 2. https://github.com/larryli/ipv4 61 | 62 | -------------------------------------------------------------------------------- /ipdb/ip_seeker.go: -------------------------------------------------------------------------------- 1 | package ipdb 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "net" 7 | "time" 8 | 9 | . "github.com/NiceLabs/geoip-seeker/shared" 10 | ) 11 | 12 | type Seeker struct { 13 | meta *meta 14 | records []byte 15 | fileSize int 16 | language uint8 17 | v4Offset int 18 | } 19 | 20 | func New(data []byte) (*Seeker, error) { 21 | if len(data) == 0 { 22 | return nil, ErrFileSize 23 | } 24 | seeker := new(Seeker) 25 | if meta, err := loadMetadata(data); err != nil { 26 | return nil, err 27 | } else { 28 | seeker.meta = meta 29 | } 30 | seeker.fileSize = len(data) 31 | seeker.records = data[seeker.fileSize-seeker.meta.TotalSize:] 32 | seeker.v4Offset = seeker.findV4Offset() 33 | return seeker, nil 34 | } 35 | 36 | func (s *Seeker) LookupByIP(address net.IP) (record *Record, err error) { 37 | node, err := s.findNode(address) 38 | if err != nil { 39 | return 40 | } 41 | data, err := s.resolveNode(node) 42 | record = makeRecord(string(data), s.language, s.meta.Fields) 43 | record.IP = address 44 | return 45 | } 46 | 47 | func (s *Seeker) IPv4Support() bool { return (s.meta.IPVersion & 0x01) == 0x01 } 48 | func (s *Seeker) IPv6Support() bool { return (s.meta.IPVersion & 0x02) == 0x02 } 49 | func (s *Seeker) RecordCount() uint64 { return uint64(s.meta.NodeCount) } 50 | func (s *Seeker) BuildTime() time.Time { 51 | location := time.FixedZone("CST", +8*3600) 52 | return time.Unix(s.meta.Build, 0).In(location) 53 | } 54 | 55 | func (s *Seeker) LanguageCode(code string) (err error) { 56 | if index, ok := s.meta.Languages[code]; ok { 57 | s.language = index 58 | return 59 | } 60 | return ErrNoSupportLanguage 61 | } 62 | 63 | func (s *Seeker) LanguageNames() (names []string) { 64 | for name := range s.meta.Languages { 65 | names = append(names, name) 66 | } 67 | return 68 | } 69 | 70 | func (s *Seeker) String() string { 71 | return ShowLibraryInfo("IPIP(IPDB)", s) 72 | } 73 | 74 | func (s *Seeker) findNode(ip net.IP) (node int, err error) { 75 | if ip := ip.To4(); ip != nil { 76 | if !s.IPv4Support() { 77 | err = ErrNoSupportIPv4 78 | return 79 | } 80 | return s.searchNode(ip, len(ip)*8) 81 | } 82 | if ip := ip.To16(); ip != nil { 83 | if !s.IPv6Support() { 84 | err = ErrNoSupportIPv6 85 | return 86 | } 87 | return s.searchNode(ip, len(ip)*8) 88 | } 89 | err = ErrIPFormat 90 | return 91 | } 92 | 93 | func (s *Seeker) searchNode(ip net.IP, bitCount int) (node int, err error) { 94 | node = 0 95 | if bitCount == 32 { 96 | node = s.v4Offset 97 | } 98 | for i := 0; i < bitCount; i++ { 99 | if node > s.meta.NodeCount { 100 | break 101 | } 102 | index := ((0xFF & int(ip[i>>3])) >> uint(7-(i%8))) & 1 103 | node = s.readNode(node, index) 104 | } 105 | if node < s.meta.NodeCount { 106 | err = ErrDataNotExists 107 | } 108 | return 109 | } 110 | 111 | func (s *Seeker) readNode(node, index int) int { 112 | offset := node*8 + index*4 113 | return int(binary.BigEndian.Uint32(s.records[offset : offset+4])) 114 | } 115 | 116 | func (s *Seeker) resolveNode(node int) (record []byte, err error) { 117 | resolved := node - s.meta.NodeCount + s.meta.NodeCount*8 118 | if resolved >= s.fileSize { 119 | err = ErrDatabaseError 120 | return 121 | } 122 | size := int(binary.BigEndian.Uint16(s.records[resolved : resolved+2])) 123 | if (resolved + 2 + size) > len(s.records) { 124 | err = ErrDatabaseError 125 | return 126 | } 127 | record = s.records[resolved+2 : resolved+2+size] 128 | return 129 | } 130 | 131 | func (s *Seeker) findV4Offset() (node int) { 132 | for i := 0; i < 96 && node < s.meta.NodeCount; i++ { 133 | if i >= 80 { 134 | node = s.readNode(node, 1) 135 | } else { 136 | node = s.readNode(node, 0) 137 | } 138 | } 139 | return 140 | } 141 | 142 | func loadMetadata(data []byte) (parsed *meta, err error) { 143 | length := int(binary.BigEndian.Uint32(data[:4])) 144 | original := data[4 : 4+length] 145 | 146 | parsed = new(meta) 147 | err = json.Unmarshal(original, parsed) 148 | if err != nil { 149 | return 150 | } 151 | if len(parsed.Languages) == 0 || len(parsed.Fields) == 0 { 152 | err = ErrMetaData 153 | return 154 | } 155 | if len(data) != (4 + length + parsed.TotalSize) { 156 | err = ErrFileSize 157 | return 158 | } 159 | return 160 | } 161 | -------------------------------------------------------------------------------- /ipdb/ip_seeker_test.go: -------------------------------------------------------------------------------- 1 | package ipdb 2 | 3 | import ( 4 | "crypto/rand" 5 | "io/ioutil" 6 | "net" 7 | "testing" 8 | ) 9 | 10 | var seeker *Seeker 11 | 12 | func init() { 13 | data, _ := ioutil.ReadFile("../testdata/ipiptest.ipdb") 14 | seeker, _ = New(data) 15 | } 16 | 17 | func TestMetadata(t *testing.T) { 18 | t.Log(seeker.String()) 19 | t.Log(seeker.LanguageNames()) 20 | t.Log(seeker.LookupByIP(net.IPv4(1, 1, 1, 1))) 21 | _ = seeker.LanguageCode("CN") 22 | } 23 | 24 | func BenchmarkIPSeeker_LookupByIP(b *testing.B) { 25 | var items []net.IP 26 | for i := 0; i < b.N; i++ { 27 | ip := make(net.IP, 4) 28 | _, _ = rand.Read(ip) 29 | items = append(items, ip) 30 | } 31 | b.ReportAllocs() 32 | b.ResetTimer() 33 | for _, item := range items { 34 | _, _ = seeker.LookupByIP(item) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ipdb/metadata.go: -------------------------------------------------------------------------------- 1 | package ipdb 2 | 3 | type meta struct { 4 | Build int64 `json:"build"` 5 | IPVersion uint8 `json:"ip_version"` 6 | Languages map[string]uint8 `json:"languages"` 7 | NodeCount int `json:"node_count"` 8 | TotalSize int `json:"total_size"` 9 | Fields []string `json:"fields"` 10 | } 11 | -------------------------------------------------------------------------------- /ipdb/record.go: -------------------------------------------------------------------------------- 1 | package ipdb 2 | 3 | import ( 4 | "strings" 5 | 6 | . "github.com/NiceLabs/geoip-seeker/shared" 7 | ) 8 | 9 | func makeRecord(data string, language uint8, fields []string) (record *Record) { 10 | record = new(Record) 11 | values := strings.Split(data, "\t") 12 | values = values[language : language+uint8(len(fields))] 13 | mapping := map[string]*string{ 14 | "country_name": &record.CountryName, 15 | "region_name": &record.RegionName, 16 | "city_name": &record.CityName, 17 | "owner_domain": &record.OwnerDomain, 18 | "isp_domain": &record.ISPDomain, 19 | "latitude": &record.Latitude, 20 | "longitude": &record.Longitude, 21 | "timezone": &record.TimeZone, 22 | "utc_offset": &record.UTCOffset, 23 | "idd_code": &record.IDDCode, 24 | "china_admin_code": &record.GB2260Code, 25 | "country_code": &record.ISO3166Alpha2Code, 26 | "country_code3": &record.ISO3166Alpha3Code, 27 | "continent_code": &record.ContinentCode, 28 | "idc": &record.IDC, 29 | "base_station": &record.BaseStation, 30 | "currency_code": &record.CurrencyCode, 31 | "currency_name": &record.CurrencyName, 32 | "european_union": &record.EuropeanUnion, 33 | "anycast": &record.AnyCast, 34 | } 35 | for index, end := language, language+uint8(len(fields)); index < end; index++ { 36 | if input, ok := mapping[fields[index]]; ok { 37 | *input = values[index] 38 | } 39 | } 40 | return record 41 | } 42 | -------------------------------------------------------------------------------- /qqwry/README.md: -------------------------------------------------------------------------------- 1 | # QQWay ip database seeker 2 | 3 | ## Notes 4 | 5 | 1. thread safe implementation 6 | 2. no cache (cache to be managed by yourself) 7 | 3. no encoding convert 8 | 9 | ## Example 10 | 11 | ```go 12 | package main 13 | 14 | import ( 15 | "encoding/json" 16 | "fmt" 17 | "io/ioutil" 18 | "net" 19 | "strings" 20 | 21 | "github.com/NiceLabs/geoip-seeker/qqwry" 22 | 23 | "golang.org/x/text/encoding/simplifiedchinese" 24 | "golang.org/x/text/transform" 25 | ) 26 | 27 | func main() { 28 | data, _ := ioutil.ReadFile("testdata/qqwry.dat") 29 | seeker, _ := qqwry.New(data) 30 | 31 | record, _ := seeker.LookupByIP(net.ParseIP("114.114.114.114")) 32 | fromGBKtoUTF8(&record.CountryName) 33 | fromGBKtoUTF8(&record.RegionName) 34 | 35 | encodedRecord, _ := json.MarshalIndent(record, "", " ") 36 | 37 | fmt.Println(seeker.String()) 38 | // QQWry 2020-07-30 525793 [IPv4] 39 | fmt.Println(seeker.BuildTime()) 40 | // 2020-07-30 00:00:00 +0800 CST 41 | fmt.Println(seeker.RecordCount()) 42 | // 525793 43 | fmt.Println(string(encodedRecord)) 44 | // { 45 | // "IP": "114.114.114.114", 46 | // "BeginIP": "114.114.114.114", 47 | // "EndIP": "114.114.114.114", 48 | // "CountryName": "江苏省南京市", 49 | // "RegionName": "南京信风网络科技有限公司GreatbitDNS服务器" 50 | // } 51 | } 52 | 53 | func fromGBKtoUTF8(value *string) { 54 | reader := transform.NewReader( 55 | strings.NewReader(*value), 56 | simplifiedchinese.GBK.NewDecoder(), 57 | ) 58 | data, _ := ioutil.ReadAll(reader) 59 | *value = string(data) 60 | } 61 | ``` 62 | 63 | ## Benchmark 64 | 65 | ``` 66 | $ go test --bench . 67 | goos: darwin 68 | goarch: amd64 69 | pkg: github.com/NiceLabs/geoip-seeker/qqwry 70 | BenchmarkIPSeeker_LookupByIP-12 2537727 470 ns/op 71 | PASS 72 | ok github.com/NiceLabs/geoip-seeker/qqwry 2.325s 73 | ``` 74 | 75 | # References 76 | 77 | 1. https://web.archive.org/web/20140423114336/http://lumaqq.linuxsir.org/article/qqwry_format_detail.html 78 | 2. http://sewm.pku.edu.cn/src/other/qqwry/qqwry_format_detail.pdf 79 | -------------------------------------------------------------------------------- /qqwry/ip_seeker.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | 7 | "github.com/NiceLabs/geoip-seeker/shared" 8 | ) 9 | 10 | type Seeker struct { 11 | data []byte 12 | indexes []*index 13 | } 14 | 15 | func New(data []byte) (*Seeker, error) { 16 | if len(data) == 0 { 17 | return nil, shared.ErrFileSize 18 | } 19 | seeker := &Seeker{data: data} 20 | seeker.expandIndexes() 21 | return seeker, nil 22 | } 23 | 24 | func (s *Seeker) LookupByIP(address net.IP) (record *shared.Record, err error) { 25 | if address = address.To4(); address == nil { 26 | err = shared.ErrInvalidIPv4 27 | return 28 | } 29 | ip := uint(binary.BigEndian.Uint32(address)) 30 | head := 0 31 | tail := len(s.indexes) - 1 32 | for (head + 1) < tail { 33 | index := (head + tail) / 2 34 | if s.indexes[index].ip <= ip { 35 | head = index 36 | } else { 37 | tail = index 38 | } 39 | } 40 | record = s.index(s.indexes[head]) 41 | record.IP = address 42 | return 43 | } 44 | 45 | func (s *Seeker) index(index *index) *shared.Record { 46 | country, area := s.readRecord(index.offset) 47 | if area == " CZ88.NET" { 48 | area = "" 49 | } 50 | return &shared.Record{ 51 | BeginIP: index.begin, 52 | EndIP: index.end, 53 | CountryName: country, 54 | RegionName: area, 55 | } 56 | } 57 | 58 | func (s *Seeker) readRecord(offset uint) (country, area string) { 59 | switch mode := s.data[offset]; mode { 60 | case 1: 61 | return s.readRecord(s.readUInt24LE(offset + 1)) 62 | case 2: 63 | country = s.readCString(s.readUInt24LE(offset + 1)) 64 | area = s.readArea(offset + 4) 65 | default: 66 | country = s.readCString(offset) 67 | area = s.readArea(offset + 1 + uint(len(country))) 68 | } 69 | return 70 | } 71 | 72 | func (s *Seeker) readArea(offset uint) string { 73 | if s.data[offset] == 2 { 74 | offset = s.readUInt24LE(offset + 1) 75 | } 76 | return s.readCString(offset) 77 | } 78 | 79 | func (s *Seeker) readCString(offset uint) string { 80 | index := offset 81 | for s.data[index] != 0 { 82 | index++ 83 | } 84 | return string(s.data[offset:index]) 85 | } 86 | -------------------------------------------------------------------------------- /qqwry/ip_seeker_test.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "net" 8 | "testing" 9 | ) 10 | 11 | var seeker *Seeker 12 | 13 | func init() { 14 | data, _ := ioutil.ReadFile("../testdata/qqwry.dat") 15 | seeker, _ = New(data) 16 | } 17 | 18 | func TestSeeker_Init(t *testing.T) { 19 | _, _ = New(nil) 20 | } 21 | 22 | func TestSeeker_Metadata(t *testing.T) { 23 | t.Log(seeker.String()) 24 | t.Log(seeker.BuildTime()) 25 | t.Log(seeker.RecordCount()) 26 | } 27 | 28 | func TestSeeker_InvalidLookup(t *testing.T) { 29 | _, _ = seeker.LookupByIP(nil) 30 | } 31 | 32 | func TestSeeker_LookupByIP(t *testing.T) { 33 | cases := []net.IP{ 34 | {0, 0, 0, 0}, 35 | {127, 0, 0, 1}, 36 | {172, 16, 0, 0}, 37 | {192, 168, 0, 0}, 38 | {100, 64, 0, 0}, 39 | {156, 154, 114, 35}, 40 | {157, 160, 206, 174}, 41 | {220, 174, 130, 251}, 42 | {255, 0, 0, 0}, 43 | {255, 255, 255, 255}, 44 | } 45 | for index, unit := range cases { 46 | record, err := seeker.LookupByIP(unit) 47 | if err != nil { 48 | t.Fatal(index, err) 49 | } 50 | fmt.Println(record, record.BeginIP, record.EndIP) 51 | } 52 | } 53 | 54 | func BenchmarkIPSeeker_LookupByIP(b *testing.B) { 55 | var ips []net.IP 56 | for i := 0; i < b.N; i++ { 57 | ip := make(net.IP, 4) 58 | _, _ = rand.Read(ip) 59 | ips = append(ips, ip) 60 | } 61 | b.ReportAllocs() 62 | b.ResetTimer() 63 | for _, ip := range ips { 64 | _, _ = seeker.LookupByIP(ip) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /qqwry/metadata.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/NiceLabs/geoip-seeker/shared" 8 | ) 9 | 10 | func (s *Seeker) IPv4Support() bool { return true } 11 | func (s *Seeker) IPv6Support() bool { return false } 12 | func (s *Seeker) RecordCount() uint64 { return uint64(len(s.indexes)) } 13 | func (s *Seeker) String() string { return shared.ShowLibraryInfo("QQWry", s) } 14 | 15 | func (s *Seeker) BuildTime() time.Time { 16 | record := s.index(s.indexes[len(s.indexes)-1]) 17 | formats := []string{ 18 | "%d\xe5\xb9\xb4%d\xe6\x9c\x88%d\xe6\x97\xa5", 19 | "%d\xc4\xea%d\xd4\xc2%d\xc8\xd5", 20 | "%4d%2d%2d", 21 | } 22 | location := time.FixedZone("CST", +8*3600) 23 | var year, month, day int 24 | for _, format := range formats { 25 | _, err := fmt.Sscanf(record.RegionName, format, &year, &month, &day) 26 | if err == nil { 27 | break 28 | } 29 | } 30 | return time.Date(year, time.Month(month), day, 0, 0, 0, 0, location) 31 | } 32 | -------------------------------------------------------------------------------- /qqwry/update.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "errors" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/NiceLabs/geoip-seeker/shared" 13 | ) 14 | 15 | const ( 16 | updateCopyWrite = "http://update.cz88.net/ip/copywrite.rar" 17 | updateQQWay = "http://update.cz88.net/ip/qqwry.rar" 18 | ) 19 | 20 | type updater struct { 21 | client *http.Client 22 | version, size, key uint32 23 | } 24 | 25 | func DownloadUpdate(client *http.Client) (update shared.Update, err error) { 26 | resp, err := client.Do(newRequest(updateCopyWrite)) 27 | if err != nil { 28 | return 29 | } 30 | defer resp.Body.Close() 31 | copywrite := new(struct { 32 | Magic [4]byte 33 | Version, _, Size, _, Key uint32 34 | Text, Link [128]byte 35 | }) 36 | err = binary.Read(resp.Body, binary.LittleEndian, copywrite) 37 | if err != nil { 38 | return 39 | } 40 | if string(copywrite.Magic[:]) != "CZIP" { 41 | err = errors.New("magic error") 42 | return 43 | } 44 | update = &updater{ 45 | client: client, 46 | version: copywrite.Version, 47 | size: copywrite.Size, 48 | key: copywrite.Key, 49 | } 50 | return 51 | } 52 | 53 | func (u *updater) BuildTime() time.Time { 54 | year, month, day := versionToDate( 55 | u.version + dateToVersion(1899, 12, 30), 56 | ) 57 | 58 | location := time.FixedZone("CST", +8*3600) 59 | return time.Date(year, time.Month(month), day, 0, 0, 0, 0, location) 60 | } 61 | 62 | func (u *updater) Size() uint64 { return uint64(u.size) } 63 | 64 | func (u *updater) Download() (payload []byte, err error) { 65 | resp, err := u.client.Do(newRequest(updateQQWay)) 66 | if err != nil { 67 | return 68 | } 69 | defer resp.Body.Close() 70 | payload, err = ioutil.ReadAll(resp.Body) 71 | if err != nil { 72 | return 73 | } 74 | return u.decode(payload) 75 | } 76 | 77 | func (u *updater) decode(payload []byte) (data []byte, err error) { 78 | key := u.key 79 | for index := 0; index < 0x200; index++ { 80 | key *= 0x805 81 | key += 1 82 | key &= 0xFF 83 | payload[index] = byte(key ^ uint32(payload[index])) 84 | } 85 | reader, err := zlib.NewReader(bytes.NewReader(payload)) 86 | if err != nil { 87 | return 88 | } 89 | return ioutil.ReadAll(reader) 90 | } 91 | 92 | func newRequest(url string) *http.Request { 93 | request, _ := http.NewRequest(http.MethodGet, url, nil) 94 | request.Header = http.Header{ 95 | "Accept": []string{"text/html, */*"}, 96 | "User-Agent": []string{"Mozilla/3.0 (compatible; Indy Library)"}, 97 | } 98 | return request 99 | } 100 | 101 | // see https://github.com/shuax/LocateIP/blob/master/loci/cz_update.c#L23-L29 102 | func dateToVersion(year, month, day uint32) uint32 { 103 | month = (month + 9) % 12 104 | year = year - month/10 105 | day = 365*year + year/4 - year/100 + year/400 + (month*153+2)/5 + day - 1 106 | return day 107 | } 108 | 109 | // see https://github.com/shuax/LocateIP/blob/master/loci/cz_update.c#L31-L41 110 | func versionToDate(version uint32) (year, month, day int) { 111 | y := (version*33 + 999) / 12053 112 | t := version - y*365 - y/4 + y/100 - y/400 113 | m := (t*5+2)/153 + 2 114 | 115 | year = int(y + m/12) 116 | month = int(m%12 + 1) 117 | day = int(t - (m*153-304)/5 + 1) 118 | return 119 | } 120 | -------------------------------------------------------------------------------- /qqwry/utils.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | ) 7 | 8 | type index struct { 9 | ip, offset uint 10 | begin, end net.IP 11 | } 12 | 13 | func (s *Seeker) expandIndexes() { 14 | first := uint(binary.LittleEndian.Uint32(s.data[0:4])) 15 | last := uint(binary.LittleEndian.Uint32(s.data[4:8])) 16 | for i := first; i < last+7; i += 7 { 17 | offset := s.readUInt24LE(i + 4) 18 | s.indexes = append(s.indexes, &index{ 19 | ip: uint(binary.LittleEndian.Uint32(s.data[i : i+4])), 20 | offset: offset + 4, 21 | begin: s.readIP(i), 22 | end: s.readIP(offset), 23 | }) 24 | } 25 | return 26 | } 27 | 28 | func (s *Seeker) readIP(offset uint) net.IP { 29 | return net.IP{s.data[offset+3], s.data[offset+2], s.data[offset+1], s.data[offset]} 30 | } 31 | 32 | func (s *Seeker) readUInt24LE(offset uint) uint { 33 | return uint(s.data[offset]) | uint(s.data[offset+1])<<8 | uint(s.data[offset+2])<<16 34 | } 35 | -------------------------------------------------------------------------------- /shared/errors.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrFileSize = errors.New("ip database file size error") 7 | ErrMetaData = errors.New("ip database metadata error") 8 | ErrDatabaseError = errors.New("database error") 9 | ErrIPFormat = errors.New("query ip format error") 10 | ErrNoSupportLanguage = errors.New("language not support") 11 | ErrNoSupportIPv4 = errors.New("ipv4 not support") 12 | ErrNoSupportIPv6 = errors.New("ipv6 not support") 13 | ErrInvalidIPv4 = errors.New("invalid ipv4 address") 14 | ErrDataNotExists = errors.New("data is not exists") 15 | ) 16 | -------------------------------------------------------------------------------- /shared/ip_seeker.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | ) 8 | 9 | type IPSeeker interface { 10 | LookupByIP(address net.IP) (*Record, error) 11 | IPv4Support() bool 12 | IPv6Support() bool 13 | RecordCount() uint64 14 | BuildTime() time.Time 15 | fmt.Stringer 16 | } 17 | 18 | type Update interface { 19 | BuildTime() time.Time 20 | Size() uint64 21 | Download() (data []byte, err error) 22 | } 23 | -------------------------------------------------------------------------------- /shared/record.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | type Record struct { 9 | // IP 10 | IP net.IP `json:",omitempty"` 11 | BeginIP net.IP `json:",omitempty"` 12 | EndIP net.IP `json:",omitempty"` 13 | // GeoInfo 14 | CountryName string `json:",omitempty"` 15 | RegionName string `json:",omitempty"` 16 | CityName string `json:",omitempty"` 17 | // Owner 18 | OwnerDomain string `json:",omitempty"` 19 | ISPDomain string `json:",omitempty"` 20 | // GeoCoding 21 | Latitude string `json:",omitempty"` 22 | Longitude string `json:",omitempty"` 23 | // Time zone 24 | TimeZone string `json:",omitempty"` 25 | UTCOffset string `json:",omitempty"` 26 | // Country Code 27 | // IDDCode = International Direct Dialing 28 | // GB2260Code = GB/T 2260 29 | // ISO3166Alpha2Code = ISO 3166-1 alpha-2 30 | // ISO3166Alpha3Code = ISO 3166-1 alpha-3 31 | IDDCode string `json:",omitempty"` 32 | GB2260Code string `json:",omitempty"` 33 | ISO3166Alpha2Code string `json:",omitempty"` 34 | ISO3166Alpha3Code string `json:",omitempty"` 35 | ContinentCode string `json:",omitempty"` 36 | // Currency 37 | // CurrencyCode = ISO 4217 38 | CurrencyCode string `json:",omitempty"` 39 | CurrencyName string `json:",omitempty"` 40 | // Service 41 | IDC string `json:",omitempty"` // IDC | VPN 42 | BaseStation string `json:",omitempty"` // WiFi | BS (Base Station) 43 | // Other 44 | EuropeanUnion string `json:",omitempty"` 45 | AnyCast string `json:",omitempty"` 46 | } 47 | 48 | func (r *Record) String() string { 49 | values := []string{ 50 | r.CountryName, 51 | r.RegionName, 52 | r.CityName, 53 | } 54 | for index := range values { 55 | if values[index] == "" { 56 | values[index] = "N/A" 57 | } 58 | } 59 | return strings.Join(values, "\t") 60 | } 61 | -------------------------------------------------------------------------------- /shared/utils.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func ShowLibraryInfo(name string, seeker IPSeeker) string { 9 | items := []string{ 10 | name, 11 | seeker.BuildTime().Format("2006-01-02"), 12 | strconv.FormatUint(seeker.RecordCount(), 10), 13 | } 14 | if seeker.IPv4Support() { 15 | items = append(items, "[IPv4]") 16 | } 17 | if seeker.IPv6Support() { 18 | items = append(items, "[IPv6]") 19 | } 20 | return strings.Join(items, " ") 21 | } 22 | -------------------------------------------------------------------------------- /testdata/17monipdb.datx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiceLabs/geoip-seeker/f4422b327de0f23f6c7b1d6b941579de15429874/testdata/17monipdb.datx -------------------------------------------------------------------------------- /testdata/ipiptest.ipdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiceLabs/geoip-seeker/f4422b327de0f23f6c7b1d6b941579de15429874/testdata/ipiptest.ipdb -------------------------------------------------------------------------------- /testdata/qqwry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiceLabs/geoip-seeker/f4422b327de0f23f6c7b1d6b941579de15429874/testdata/qqwry.dat --------------------------------------------------------------------------------