mmdb-writer 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mmdb_writer-0.1.1/LICENSE +21 -0
- mmdb_writer-0.1.1/PKG-INFO +53 -0
- mmdb_writer-0.1.1/README.md +33 -0
- mmdb_writer-0.1.1/mmdb_writer.py +460 -0
- mmdb_writer-0.1.1/pyproject.toml +18 -0
- mmdb_writer-0.1.1/setup.py +25 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 VimT
|
|
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.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mmdb-writer
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Make mmdb format ip library file which can be read by maxmind official language reader
|
|
5
|
+
Home-page: https://github.com/vimt/MaxMind-DB-Writer-python
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: VimT
|
|
8
|
+
Requires-Python: >=3.7,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Requires-Dist: maxminddb (>=2.0,<3.0)
|
|
17
|
+
Requires-Dist: netaddr (>=0.7.0,<1.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# MaxMind-DB-Writer-python
|
|
21
|
+
|
|
22
|
+
Make `mmdb` format ip library file which can be read by [`maxmind` official language reader](https://dev.maxmind.com/geoip/geoip2/downloadable/)
|
|
23
|
+
|
|
24
|
+
[The official perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) was written in perl, which was difficult to customize. So I implemented the `MaxmindDB format` ip library in python language
|
|
25
|
+
## Install
|
|
26
|
+
```shell script
|
|
27
|
+
pip install -U git+https://github.com/VimT/MaxMind-DB-Writer-python
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
```python
|
|
32
|
+
from netaddr import IPSet
|
|
33
|
+
|
|
34
|
+
from mmdb_writer import MMDBWriter
|
|
35
|
+
writer = MMDBWriter()
|
|
36
|
+
|
|
37
|
+
writer.insert_network(IPSet(['1.1.0.0/24', '1.1.1.0/24']), {'country': 'COUNTRY', 'isp': 'ISP'})
|
|
38
|
+
writer.to_db_file('test.mmdb')
|
|
39
|
+
|
|
40
|
+
import maxminddb
|
|
41
|
+
m = maxminddb.open_database('test.mmdb')
|
|
42
|
+
r = m.get('1.1.1.1')
|
|
43
|
+
assert r == {'country': 'COUNTRY', 'isp': 'ISP'}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
see [csv_to_mmdb.py](./examples/csv_to_mmdb.py)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Reference:
|
|
51
|
+
- [MaxmindDB format](http://maxmind.github.io/MaxMind-DB/)
|
|
52
|
+
- [geoip-mmdb](https://github.com/i-rinat/geoip-mmdb)
|
|
53
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# MaxMind-DB-Writer-python
|
|
2
|
+
|
|
3
|
+
Make `mmdb` format ip library file which can be read by [`maxmind` official language reader](https://dev.maxmind.com/geoip/geoip2/downloadable/)
|
|
4
|
+
|
|
5
|
+
[The official perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) was written in perl, which was difficult to customize. So I implemented the `MaxmindDB format` ip library in python language
|
|
6
|
+
## Install
|
|
7
|
+
```shell script
|
|
8
|
+
pip install -U git+https://github.com/VimT/MaxMind-DB-Writer-python
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
```python
|
|
13
|
+
from netaddr import IPSet
|
|
14
|
+
|
|
15
|
+
from mmdb_writer import MMDBWriter
|
|
16
|
+
writer = MMDBWriter()
|
|
17
|
+
|
|
18
|
+
writer.insert_network(IPSet(['1.1.0.0/24', '1.1.1.0/24']), {'country': 'COUNTRY', 'isp': 'ISP'})
|
|
19
|
+
writer.to_db_file('test.mmdb')
|
|
20
|
+
|
|
21
|
+
import maxminddb
|
|
22
|
+
m = maxminddb.open_database('test.mmdb')
|
|
23
|
+
r = m.get('1.1.1.1')
|
|
24
|
+
assert r == {'country': 'COUNTRY', 'isp': 'ISP'}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
see [csv_to_mmdb.py](./examples/csv_to_mmdb.py)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Reference:
|
|
32
|
+
- [MaxmindDB format](http://maxmind.github.io/MaxMind-DB/)
|
|
33
|
+
- [geoip-mmdb](https://github.com/i-rinat/geoip-mmdb)
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
__version__ = '0.1.1'
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
import struct
|
|
7
|
+
import time
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
from netaddr import IPSet, IPNetwork
|
|
11
|
+
|
|
12
|
+
MMDBType = Union[dict, list, str, bytes, int, bool]
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
METADATA_MAGIC = b'\xab\xcd\xefMaxMind.com'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SearchTreeNode(object):
|
|
20
|
+
def __init__(self, left=None, right=None):
|
|
21
|
+
self.left = left
|
|
22
|
+
self.right = right
|
|
23
|
+
|
|
24
|
+
def get_or_create(self, item):
|
|
25
|
+
if item == 0:
|
|
26
|
+
self.left = self.left or SearchTreeNode()
|
|
27
|
+
return self.left
|
|
28
|
+
elif item == 1:
|
|
29
|
+
self.right = self.right or SearchTreeNode()
|
|
30
|
+
return self.right
|
|
31
|
+
|
|
32
|
+
def __getitem__(self, item):
|
|
33
|
+
if item == 0:
|
|
34
|
+
return self.left
|
|
35
|
+
elif item == 1:
|
|
36
|
+
return self.right
|
|
37
|
+
|
|
38
|
+
def __setitem__(self, key, value):
|
|
39
|
+
if key == 0:
|
|
40
|
+
self.left = value
|
|
41
|
+
elif key == 1:
|
|
42
|
+
self.right = value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SearchTreeLeaf(object):
|
|
46
|
+
def __init__(self, value):
|
|
47
|
+
self.value = value
|
|
48
|
+
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return "SearchTreeLeaf(value={value})".format(value=self.value)
|
|
51
|
+
|
|
52
|
+
__str__ = __repr__
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Encoder(object):
|
|
56
|
+
|
|
57
|
+
def __init__(self, cache=True):
|
|
58
|
+
self.data_cache = {}
|
|
59
|
+
self.data_list = []
|
|
60
|
+
self.data_pointer = 0
|
|
61
|
+
|
|
62
|
+
self.cache = cache
|
|
63
|
+
|
|
64
|
+
def _encode_pointer(self, value):
|
|
65
|
+
pointer = value
|
|
66
|
+
if pointer >= 134744064:
|
|
67
|
+
res = struct.pack('>BI', 0x38, pointer)
|
|
68
|
+
elif pointer >= 526336:
|
|
69
|
+
pointer -= 526336
|
|
70
|
+
res = struct.pack('>BBBB', 0x30 + ((pointer >> 24) & 0x07),
|
|
71
|
+
(pointer >> 16) & 0xff, (pointer >> 8) & 0xff,
|
|
72
|
+
pointer & 0xff)
|
|
73
|
+
elif pointer >= 2048:
|
|
74
|
+
pointer -= 2048
|
|
75
|
+
res = struct.pack('>BBB', 0x28 + ((pointer >> 16) & 0x07),
|
|
76
|
+
(pointer >> 8) & 0xff, pointer & 0xff)
|
|
77
|
+
else:
|
|
78
|
+
res = struct.pack('>BB', 0x20 + ((pointer >> 8) & 0x07),
|
|
79
|
+
pointer & 0xff)
|
|
80
|
+
|
|
81
|
+
return res
|
|
82
|
+
|
|
83
|
+
def _encode_utf8_string(self, value):
|
|
84
|
+
encoded_value = value.encode('utf-8')
|
|
85
|
+
res = self._make_header(2, len(encoded_value))
|
|
86
|
+
res += encoded_value
|
|
87
|
+
return res
|
|
88
|
+
|
|
89
|
+
def _encode_bytes(self, value):
|
|
90
|
+
return self._make_header(4, len(value)) + value
|
|
91
|
+
|
|
92
|
+
def _encode_uint(self, type_id, max_len):
|
|
93
|
+
def _encode_unsigned_value(value):
|
|
94
|
+
res = b''
|
|
95
|
+
while value != 0 and len(res) < max_len:
|
|
96
|
+
res = struct.pack('>B', value & 0xff) + res
|
|
97
|
+
value = value >> 8
|
|
98
|
+
return self._make_header(type_id, len(res)) + res
|
|
99
|
+
|
|
100
|
+
return _encode_unsigned_value
|
|
101
|
+
|
|
102
|
+
def _encode_map(self, value):
|
|
103
|
+
res = self._make_header(7, len(value))
|
|
104
|
+
for k, v in list(value.items()):
|
|
105
|
+
# Keys are always stored by value.
|
|
106
|
+
res += self.encode(k)
|
|
107
|
+
res += self.encode(v)
|
|
108
|
+
return res
|
|
109
|
+
|
|
110
|
+
def _encode_array(self, value):
|
|
111
|
+
res = self._make_header(11, len(value))
|
|
112
|
+
for k in value:
|
|
113
|
+
res += self.encode(k)
|
|
114
|
+
return res
|
|
115
|
+
|
|
116
|
+
def _encode_boolean(self, value):
|
|
117
|
+
return self._make_header(14, 1 if value else 0)
|
|
118
|
+
|
|
119
|
+
def _encode_pack_type(self, type_id, fmt):
|
|
120
|
+
def pack_type(value):
|
|
121
|
+
res = struct.pack(fmt, value)
|
|
122
|
+
return self._make_header(type_id, len(res)) + res
|
|
123
|
+
|
|
124
|
+
return pack_type
|
|
125
|
+
|
|
126
|
+
_type_decoder = None
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def type_decoder(self):
|
|
130
|
+
if self._type_decoder is None:
|
|
131
|
+
self._type_decoder = {
|
|
132
|
+
1: self._encode_pointer,
|
|
133
|
+
2: self._encode_utf8_string,
|
|
134
|
+
3: self._encode_pack_type(3, '>d'), # double,
|
|
135
|
+
4: self._encode_bytes,
|
|
136
|
+
5: self._encode_uint(5, 2), # uint16
|
|
137
|
+
6: self._encode_uint(6, 4), # uint32
|
|
138
|
+
7: self._encode_map,
|
|
139
|
+
8: self._encode_pack_type(8, '>i'), # int32
|
|
140
|
+
9: self._encode_uint(9, 8), # uint64
|
|
141
|
+
10: self._encode_uint(10, 16), # uint128
|
|
142
|
+
11: self._encode_array,
|
|
143
|
+
14: self._encode_boolean,
|
|
144
|
+
15: self._encode_pack_type(15, '>f'), # float,
|
|
145
|
+
}
|
|
146
|
+
return self._type_decoder
|
|
147
|
+
|
|
148
|
+
def _make_header(self, type_id, length):
|
|
149
|
+
if length >= 16843036:
|
|
150
|
+
raise Exception('length >= 16843036')
|
|
151
|
+
|
|
152
|
+
elif length >= 65821:
|
|
153
|
+
five_bits = 31
|
|
154
|
+
length -= 65821
|
|
155
|
+
b3 = length & 0xff
|
|
156
|
+
b2 = (length >> 8) & 0xff
|
|
157
|
+
b1 = (length >> 16) & 0xff
|
|
158
|
+
additional_length_bytes = struct.pack('>BBB', b1, b2, b3)
|
|
159
|
+
|
|
160
|
+
elif length >= 285:
|
|
161
|
+
five_bits = 30
|
|
162
|
+
length -= 285
|
|
163
|
+
b2 = length & 0xff
|
|
164
|
+
b1 = (length >> 8) & 0xff
|
|
165
|
+
additional_length_bytes = struct.pack('>BB', b1, b2)
|
|
166
|
+
|
|
167
|
+
elif length >= 29:
|
|
168
|
+
five_bits = 29
|
|
169
|
+
length -= 29
|
|
170
|
+
additional_length_bytes = struct.pack('>B', length & 0xff)
|
|
171
|
+
|
|
172
|
+
else:
|
|
173
|
+
five_bits = length
|
|
174
|
+
additional_length_bytes = b''
|
|
175
|
+
|
|
176
|
+
if type_id <= 7:
|
|
177
|
+
res = struct.pack('>B', (type_id << 5) + five_bits)
|
|
178
|
+
else:
|
|
179
|
+
res = struct.pack('>BB', five_bits, type_id - 7)
|
|
180
|
+
|
|
181
|
+
return res + additional_length_bytes
|
|
182
|
+
|
|
183
|
+
_python_type_id = {
|
|
184
|
+
float: 15,
|
|
185
|
+
bool: 14,
|
|
186
|
+
list: 11,
|
|
187
|
+
dict: 7,
|
|
188
|
+
bytes: 4,
|
|
189
|
+
str: 2
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def python_type_id(self, value):
|
|
193
|
+
value_type = type(value)
|
|
194
|
+
type_id = self._python_type_id.get(value_type)
|
|
195
|
+
if type_id:
|
|
196
|
+
return type_id
|
|
197
|
+
if value_type is int:
|
|
198
|
+
if value > 0xffffffffffffffff:
|
|
199
|
+
return 10
|
|
200
|
+
elif value > 0xffffffff:
|
|
201
|
+
return 9
|
|
202
|
+
elif value > 0xffff:
|
|
203
|
+
return 6
|
|
204
|
+
elif value < 0:
|
|
205
|
+
return 8
|
|
206
|
+
else:
|
|
207
|
+
return 5
|
|
208
|
+
raise TypeError("unknown type {value_type}".format(value_type=value_type))
|
|
209
|
+
|
|
210
|
+
def encode_meta(self, meta):
|
|
211
|
+
res = self._make_header(7, len(meta))
|
|
212
|
+
meta_type = {'node_count': 6, 'record_size': 5, 'ip_version': 5,
|
|
213
|
+
'binary_format_major_version': 5, 'binary_format_minor_version': 5,
|
|
214
|
+
'build_epoch': 9}
|
|
215
|
+
for k, v in list(meta.items()):
|
|
216
|
+
# Keys are always stored by value.
|
|
217
|
+
res += self.encode(k)
|
|
218
|
+
res += self.encode(v, meta_type.get(k))
|
|
219
|
+
return res
|
|
220
|
+
|
|
221
|
+
def encode(self, value, type_id=None):
|
|
222
|
+
if self.cache:
|
|
223
|
+
try:
|
|
224
|
+
return self.data_cache[id(value)]
|
|
225
|
+
except KeyError:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
if not type_id:
|
|
229
|
+
type_id = self.python_type_id(value)
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
encoder = self.type_decoder[type_id]
|
|
233
|
+
except KeyError:
|
|
234
|
+
raise ValueError("unknown type_id={type_id}".format(type_id=type_id))
|
|
235
|
+
res = encoder(value)
|
|
236
|
+
|
|
237
|
+
if self.cache:
|
|
238
|
+
# add to cache
|
|
239
|
+
if type_id == 1:
|
|
240
|
+
self.data_list.append(res)
|
|
241
|
+
self.data_pointer += len(res)
|
|
242
|
+
return res
|
|
243
|
+
else:
|
|
244
|
+
self.data_list.append(res)
|
|
245
|
+
pointer_position = self.data_pointer
|
|
246
|
+
self.data_pointer += len(res)
|
|
247
|
+
pointer = self.encode(pointer_position, 1)
|
|
248
|
+
self.data_cache[id(value)] = pointer
|
|
249
|
+
return pointer
|
|
250
|
+
return res
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class TreeWriter(object):
|
|
254
|
+
encoder_cls = Encoder
|
|
255
|
+
|
|
256
|
+
def __init__(self, tree, meta):
|
|
257
|
+
self._node_idx = {}
|
|
258
|
+
self._leaf_offset = {}
|
|
259
|
+
self._node_list = []
|
|
260
|
+
self._node_counter = 0
|
|
261
|
+
self._record_size = 0
|
|
262
|
+
|
|
263
|
+
self.tree = tree
|
|
264
|
+
self.meta = meta
|
|
265
|
+
|
|
266
|
+
self.encoder = self.encoder_cls(cache=True)
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def _data_list(self):
|
|
270
|
+
return self.encoder.data_list
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def _data_pointer(self):
|
|
274
|
+
return self.encoder.data_pointer + 16
|
|
275
|
+
|
|
276
|
+
def _build_meta(self):
|
|
277
|
+
return {
|
|
278
|
+
"node_count": self._node_counter,
|
|
279
|
+
"record_size": self.record_size,
|
|
280
|
+
**self.meta
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
def _adjust_record_size(self):
|
|
284
|
+
# Tree records should be large enough to contain either tree node index
|
|
285
|
+
# or data offset.
|
|
286
|
+
max_id = self._node_counter + self._data_pointer + 1
|
|
287
|
+
|
|
288
|
+
# Estimate required bit count.
|
|
289
|
+
bit_count = int(math.ceil(math.log(max_id, 2)))
|
|
290
|
+
if bit_count <= 24:
|
|
291
|
+
self.record_size = 24
|
|
292
|
+
elif bit_count <= 28:
|
|
293
|
+
self.record_size = 28
|
|
294
|
+
elif bit_count <= 32:
|
|
295
|
+
self.record_size = 32
|
|
296
|
+
else:
|
|
297
|
+
raise Exception('record_size > 32')
|
|
298
|
+
|
|
299
|
+
self.data_offset = self.record_size * 2 / 8 * self._node_counter
|
|
300
|
+
|
|
301
|
+
def _enumerate_nodes(self, node):
|
|
302
|
+
if type(node) is SearchTreeNode:
|
|
303
|
+
node_id = id(node)
|
|
304
|
+
if node_id not in self._node_idx:
|
|
305
|
+
self._node_idx[node_id] = self._node_counter
|
|
306
|
+
self._node_counter += 1
|
|
307
|
+
self._node_list.append(node)
|
|
308
|
+
|
|
309
|
+
self._enumerate_nodes(node.left)
|
|
310
|
+
self._enumerate_nodes(node.right)
|
|
311
|
+
|
|
312
|
+
elif type(node) is SearchTreeLeaf:
|
|
313
|
+
node_id = id(node)
|
|
314
|
+
if node_id not in self._leaf_offset:
|
|
315
|
+
res = self.encoder.encode(node.value)
|
|
316
|
+
self._leaf_offset[node_id] = self._data_pointer - len(res)
|
|
317
|
+
else: # == None
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
def _calc_record_idx(self, node):
|
|
321
|
+
if node is None:
|
|
322
|
+
return self._node_counter
|
|
323
|
+
elif type(node) is SearchTreeNode:
|
|
324
|
+
return self._node_idx[id(node)]
|
|
325
|
+
elif type(node) is SearchTreeLeaf:
|
|
326
|
+
return self._leaf_offset[id(node)] + self._node_counter
|
|
327
|
+
else:
|
|
328
|
+
raise Exception("unexpected type")
|
|
329
|
+
|
|
330
|
+
def _cal_node_bytes(self, node) -> bytes:
|
|
331
|
+
left_idx = self._calc_record_idx(node.left)
|
|
332
|
+
right_idx = self._calc_record_idx(node.right)
|
|
333
|
+
|
|
334
|
+
if self.record_size == 24:
|
|
335
|
+
b1 = (left_idx >> 16) & 0xff
|
|
336
|
+
b2 = (left_idx >> 8) & 0xff
|
|
337
|
+
b3 = left_idx & 0xff
|
|
338
|
+
b4 = (right_idx >> 16) & 0xff
|
|
339
|
+
b5 = (right_idx >> 8) & 0xff
|
|
340
|
+
b6 = right_idx & 0xff
|
|
341
|
+
return struct.pack('>BBBBBB', b1, b2, b3, b4, b5, b6)
|
|
342
|
+
|
|
343
|
+
elif self.record_size == 28:
|
|
344
|
+
b1 = (left_idx >> 16) & 0xff
|
|
345
|
+
b2 = (left_idx >> 8) & 0xff
|
|
346
|
+
b3 = left_idx & 0xff
|
|
347
|
+
b4 = ((left_idx >> 24) & 0xf) * 16 + \
|
|
348
|
+
((right_idx >> 24) & 0xf)
|
|
349
|
+
b5 = (right_idx >> 16) & 0xff
|
|
350
|
+
b6 = (right_idx >> 8) & 0xff
|
|
351
|
+
b7 = right_idx & 0xff
|
|
352
|
+
return struct.pack('>BBBBBBB', b1, b2, b3, b4, b5, b6, b7)
|
|
353
|
+
|
|
354
|
+
elif self.record_size == 32:
|
|
355
|
+
return struct.pack('>II', left_idx, right_idx)
|
|
356
|
+
|
|
357
|
+
else:
|
|
358
|
+
raise Exception('self.record_size > 32')
|
|
359
|
+
|
|
360
|
+
def write(self, fname):
|
|
361
|
+
self._enumerate_nodes(self.tree)
|
|
362
|
+
self._adjust_record_size()
|
|
363
|
+
|
|
364
|
+
with open(fname, 'wb') as f:
|
|
365
|
+
for node in self._node_list:
|
|
366
|
+
f.write(self._cal_node_bytes(node))
|
|
367
|
+
|
|
368
|
+
f.write(b'\x00' * 16)
|
|
369
|
+
|
|
370
|
+
for element in self._data_list:
|
|
371
|
+
f.write(element)
|
|
372
|
+
|
|
373
|
+
f.write(METADATA_MAGIC)
|
|
374
|
+
f.write(self.encoder_cls(cache=False).encode_meta(self._build_meta()))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def bits_rstrip(n, length=None, keep=0):
|
|
378
|
+
return map(int, bin(n)[2:].rjust(length, '0')[:keep])
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class MMDBWriter(object):
|
|
382
|
+
|
|
383
|
+
def __init__(self, ip_version=4, database_type='GeoIP',
|
|
384
|
+
languages=None, description='GeoIP db',
|
|
385
|
+
ipv4_compatible=False):
|
|
386
|
+
self.tree = SearchTreeNode()
|
|
387
|
+
self.ipv4_compatible = ipv4_compatible
|
|
388
|
+
|
|
389
|
+
if languages is None:
|
|
390
|
+
languages = []
|
|
391
|
+
self.description = description
|
|
392
|
+
self.database_type = database_type
|
|
393
|
+
self.ip_version = ip_version
|
|
394
|
+
self.languages = languages
|
|
395
|
+
self.binary_format_major_version = 2
|
|
396
|
+
self.binary_format_minor_version = 0
|
|
397
|
+
|
|
398
|
+
self._bit_length = 128 if ip_version == 6 else 32
|
|
399
|
+
|
|
400
|
+
if ip_version not in [4, 6]:
|
|
401
|
+
raise ValueError("ip_version should be 4 or 6, {} is incorrect".format(ip_version))
|
|
402
|
+
if ip_version == 4 and ipv4_compatible:
|
|
403
|
+
raise ValueError("ipv4_compatible=True can set when ip_version=6")
|
|
404
|
+
if not self.binary_format_major_version:
|
|
405
|
+
raise ValueError("major_version can't be empty or 0: {}".format(self.binary_format_major_version))
|
|
406
|
+
if isinstance(description, str):
|
|
407
|
+
self.description = {i: description for i in languages}
|
|
408
|
+
for i in languages:
|
|
409
|
+
if i not in self.description:
|
|
410
|
+
raise ValueError("language {} must have description!")
|
|
411
|
+
|
|
412
|
+
def insert_network(self, network: IPSet, content: MMDBType):
|
|
413
|
+
leaf = SearchTreeLeaf(content)
|
|
414
|
+
if not isinstance(network, IPSet):
|
|
415
|
+
raise ValueError("network type should be netaddr.IPSet.")
|
|
416
|
+
network = network.iter_cidrs()
|
|
417
|
+
for cidr in network:
|
|
418
|
+
if self.ip_version == 4 and cidr.version == 6:
|
|
419
|
+
raise ValueError('You inserted a IPv6 address {} '
|
|
420
|
+
'to an IPv4-only database.'.format(cidr))
|
|
421
|
+
if self.ip_version == 6 and cidr.version == 4:
|
|
422
|
+
if not self.ipv4_compatible:
|
|
423
|
+
raise ValueError("You inserted a IPv4 address {} to an IPv6 database."
|
|
424
|
+
"Please use ipv4_compatible=True option store "
|
|
425
|
+
"IPv4 address in IPv6 database as ::/96 format".format(cidr))
|
|
426
|
+
cidr = cidr.ipv6(True)
|
|
427
|
+
node = self.tree
|
|
428
|
+
bits = list(bits_rstrip(cidr.value, self._bit_length, cidr.prefixlen))
|
|
429
|
+
current_node = node
|
|
430
|
+
supernet_leaf = None # Tracks whether we are inserting into a subnet
|
|
431
|
+
for (index, ip_bit) in enumerate(bits[:-1]):
|
|
432
|
+
previous_node = current_node
|
|
433
|
+
current_node = previous_node.get_or_create(ip_bit)
|
|
434
|
+
|
|
435
|
+
if isinstance(current_node, SearchTreeLeaf):
|
|
436
|
+
current_cidr = IPNetwork((int(''.join(map(str, bits[:index + 1])).ljust(self._bit_length, '0'), 2), index + 1))
|
|
437
|
+
logger.info(f"Inserting {cidr} ({content}) into subnet of {current_cidr} ({current_node.value})")
|
|
438
|
+
supernet_leaf = current_node
|
|
439
|
+
current_node = SearchTreeNode()
|
|
440
|
+
previous_node[ip_bit] = current_node
|
|
441
|
+
|
|
442
|
+
if supernet_leaf:
|
|
443
|
+
next_bit = bits[index + 1]
|
|
444
|
+
# Insert supernet information on each inverse bit of the current subnet
|
|
445
|
+
current_node[1 - next_bit] = supernet_leaf
|
|
446
|
+
current_node[bits[-1]] = leaf
|
|
447
|
+
|
|
448
|
+
def to_db_file(self, filename: str):
|
|
449
|
+
return TreeWriter(self.tree, self._build_meta()).write(filename)
|
|
450
|
+
|
|
451
|
+
def _build_meta(self):
|
|
452
|
+
return {
|
|
453
|
+
"ip_version": self.ip_version,
|
|
454
|
+
"database_type": self.database_type,
|
|
455
|
+
"languages": self.languages,
|
|
456
|
+
"binary_format_major_version": self.binary_format_major_version,
|
|
457
|
+
"binary_format_minor_version": self.binary_format_minor_version,
|
|
458
|
+
"build_epoch": int(time.time()),
|
|
459
|
+
"description": self.description,
|
|
460
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "mmdb-writer"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Make mmdb format ip library file which can be read by maxmind official language reader"
|
|
5
|
+
authors = ["VimT"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [{include = "mmdb_writer.py"}]
|
|
9
|
+
homepage = "https://github.com/vimt/MaxMind-DB-Writer-python"
|
|
10
|
+
|
|
11
|
+
[tool.poetry.dependencies]
|
|
12
|
+
python = ">=3.7,<4.0"
|
|
13
|
+
maxminddb = ">=2.0,<3.0"
|
|
14
|
+
netaddr = ">=0.7.0,<1.0"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
modules = \
|
|
5
|
+
['mmdb_writer']
|
|
6
|
+
install_requires = \
|
|
7
|
+
['maxminddb>=2.0,<3.0', 'netaddr>=0.7.0,<1.0']
|
|
8
|
+
|
|
9
|
+
setup_kwargs = {
|
|
10
|
+
'name': 'mmdb-writer',
|
|
11
|
+
'version': '0.1.1',
|
|
12
|
+
'description': 'Make mmdb format ip library file which can be read by maxmind official language reader',
|
|
13
|
+
'long_description': "# MaxMind-DB-Writer-python\n\nMake `mmdb` format ip library file which can be read by [`maxmind` official language reader](https://dev.maxmind.com/geoip/geoip2/downloadable/)\n\n[The official perl writer](https://github.com/maxmind/MaxMind-DB-Writer-perl) was written in perl, which was difficult to customize. So I implemented the `MaxmindDB format` ip library in python language\n## Install\n```shell script\npip install -U git+https://github.com/VimT/MaxMind-DB-Writer-python\n```\n\n## Usage\n```python\nfrom netaddr import IPSet\n\nfrom mmdb_writer import MMDBWriter\nwriter = MMDBWriter()\n\nwriter.insert_network(IPSet(['1.1.0.0/24', '1.1.1.0/24']), {'country': 'COUNTRY', 'isp': 'ISP'})\nwriter.to_db_file('test.mmdb')\n\nimport maxminddb\nm = maxminddb.open_database('test.mmdb')\nr = m.get('1.1.1.1')\nassert r == {'country': 'COUNTRY', 'isp': 'ISP'}\n```\n\n## Examples\nsee [csv_to_mmdb.py](./examples/csv_to_mmdb.py)\n\n\n## Reference: \n- [MaxmindDB format](http://maxmind.github.io/MaxMind-DB/)\n- [geoip-mmdb](https://github.com/i-rinat/geoip-mmdb)\n",
|
|
14
|
+
'author': 'VimT',
|
|
15
|
+
'author_email': 'None',
|
|
16
|
+
'maintainer': 'None',
|
|
17
|
+
'maintainer_email': 'None',
|
|
18
|
+
'url': 'https://github.com/vimt/MaxMind-DB-Writer-python',
|
|
19
|
+
'py_modules': modules,
|
|
20
|
+
'install_requires': install_requires,
|
|
21
|
+
'python_requires': '>=3.7,<4.0',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
setup(**setup_kwargs)
|