platphorm-maxmind-db 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,539 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/db'
4
+ require 'minitest/autorun'
5
+ require 'mmdb_util'
6
+
7
+ class ReaderTest < Minitest::Test
8
+ def test_reader
9
+ modes = [
10
+ MaxMind::DB::MODE_FILE,
11
+ MaxMind::DB::MODE_MEMORY,
12
+ ]
13
+
14
+ modes.each do |mode|
15
+ [24, 28, 32].each do |record_size|
16
+ [4, 6].each do |ip_version|
17
+ filename = 'test/data/test-data/MaxMind-DB-test-ipv' +
18
+ ip_version.to_s + '-' + record_size.to_s + '.mmdb'
19
+ reader = MaxMind::DB.new(filename, mode: mode)
20
+ check_metadata(reader, ip_version, record_size)
21
+ if ip_version == 4
22
+ check_ipv4(reader, filename)
23
+ else
24
+ check_ipv6(reader, filename)
25
+ end
26
+ reader.close
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def test_reader_inspect
33
+ modes = [
34
+ MaxMind::DB::MODE_FILE,
35
+ MaxMind::DB::MODE_MEMORY,
36
+ ]
37
+
38
+ modes.each do |mode|
39
+ filename = 'test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
40
+ reader = MaxMind::DB.new(filename, mode: mode)
41
+ assert_instance_of(String, reader.inspect)
42
+ end
43
+ end
44
+
45
+ def test_get_with_prefix_len
46
+ decoder_record = {
47
+ 'array' => [1, 2, 3],
48
+ 'boolean' => true,
49
+ 'bytes' => "\x00\x00\x00*",
50
+ 'double' => 42.123456,
51
+ 'float' => 1.100000023841858,
52
+ 'int32' => -268_435_456,
53
+ 'map' => {
54
+ 'mapX' => {
55
+ 'arrayX' => [7, 8, 9],
56
+ 'utf8_stringX' => 'hello',
57
+ },
58
+ },
59
+ 'uint128' => 1_329_227_995_784_915_872_903_807_060_280_344_576,
60
+ 'uint16' => 0x64,
61
+ 'uint32' => 0x10000000,
62
+ 'uint64' => 0x1000000000000000,
63
+ 'utf8_string' => 'unicode! ☯ - ♫',
64
+ }
65
+
66
+ tests = [{
67
+ 'ip' => '1.1.1.1',
68
+ 'file_name' => 'MaxMind-DB-test-ipv6-32.mmdb',
69
+ 'expected_prefix_length' => 8,
70
+ 'expected_record' => nil,
71
+ }, {
72
+ 'ip' => '::1:ffff:ffff',
73
+ 'file_name' => 'MaxMind-DB-test-ipv6-24.mmdb',
74
+ 'expected_prefix_length' => 128,
75
+ 'expected_record' => {
76
+ 'ip' => '::1:ffff:ffff'
77
+ },
78
+ }, {
79
+ 'ip' => '::2:0:1',
80
+ 'file_name' => 'MaxMind-DB-test-ipv6-24.mmdb',
81
+ 'expected_prefix_length' => 122,
82
+ 'expected_record' => {
83
+ 'ip' => '::2:0:0'
84
+ },
85
+ }, {
86
+ 'ip' => '1.1.1.1',
87
+ 'file_name' => 'MaxMind-DB-test-ipv4-24.mmdb',
88
+ 'expected_prefix_length' => 32,
89
+ 'expected_record' => {
90
+ 'ip' => '1.1.1.1'
91
+ },
92
+ }, {
93
+ 'ip' => '1.1.1.3',
94
+ 'file_name' => 'MaxMind-DB-test-ipv4-24.mmdb',
95
+ 'expected_prefix_length' => 31,
96
+ 'expected_record' => {
97
+ 'ip' => '1.1.1.2'
98
+ },
99
+ }, {
100
+ 'ip' => '1.1.1.3',
101
+ 'file_name' => 'MaxMind-DB-test-decoder.mmdb',
102
+ 'expected_prefix_length' => 24,
103
+ 'expected_record' => decoder_record,
104
+ }, {
105
+ 'ip' => '::ffff:1.1.1.128',
106
+ 'file_name' => 'MaxMind-DB-test-decoder.mmdb',
107
+ 'expected_prefix_length' => 120,
108
+ 'expected_record' => decoder_record,
109
+ }, {
110
+ 'ip' => '::1.1.1.128',
111
+ 'file_name' => 'MaxMind-DB-test-decoder.mmdb',
112
+ 'expected_prefix_length' => 120,
113
+ 'expected_record' => decoder_record,
114
+ }, {
115
+ 'ip' => '200.0.2.1',
116
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
117
+ 'expected_prefix_length' => 0,
118
+ 'expected_record' => '::0/64',
119
+ }, {
120
+ 'ip' => '::200.0.2.1',
121
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
122
+ 'expected_prefix_length' => 64,
123
+ 'expected_record' => '::0/64',
124
+ }, {
125
+ 'ip' => '0:0:0:0:ffff:ffff:ffff:ffff',
126
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
127
+ 'expected_prefix_length' => 64,
128
+ 'expected_record' => '::0/64',
129
+ }, {
130
+ 'ip' => 'ef00::',
131
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
132
+ 'expected_prefix_length' => 1,
133
+ 'expected_record' => nil,
134
+ }]
135
+
136
+ tests.each do |test|
137
+ reader = MaxMind::DB.new('test/data/test-data/' + test['file_name'])
138
+ record, prefix_length = reader.get_with_prefix_length(test['ip'])
139
+
140
+ assert_equal(test['expected_prefix_length'], prefix_length,
141
+ format('expected prefix_length of %d for %s in %s but got %p',
142
+ test['expected_prefix_length'], test['ip'],
143
+ test['file_name'], prefix_length))
144
+
145
+ msg = format('expected_record for %s in %s', test['ip'], test['file_name'])
146
+ if test['expected_record'].nil?
147
+ assert_nil(record, msg)
148
+ else
149
+ assert_equal(test['expected_record'], record, msg)
150
+ end
151
+ end
152
+ end
153
+
154
+ def test_decoder
155
+ reader = MaxMind::DB.new(
156
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
157
+ )
158
+ record = reader.get('::1.1.1.0')
159
+ assert_equal([1, 2, 3], record['array'])
160
+ assert_equal(true, record['boolean'])
161
+ assert_equal("\x00\x00\x00*".b, record['bytes'])
162
+ assert_equal(42.123456, record['double'])
163
+ assert_in_delta(1.1, record['float'])
164
+ assert_equal(-268_435_456, record['int32'])
165
+ assert_equal(
166
+ {
167
+ 'mapX' => {
168
+ 'arrayX' => [7, 8, 9],
169
+ 'utf8_stringX' => 'hello',
170
+ },
171
+ },
172
+ record['map'],
173
+ )
174
+ assert_equal(100, record['uint16'])
175
+ assert_equal(268_435_456, record['uint32'])
176
+ assert_equal(1_152_921_504_606_846_976, record['uint64'])
177
+ assert_equal('unicode! ☯ - ♫', record['utf8_string'])
178
+ assert_equal(1_329_227_995_784_915_872_903_807_060_280_344_576, record['uint128'])
179
+ reader.close
180
+ end
181
+
182
+ def test_no_ipv4_search_tree
183
+ reader = MaxMind::DB.new(
184
+ 'test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb'
185
+ )
186
+ assert_equal('::0/64', reader.get('1.1.1.1'))
187
+ assert_equal('::0/64', reader.get('192.1.1.1'))
188
+ reader.close
189
+ end
190
+
191
+ def test_ipv6_address_in_ipv4_database
192
+ reader = MaxMind::DB.new(
193
+ 'test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
194
+ )
195
+ e = assert_raises ArgumentError do
196
+ reader.get('2001::')
197
+ end
198
+ assert_equal(
199
+ 'Error looking up 2001::. You attempted to look up an IPv6 address in an IPv4-only database.',
200
+ e.message,
201
+ )
202
+ reader.close
203
+ end
204
+
205
+ def test_bad_ip_parameter
206
+ reader = MaxMind::DB.new('test/data/test-data/GeoIP2-City-Test.mmdb')
207
+ e = assert_raises ArgumentError do
208
+ reader.get(Object.new)
209
+ end
210
+ assert_equal(
211
+ 'address family must be specified', # Not great, but type is ok
212
+ e.message,
213
+ )
214
+ reader.close
215
+ end
216
+
217
+ def test_broken_database
218
+ reader = MaxMind::DB.new(
219
+ 'test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb'
220
+ )
221
+ e = assert_raises MaxMind::DB::InvalidDatabaseError do
222
+ reader.get('2001:220::')
223
+ end
224
+ assert_equal(
225
+ 'The MaxMind DB file\'s data section contains bad data (unknown data type or corrupt data)',
226
+ e.message,
227
+ )
228
+ reader.close
229
+ end
230
+
231
+ def test_ip_validation
232
+ reader = MaxMind::DB.new(
233
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
234
+ )
235
+ e = assert_raises ArgumentError do
236
+ reader.get('not_ip')
237
+ end
238
+ assert(e.message.match(/invalid address/))
239
+ reader.close
240
+ end
241
+
242
+ def test_missing_database
243
+ e = assert_raises SystemCallError do
244
+ MaxMind::DB.new('file-does-not-exist.mmdb')
245
+ end
246
+ assert(e.message.match(/No such file or directory/))
247
+ end
248
+
249
+ def test_nondatabase
250
+ e = assert_raises MaxMind::DB::InvalidDatabaseError do
251
+ MaxMind::DB.new('README.md')
252
+ end
253
+ assert_equal(
254
+ 'Metadata section not found. Is this a valid MaxMind DB file?',
255
+ e.message,
256
+ )
257
+ end
258
+
259
+ def test_too_many_constructor_args
260
+ e = assert_raises ArgumentError do
261
+ MaxMind::DB.new('README.md', {}, 'blah')
262
+ end
263
+ assert(e.message.match(/wrong number of arguments/))
264
+ end
265
+
266
+ def test_no_constructor_args
267
+ e = assert_raises ArgumentError do
268
+ MaxMind::DB.new
269
+ end
270
+ assert(e.message.match(/wrong number of arguments/))
271
+ end
272
+
273
+ def test_too_many_get_args
274
+ reader = MaxMind::DB.new(
275
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
276
+ )
277
+ e = assert_raises ArgumentError do
278
+ reader.get('1.1.1.1', 'blah')
279
+ end
280
+ assert(e.message.match(/wrong number of arguments/))
281
+ reader.close
282
+ end
283
+
284
+ def test_no_get_args
285
+ reader = MaxMind::DB.new(
286
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
287
+ )
288
+ e = assert_raises ArgumentError do
289
+ reader.get
290
+ end
291
+ assert(e.message.match(/wrong number of arguments/))
292
+ reader.close
293
+ end
294
+
295
+ def test_metadata_args
296
+ reader = MaxMind::DB.new(
297
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
298
+ )
299
+ e = assert_raises ArgumentError do
300
+ reader.metadata('hi')
301
+ end
302
+ assert(e.message.match(/wrong number of arguments/))
303
+ reader.close
304
+ end
305
+
306
+ def test_metadata_unknown_attribute
307
+ reader = MaxMind::DB.new(
308
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
309
+ )
310
+ e = assert_raises NoMethodError do
311
+ reader.metadata.what
312
+ end
313
+ assert(e.message.match(/undefined method `what'/))
314
+ reader.close
315
+ end
316
+
317
+ def test_close
318
+ reader = MaxMind::DB.new(
319
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
320
+ )
321
+ reader.close
322
+ end
323
+
324
+ def test_double_close
325
+ reader = MaxMind::DB.new(
326
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
327
+ )
328
+ reader.close
329
+ reader.close
330
+ end
331
+
332
+ def test_closed_get
333
+ reader = MaxMind::DB.new(
334
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
335
+ )
336
+ reader.close
337
+ e = assert_raises IOError do
338
+ reader.get('1.1.1.1')
339
+ end
340
+ assert_equal('closed stream', e.message)
341
+ end
342
+
343
+ def test_closed_metadata
344
+ reader = MaxMind::DB.new(
345
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
346
+ )
347
+ reader.close
348
+ assert_equal(
349
+ { 'en' => 'MaxMind DB Decoder Test database - contains every MaxMind DB data type' },
350
+ reader.metadata.description,
351
+ )
352
+ end
353
+
354
+ def test_threads
355
+ reader = MaxMind::DB.new(
356
+ 'test/data/test-data/GeoIP2-Domain-Test.mmdb'
357
+ )
358
+
359
+ num_threads = 16
360
+ num_lookups = 32
361
+ thread_lookups = []
362
+ num_threads.times do
363
+ thread_lookups << []
364
+ end
365
+
366
+ threads = []
367
+ num_threads.times do |i|
368
+ threads << Thread.new do
369
+ num_lookups.times do |j|
370
+ thread_lookups[i] << reader.get("65.115.240.#{j}")
371
+ thread_lookups[i] << reader.get("2a02:2770:3::#{j}")
372
+ end
373
+ end
374
+ end
375
+
376
+ threads.each(&:join)
377
+
378
+ thread_lookups.each do |a|
379
+ assert_equal(num_lookups * 2, a.length)
380
+ thread_lookups.each do |b|
381
+ assert_equal(a, b)
382
+ end
383
+ end
384
+
385
+ reader.close
386
+ end
387
+
388
+ # In these tests I am trying to exercise Reader#read_node directly. It is not
389
+ # too easy to test its behaviour with real databases, so construct dummy ones
390
+ # directly.
391
+ #
392
+ def test_read_node
393
+ tests = [
394
+ {
395
+ record_size: 24,
396
+ # Left record + right record
397
+ node_bytes: "\xab\xcd\xef".b + "\xbc\xfe\xfa".b,
398
+ left: 11_259_375,
399
+ right: 12_386_042,
400
+ check_left: "\x00\xab\xcd\xef".b.unpack('N').first,
401
+ check_right: "\x00\xbc\xfe\xfa".b.unpack('N').first,
402
+ },
403
+ {
404
+ record_size: 28,
405
+ # Left record (part) + middle byte + right record (part)
406
+ node_bytes: "\xab\xcd\xef".b + "\x12".b + "\xfd\xdc\xfa".b,
407
+ left: 28_036_591,
408
+ right: 50_191_610,
409
+ check_left: "\x01\xab\xcd\xef".b.unpack('N').first,
410
+ check_right: "\x02\xfd\xdc\xfa".b.unpack('N').first,
411
+ },
412
+ {
413
+ record_size: 32,
414
+ # Left record + right record
415
+ node_bytes: "\xab\xcd\xef\x12".b + "\xfd\xdc\xfa\x15".b,
416
+ left: 2_882_400_018,
417
+ right: 4_259_117_589,
418
+ check_left: "\xab\xcd\xef\x12".b.unpack('N').first,
419
+ check_right: "\xfd\xdc\xfa\x15".b.unpack('N').first,
420
+ },
421
+ ]
422
+
423
+ tests.each do |test|
424
+ buf = ''.b
425
+ buf += test[:node_bytes]
426
+
427
+ buf += "\x00".b * 16
428
+
429
+ buf += "\xab\xcd\xefMaxMind.com".b
430
+ buf += MMDBUtil.make_metadata_map(test[:record_size])
431
+
432
+ reader = MaxMind::DB.new(
433
+ buf, mode: MaxMind::DB::MODE_PARAM_IS_BUFFER
434
+ )
435
+
436
+ assert_equal(reader.metadata.record_size, test[:record_size])
437
+
438
+ assert_equal(test[:left], reader.send(:read_node, 0, 0))
439
+ assert_equal(test[:right], reader.send(:read_node, 0, 1))
440
+ assert_equal(test[:left], test[:check_left])
441
+ assert_equal(test[:right], test[:check_right])
442
+ end
443
+ end
444
+
445
+ def check_metadata(reader, ip_version, record_size)
446
+ metadata = reader.metadata
447
+
448
+ assert_equal(2, metadata.binary_format_major_version, 'major_version')
449
+ assert_equal(0, metadata.binary_format_minor_version, 'minor_version')
450
+ assert_operator(metadata.build_epoch, :>, 1_373_571_901, 'build_epoch')
451
+ assert_equal('Test', metadata.database_type, 'database_type')
452
+ assert_equal(
453
+ {
454
+ 'en' => 'Test Database',
455
+ 'zh' => 'Test Database Chinese',
456
+ },
457
+ metadata.description,
458
+ 'description',
459
+ )
460
+ assert_equal(ip_version, metadata.ip_version, 'ip_version')
461
+ assert_equal(%w[en zh], metadata.languages, 'languages')
462
+ assert_operator(metadata.node_count, :>, 36, 'node_count')
463
+ assert_equal(record_size, metadata.record_size, 'record_size')
464
+ end
465
+
466
+ def check_ipv4(reader, filename)
467
+ 6.times do |i|
468
+ address = "1.1.1.#{2**i}"
469
+ assert_equal(
470
+ { 'ip' => address },
471
+ reader.get(address),
472
+ "found expected data record for #{address} in #{filename}",
473
+ )
474
+ end
475
+
476
+ pairs = {
477
+ '1.1.1.3' => '1.1.1.2',
478
+ '1.1.1.5' => '1.1.1.4',
479
+ '1.1.1.7' => '1.1.1.4',
480
+ '1.1.1.9' => '1.1.1.8',
481
+ '1.1.1.15' => '1.1.1.8',
482
+ '1.1.1.17' => '1.1.1.16',
483
+ '1.1.1.31' => '1.1.1.16',
484
+ }
485
+ pairs.each do |key_address, value_address|
486
+ data = { 'ip' => value_address }
487
+ assert_equal(
488
+ data,
489
+ reader.get(key_address),
490
+ "found expected data record for #{key_address} in #{filename}",
491
+ )
492
+ end
493
+
494
+ ['1.1.1.33', '255.254.253.123'].each do |ip|
495
+ assert_nil(
496
+ reader.get(ip),
497
+ "#{ip} is not in #{filename}",
498
+ )
499
+ end
500
+ end
501
+
502
+ def check_ipv6(reader, filename)
503
+ subnets = [
504
+ '::1:ffff:ffff', '::2:0:0', '::2:0:40', '::2:0:50', '::2:0:58',
505
+ ]
506
+ subnets.each do |address|
507
+ assert_equal(
508
+ { 'ip' => address },
509
+ reader.get(address),
510
+ "found expected data record for #{address} in #{filename}",
511
+ )
512
+ end
513
+
514
+ pairs = {
515
+ '::2:0:1' => '::2:0:0',
516
+ '::2:0:33' => '::2:0:0',
517
+ '::2:0:39' => '::2:0:0',
518
+ '::2:0:41' => '::2:0:40',
519
+ '::2:0:49' => '::2:0:40',
520
+ '::2:0:52' => '::2:0:50',
521
+ '::2:0:57' => '::2:0:50',
522
+ '::2:0:59' => '::2:0:58',
523
+ }
524
+ pairs.each do |key_address, value_address|
525
+ assert_equal(
526
+ { 'ip' => value_address },
527
+ reader.get(key_address),
528
+ "found expected data record for #{key_address} in #{filename}",
529
+ )
530
+ end
531
+
532
+ ['1.1.1.33', '255.254.253.123', '89fa::'].each do |ip|
533
+ assert_nil(
534
+ reader.get(ip),
535
+ "#{ip} is not in #{filename}",
536
+ )
537
+ end
538
+ end
539
+ end