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 206 207
|
require "maxminddb/version"
require 'maxminddb/result'
require 'maxminddb/reader'
require 'ipaddr'
module MaxMindDB
# The default reader for MaxMindDB files. Reads the database into memory.
# This creates a higher memory overhead, but faster lookup times.
DEFAULT_FILE_READER = proc { |path| File.binread(path) }
# A low memory file reader for MaxMindDB files. Avoids reading the database
# into memory. Has a lower memory footprint but slower lookup times.
LOW_MEMORY_FILE_READER = proc { |path| MaxMindDB::LowMemoryReader.new(path) }
def self.new(path, file_reader=DEFAULT_FILE_READER)
Client.new(path, file_reader)
end
class Client
METADATA_BEGIN_MARKER = ([0xAB, 0xCD, 0xEF].pack('C*') + 'MaxMind.com').encode('ascii-8bit', 'ascii-8bit')
DATA_SECTION_SEPARATOR_SIZE = 16
SIZE_BASE_VALUES = [0, 29, 285, 65821]
POINTER_BASE_VALUES = [0, 0, 2048, 526336]
attr_reader :metadata
# An IP that is used instead of local IPs
attr_accessor :local_ip_alias
def initialize(path, file_reader = DEFAULT_FILE_READER)
@path = path
@data = file_reader.call(path)
pos = @data.rindex(METADATA_BEGIN_MARKER)
raise 'invalid file format' unless pos
pos += METADATA_BEGIN_MARKER.size
@metadata = decode(pos, 0)[1]
@ip_version = @metadata['ip_version']
@node_count = @metadata['node_count']
@node_byte_size = @metadata['record_size'] * 2 / 8
@search_tree_size = @node_count * @node_byte_size
end
def inspect
"#<MaxMindDB::Client: DBPath:'#{@path}'>"
end
def lookup(ip_or_hostname)
if @local_ip_alias && is_local?(ip_or_hostname)
ip_or_hostname = @local_ip_alias
end
node_no = 0
addr = addr_from_ip(ip_or_hostname)
start_idx = @ip_version == 4 ? 96 : 0
for i in start_idx ... 128
flag = (addr >> (127 - i)) & 1
next_node_no = read_record(node_no, flag)
if next_node_no == 0
raise 'invalid file format'
elsif next_node_no >= @node_count
data_section_start = @search_tree_size + DATA_SECTION_SEPARATOR_SIZE
pos = (next_node_no - @node_count) - DATA_SECTION_SEPARATOR_SIZE
result = decode(pos, data_section_start)[1]
result['network'] = network_from_addr(addr, i) unless result.empty?
return MaxMindDB::Result.new(result)
else
node_no = next_node_no
end
end
raise 'invalid file format'
end
private
def read_record(node_no, flag)
rec_byte_size = @node_byte_size / 2
pos = @node_byte_size * node_no
middle = @data[pos + rec_byte_size].ord if @node_byte_size.odd?
if flag == 0 # left
val = read_value(pos, 0, rec_byte_size)
val += ((middle & 0xf0) << 20) if middle
else # right
val = read_value(pos + @node_byte_size - rec_byte_size, 0, rec_byte_size)
val += ((middle & 0xf) << 24) if middle
end
val
end
def decode(pos, base_pos)
ctrl = @data[pos + base_pos].ord
pos += 1
type = ctrl >> 5
if type == 1 # pointer
size = ((ctrl >> 3) & 0x3) + 1
v1 = ctrl & 0x7
v2 = read_value(pos, base_pos, size)
pos += size
pointer = (v1 << (8 * size)) + v2 + POINTER_BASE_VALUES[size]
val = decode(pointer, base_pos)[1]
else
if type == 0 # extended type
type = 7 + @data[pos + base_pos].ord
pos += 1
end
size = ctrl & 0x1f
if size >= 29
byte_size = size - 29 + 1
val = read_value(pos, base_pos, byte_size)
pos += byte_size
size = val + SIZE_BASE_VALUES[byte_size]
end
case type
when 2 # utf8
val = @data[pos + base_pos, size].encode('utf-8', 'utf-8')
pos += size
when 3 # double
val = @data[pos + base_pos, size].unpack('G')[0]
pos += size
when 4 # bytes
val = @data[pos + base_pos, size]
pos += size
when 5 # unsigned 16-bit int
val = read_value(pos, base_pos, size)
pos += size
when 6 # unsigned 32-bit int
val = read_value(pos, base_pos, size)
pos += size
when 7 # map
val = {}
size.times do
pos, k = decode(pos, base_pos)
pos, v = decode(pos, base_pos)
val[k] = v
end
when 8 # signed 32-bit int
v1 = @data[pos + base_pos, size].unpack('N')[0]
bits = size * 8
val = (v1 & ~(1 << bits)) - (v1 & (1 << bits))
pos += size
when 9 # unsigned 64-bit int
val = read_value(pos, base_pos, size)
pos += size
when 10 # unsigned 128-bit int
val = read_value(pos, base_pos, size)
pos += size
when 11 # array
val = []
size.times do
pos, v = decode(pos, base_pos)
val.push(v)
end
when 12 # data cache container
raise 'TODO:'
when 13 # end marker
val = nil
when 14 # boolean
val = (size != 0)
when 15 # float
val = @data[pos + base_pos, size].unpack('g')[0]
pos += size
end
end
[pos, val]
end
def read_value(pos, base_pos, size)
bytes = @data[pos + base_pos, size].unpack('C*')
bytes.inject(0){|r, v| (r << 8) + v }
end
def addr_from_ip(ip_or_hostname)
klass = ip_or_hostname.class
return ip_or_hostname if RUBY_VERSION.to_f < 2.4 && (klass == Fixnum || klass == Bignum)
return ip_or_hostname if RUBY_VERSION.to_f >= 2.4 && klass == Integer
addr = IPAddr.new(ip_or_hostname)
addr = addr.ipv4_compat if addr.ipv4?
addr.to_i
end
def network_from_addr(addr, i)
fam = addr > 4294967295 ? Socket::AF_INET6 : Socket::AF_INET
ip = IPAddr.new(addr, family = fam)
subnet_size = ip.ipv4? ? i - 96 + 1 : i + 1
subnet = IPAddr.new("#{ip}/#{subnet_size}")
"#{subnet}/#{subnet_size}"
end
def is_local?(ip_or_hostname)
["127.0.0.1", "localhost", "::1", "0000::1", "0:0:0:0:0:0:0:1"].include? ip_or_hostname
end
end
end
# vim: et ts=2 sw=2 ff=unix
|