/
qqwry.go
205 lines (168 loc) · 4.17 KB
/
qqwry.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// Package qqwry implements download and query IP geo-location information
// facilities for the famous qqwry.dat database.
//
// Inspired from github.com/tonywubo/qqwry, with bug fixes, unit tests
// and performance improvements.
package qqwry
import (
"bytes"
"encoding/binary"
"fmt"
"io/ioutil"
"net"
"os"
"golang.org/x/text/encoding/simplifiedchinese"
)
const (
redirectMode1 = 0x01
redirectMode2 = 0x02
)
// DB is qqwry database instance.
type DB struct {
buff []byte
start, end, total uint32
}
// Record is query result.
type Record struct {
Country, City string
}
// Open the qqwry.dat database.
func Open(file string) (*DB, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
buff, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
start := binary.LittleEndian.Uint32(buff[:4])
end := binary.LittleEndian.Uint32(buff[4:8])
return &DB{
buff: buff,
start: start,
end: end,
total: (end-start)/7 + 1,
}, nil
}
func (record *Record) String() string {
return fmt.Sprintf("%s %s", record.Country, record.City)
}
// Query ip geo location information from giving net.IP.
func (db *DB) Query(ip net.IP) (*Record, error) {
if db.buff == nil {
return nil, fmt.Errorf("db not initialized")
}
ipv4 := ip.To4()
if ipv4 == nil {
return nil, fmt.Errorf("not a valid ipv4 address")
}
offset := db.search(binary.BigEndian.Uint32(ipv4))
if offset <= 0 {
return &Record{}, nil
}
return db.readRecord(offset), nil
}
// Dump all the records from the database.
//
// This function directly prints lots of lines to stdout.
func (db *DB) Dump() {
fmt.Println("Total records:", db.total)
for cur := db.start; cur < db.end+7; cur += 7 {
buff := db.buff[cur : cur+7]
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, getIPFromRecord(buff))
offset := getAddrFromRecord(buff)
fmt.Printf("%s\t%s\n", ip, db.readRecord(offset))
}
}
// Version return the version record for the database, from the last record for 255.255.255.255.
func (db *DB) Version() string {
offset := getAddrFromRecord(db.buff[db.end : db.end+7])
return db.readRecord(offset).City
}
// Total returns the total number of records for the database
func (db *DB) Total() uint32 {
return db.total
}
func (db *DB) readRecord(offset uint32) *Record {
rq := &Record{}
offset += 4
mode := db.buff[offset]
// full redirect
if mode == redirectMode1 {
offset = db.readUint32FromByte3(offset + 1)
mode = db.buff[offset]
}
// country
var country []byte
if mode == redirectMode2 {
off1 := db.readUint32FromByte3(offset + 1)
country = readCString(db.buff[off1:])
offset += 4
} else {
country = readCString(db.buff[offset:])
offset += uint32(len(country)) + 1
}
// area
mode = db.buff[offset]
if mode == redirectMode2 {
offset = db.readUint32FromByte3(offset + 1)
}
area := readCString(db.buff[offset:])
// decode gbk
enc := simplifiedchinese.GBK.NewDecoder()
if encoded, err := enc.Bytes(country); err == nil {
rq.Country = string(encoded)
}
if encoded, err := enc.Bytes(area); err == nil {
rq.City = string(encoded)
}
return rq
}
func (db *DB) readUint32FromByte3(offset uint32) uint32 {
return byte3ToUInt32(db.buff[offset : offset+3])
}
func readCString(buf []byte) []byte {
idx := bytes.IndexByte(buf, 0)
if idx < 0 {
// TODO: return error
return nil
}
return buf[:idx]
}
func getIPFromRecord(buf []byte) uint32 {
return binary.LittleEndian.Uint32(buf[:4])
}
func getAddrFromRecord(buf []byte) uint32 {
return byte3ToUInt32(buf[4:7])
}
func (db *DB) search(ip uint32) uint32 {
left := uint32(0)
right := db.total
for right-left > 1 {
mid := (left + right) / 2
offset := db.start + mid*7
cur := getIPFromRecord(db.buff[offset : offset+7])
if ip < cur {
right = mid
} else {
left = mid
}
}
offset := db.start + 7*left
ipBegin := getIPFromRecord(db.buff[offset : offset+7])
offset = getAddrFromRecord(db.buff[offset : offset+7])
ipEnd := getIPFromRecord(db.buff[offset : offset+7])
if ipBegin <= ip && ip <= ipEnd {
return offset
}
return 0
}
func byte3ToUInt32(data []byte) uint32 {
i := uint32(data[0]) & 0xff
i |= (uint32(data[1]) << 8) & 0xff00
i |= (uint32(data[2]) << 16) & 0xff0000
return i
}