iparty 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/Rakefile +62 -0
- data/exe/iparty +10 -0
- data/lib/iparty/address.rb +146 -0
- data/lib/iparty/cli/application/actions.rb +56 -0
- data/lib/iparty/cli/application/appinfo.rb +107 -0
- data/lib/iparty/cli/application/irb_context.rb +41 -0
- data/lib/iparty/cli/application/options.rb +133 -0
- data/lib/iparty/cli/application.rb +258 -0
- data/lib/iparty/cli/colorize.rb +39 -0
- data/lib/iparty/cli/formatter.rb +169 -0
- data/lib/iparty/config.rb +141 -0
- data/lib/iparty/max_mind/database.rb +216 -0
- data/lib/iparty/max_mind/eager_reader.rb +33 -0
- data/lib/iparty/max_mind/lazy_reader.rb +47 -0
- data/lib/iparty/max_mind/result.rb +205 -0
- data/lib/iparty/max_mind.rb +93 -0
- data/lib/iparty/railtie.rb +22 -0
- data/lib/iparty/rake_task.rb +121 -0
- data/lib/iparty/version.rb +5 -0
- data/lib/iparty.rb +71 -0
- metadata +125 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IParty
|
|
4
|
+
Config = Struct.new(
|
|
5
|
+
:account_id,
|
|
6
|
+
:license_key,
|
|
7
|
+
:mirror,
|
|
8
|
+
:directory,
|
|
9
|
+
:editions,
|
|
10
|
+
:eager_load,
|
|
11
|
+
:singletons,
|
|
12
|
+
:local_ip_alias,
|
|
13
|
+
:ipv6_significant,
|
|
14
|
+
:url_to_mmdb,
|
|
15
|
+
:annotations,
|
|
16
|
+
keyword_init: true,
|
|
17
|
+
) do
|
|
18
|
+
def singletons=(val)
|
|
19
|
+
self[:singletons] = val
|
|
20
|
+
init_singletons! if val == true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def init_singletons!
|
|
24
|
+
self[:singletons] = {} if !self[:singletons] || self[:singletons] == true
|
|
25
|
+
|
|
26
|
+
editions.each {|edition| IParty::MaxMind.db(edition) }
|
|
27
|
+
self[:singletons]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def env_value *args, **kw, &block
|
|
31
|
+
IParty.env_value(*args, **kw, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def annotate *args, **kw
|
|
35
|
+
self[:annotations] ||= {}
|
|
36
|
+
result = {}
|
|
37
|
+
IParty.expand_hostnames(args).each do |net|
|
|
38
|
+
ipp = IParty(net)
|
|
39
|
+
kw[:tags] = ((result[ipp] || self[:annotations][ipp] || {})[:tags] || []) | kw[:tags] if kw.key?(:tags)
|
|
40
|
+
result[ipp] = (result[ipp] || self[:annotations][ipp] || {}).merge(kw)
|
|
41
|
+
end
|
|
42
|
+
self[:annotations].merge!(result)
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def annotate_tag tags, *args
|
|
47
|
+
self[:annotations] ||= {}
|
|
48
|
+
IParty.expand_hostnames(args).to_h do |net|
|
|
49
|
+
ipp = IParty(net)
|
|
50
|
+
self[:annotations][ipp] ||= {}
|
|
51
|
+
self[:annotations][ipp][:tags] ||= []
|
|
52
|
+
self[:annotations][ipp][:tags] |= Array(tags)
|
|
53
|
+
[net, self[:annotations][ipp][:tags]]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
attr_accessor :config
|
|
60
|
+
|
|
61
|
+
def configure
|
|
62
|
+
yield(config)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def env_value key, default = nil, &vproc
|
|
66
|
+
env_value = ENV.fetch(key, default)
|
|
67
|
+
|
|
68
|
+
env_value = case env_value
|
|
69
|
+
when "1", "true", "on", "yes" then true
|
|
70
|
+
when "0", "false", "off", "no" then false
|
|
71
|
+
when "" then nil
|
|
72
|
+
else env_value
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
vproc ? vproc.call(env_value) : env_value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def with_config to_merge = {}, &block
|
|
79
|
+
config_was = @config
|
|
80
|
+
new_config = Config.new(config_was.to_h.merge(to_merge))
|
|
81
|
+
|
|
82
|
+
if block
|
|
83
|
+
begin
|
|
84
|
+
block.call(self.config = new_config)
|
|
85
|
+
ensure
|
|
86
|
+
self.config = config_was
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
new_config
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def default_config
|
|
94
|
+
Config.new(
|
|
95
|
+
# If set to false drop last half of v6-addresses as they are insignificant for most applications.
|
|
96
|
+
# The then 64-bit addresses also fit into unsigned bigints allowing for easy range representations.
|
|
97
|
+
# Each IParty::Address can overwrite this with
|
|
98
|
+
# * #ipv6_significant accessors
|
|
99
|
+
# * significant: keyword (affected methods only but including new/initialize)
|
|
100
|
+
ipv6_significant: env_value("IPARTY_IPV6_SIGNIFICANT", true),
|
|
101
|
+
|
|
102
|
+
# Whether to use the low memory file reader or load mmdb into memory as a whole (see docs/BENCHMARK.md)
|
|
103
|
+
eager_load: env_value("IPARTY_EAGER_LOAD", false),
|
|
104
|
+
|
|
105
|
+
# Singleton instances (may be false, true, hash or proc which returns hash-like)
|
|
106
|
+
singletons: env_value("IPARTY_SINGLETONS", false),
|
|
107
|
+
|
|
108
|
+
# An IP that is used instead of local IPs
|
|
109
|
+
local_ip_alias: env_value("IPARTY_LOCAL_IP_ALIAS", nil),
|
|
110
|
+
|
|
111
|
+
# MaxMind account_id and license_key aka mirror basic-auth
|
|
112
|
+
account_id: env_value("MAXMIND_ACCOUNT_ID", nil),
|
|
113
|
+
license_key: env_value("MAXMIND_LICENSE_KEY", nil),
|
|
114
|
+
|
|
115
|
+
# Mirror to download tar.gz compressed mmdb-files from
|
|
116
|
+
mirror: env_value("MAXMIND_MIRROR", "https://download.maxmind.com/geoip/databases/:edition/download?suffix=tar.gz"),
|
|
117
|
+
|
|
118
|
+
# The mmdb editions to fetch, you don't really need country if you have city
|
|
119
|
+
editions: env_value("MAXMIND_EDITIONS", "GeoLite2-ASN GeoLite2-Country GeoLite2-City") {|v| v.split(/\s+|,\s*/) },
|
|
120
|
+
|
|
121
|
+
# Directory to store mmdb-files in, also creates .updating subdirectory
|
|
122
|
+
directory: env_value("IPARTY_DIRECTORY", nil) do |dir|
|
|
123
|
+
if dir && !dir.empty?
|
|
124
|
+
Pathname.new(dir)
|
|
125
|
+
else
|
|
126
|
+
Pathname.new(Dir.tmpdir).join("iparty")
|
|
127
|
+
end
|
|
128
|
+
end,
|
|
129
|
+
|
|
130
|
+
# Proc to turn URL into mmdb-file(s) inside target directory.
|
|
131
|
+
# All .mmdb files will be moved and then dir gets removed.
|
|
132
|
+
url_to_mmdb: proc do |url, dir, config|
|
|
133
|
+
auth = %{-u "#{config.account_id}:#{config.license_key}"} if config.account_id && config.license_key
|
|
134
|
+
curl = %{curl -L -s #{"#{auth} " if auth}"#{url}"}
|
|
135
|
+
tar = %{tar xz --strip-components 1 --exclude "*.txt" --no-same-owner -C #{dir.to_s.shellescape}}
|
|
136
|
+
system("#{curl} | #{tar}")
|
|
137
|
+
end,
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lazy_reader"
|
|
4
|
+
require_relative "eager_reader"
|
|
5
|
+
require_relative "result"
|
|
6
|
+
|
|
7
|
+
module IParty
|
|
8
|
+
class MaxMind
|
|
9
|
+
class Database
|
|
10
|
+
class Error < IParty::Error; end
|
|
11
|
+
class InvalidFileFormatError < Error; end
|
|
12
|
+
|
|
13
|
+
METADATA_BEGIN_MARKER = "#{[0xAB, 0xCD, 0xEF].pack("C*")}MaxMind.com".encode("ascii-8bit", "ascii-8bit").freeze
|
|
14
|
+
DATA_SECTION_SEPARATOR_SIZE = 16
|
|
15
|
+
SIZE_BASE_VALUES = [0, 29, 285, 65_821].freeze
|
|
16
|
+
POINTER_BASE_VALUES = [0, 0, 2048, 526_336].freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :metadata
|
|
19
|
+
|
|
20
|
+
def initialize(path, reader: EagerReader)
|
|
21
|
+
@path = path
|
|
22
|
+
@data = reader.new(path)
|
|
23
|
+
|
|
24
|
+
pos = @data.rindex(METADATA_BEGIN_MARKER) || raise(InvalidFileFormatError, "invalid file format")
|
|
25
|
+
pos += METADATA_BEGIN_MARKER.size
|
|
26
|
+
@metadata = decode(0, pos)[1]
|
|
27
|
+
|
|
28
|
+
@ip_version = @metadata[:ip_version]
|
|
29
|
+
@start_idx = @ip_version == 4 ? 96 : 0
|
|
30
|
+
@node_count = @metadata[:node_count]
|
|
31
|
+
@node_byte_size = @metadata[:record_size] * 2 / 8
|
|
32
|
+
@search_tree_size = @node_count * @node_byte_size
|
|
33
|
+
@data_section_start = @search_tree_size + DATA_SECTION_SEPARATOR_SIZE
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def close
|
|
37
|
+
@data.close
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def closed?
|
|
41
|
+
@data.closed?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspect
|
|
45
|
+
"#<#{self.class}:#{format("0x%x", object_id << 1)}: @path:#{@path} @metadata:#{@metadata}>"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def lookup(addr, result_class: Result::Geo)
|
|
49
|
+
addr = IPAddr.new(addr) unless addr.is_a?(IPAddr)
|
|
50
|
+
addr = IParty.config.local_ip_alias if IParty.config.local_ip_alias && addr.loopback?
|
|
51
|
+
addr = IPAddr.new(addr) unless addr.is_a?(IPAddr)
|
|
52
|
+
addr = addr.ipv4_compat if addr.ipv4?
|
|
53
|
+
long = addr.is_a?(IParty::Address) ? addr.to_i(significant: true) : addr.to_i
|
|
54
|
+
node_no = 0
|
|
55
|
+
|
|
56
|
+
(@start_idx...128).each do |i|
|
|
57
|
+
flag = (long >> (127 - i)) & 1
|
|
58
|
+
next_node_no = read_record(node_no, flag)
|
|
59
|
+
|
|
60
|
+
if next_node_no == 0
|
|
61
|
+
raise(InvalidFileFormatError, "invalid file format")
|
|
62
|
+
elsif next_node_no >= @node_count
|
|
63
|
+
pos = (next_node_no - @node_count) - DATA_SECTION_SEPARATOR_SIZE
|
|
64
|
+
result = decode(@data_section_start, pos)[1]
|
|
65
|
+
result[:network] = cidr_from_long(long, i) unless result.empty?
|
|
66
|
+
return result_class.new(result)
|
|
67
|
+
else
|
|
68
|
+
node_no = next_node_no
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
raise(InvalidFileFormatError, "invalid file format")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def cidr_from_long(long, net)
|
|
76
|
+
addr = IPAddr.new(long, long > (2**32) - 1 ? Socket::AF_INET6 : Socket::AF_INET)
|
|
77
|
+
subnet_size = addr.ipv4? ? net - 96 + 1 : net + 1
|
|
78
|
+
subnet = IPAddr.new("#{addr}/#{subnet_size}")
|
|
79
|
+
|
|
80
|
+
"#{subnet}/#{subnet_size}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def read_record(node_no, flag)
|
|
84
|
+
rec_byte_size = @node_byte_size / 2
|
|
85
|
+
pos = @node_byte_size * node_no
|
|
86
|
+
middle = @data[pos + rec_byte_size].ord if @node_byte_size.odd?
|
|
87
|
+
|
|
88
|
+
if flag == 0 # left
|
|
89
|
+
val = read_value(pos, 0, rec_byte_size)
|
|
90
|
+
val += ((middle & 0xf0) << 20) if middle
|
|
91
|
+
else # right
|
|
92
|
+
val = read_value(pos + @node_byte_size - rec_byte_size, 0, rec_byte_size)
|
|
93
|
+
val += ((middle & 0xf) << 24) if middle
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
val
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read_value(base_pos, pos, size)
|
|
100
|
+
bytes = @data[base_pos + pos, size].unpack("C*")
|
|
101
|
+
bytes.inject(0){|r, v| (r << 8) + v }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# rubocop:disable Metrics/CyclomaticComplexity -- we could reduce this, sacrificing the neat overview
|
|
105
|
+
def decode base_pos, pos
|
|
106
|
+
ctrl = @data[base_pos + pos].ord
|
|
107
|
+
pos += 1
|
|
108
|
+
type = ctrl >> 5
|
|
109
|
+
|
|
110
|
+
if type == 1 # pointer
|
|
111
|
+
decode_pointer(base_pos, pos, ctrl)
|
|
112
|
+
else
|
|
113
|
+
if type == 0 # extended type
|
|
114
|
+
type = 7 + @data[base_pos + pos].ord
|
|
115
|
+
pos += 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
size = ctrl & 0x1f
|
|
119
|
+
if size >= 29
|
|
120
|
+
byte_size = size - 29 + 1
|
|
121
|
+
val = read_value(base_pos, pos, byte_size)
|
|
122
|
+
pos += byte_size
|
|
123
|
+
size = val + SIZE_BASE_VALUES[byte_size]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# rubocop:disable Lint/DuplicateBranch -- readable order is more important
|
|
127
|
+
case type
|
|
128
|
+
when 2 # utf8
|
|
129
|
+
decode_utf8(base_pos, pos, size)
|
|
130
|
+
when 3 # double
|
|
131
|
+
decode_double(base_pos, pos, size)
|
|
132
|
+
when 4 # bytes
|
|
133
|
+
decode_bytes(base_pos, pos, size)
|
|
134
|
+
when 5 # unsigned 16-bit int
|
|
135
|
+
decode_uint(base_pos, pos, size)
|
|
136
|
+
when 6 # unsigned 32-bit int
|
|
137
|
+
decode_uint(base_pos, pos, size)
|
|
138
|
+
when 7 # map
|
|
139
|
+
decode_map(base_pos, pos, size)
|
|
140
|
+
when 8 # signed 32-bit int
|
|
141
|
+
decode_int(base_pos, pos, size)
|
|
142
|
+
when 9 # unsigned 64-bit int
|
|
143
|
+
decode_uint(base_pos, pos, size)
|
|
144
|
+
when 10 # unsigned 128-bit int
|
|
145
|
+
decode_uint(base_pos, pos, size)
|
|
146
|
+
when 11 # array
|
|
147
|
+
decode_array(base_pos, pos, size)
|
|
148
|
+
when 12 # (deprecated) data cache container
|
|
149
|
+
raise "TODO: (deprecated) data cache container format"
|
|
150
|
+
when 13 # (deprecated) end marker
|
|
151
|
+
[pos, nil]
|
|
152
|
+
when 14 # boolean
|
|
153
|
+
[pos, size != 0]
|
|
154
|
+
when 15 # float
|
|
155
|
+
decode_float(base_pos, pos, size)
|
|
156
|
+
end
|
|
157
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
161
|
+
|
|
162
|
+
def decode_double base_pos, pos, size
|
|
163
|
+
[pos + size, @data[base_pos + pos, size].unpack1("G")]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def decode_bytes base_pos, pos, size
|
|
167
|
+
[pos + size, @data[base_pos + pos, size]]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def decode_int base_pos, pos, size
|
|
171
|
+
v1 = @data[base_pos + pos, size].unpack1("N")
|
|
172
|
+
bits = size * 8
|
|
173
|
+
[pos + size, (v1 & ~(1 << bits)) - (v1 & (1 << bits))]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def decode_uint base_pos, pos, size
|
|
177
|
+
[pos + size, read_value(base_pos, pos, size)]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def decode_float base_pos, pos, size
|
|
181
|
+
[pos + size, @data[base_pos + pos, size].unpack1("g")]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def decode_utf8 base_pos, pos, size
|
|
185
|
+
[pos + size, @data[base_pos + pos, size].encode("utf-8", "utf-8")]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def decode_pointer base_pos, pos, ctrl
|
|
189
|
+
size = ((ctrl >> 3) & 0x3) + 1
|
|
190
|
+
v1 = ctrl & 0x7
|
|
191
|
+
v2 = read_value(base_pos, pos, size)
|
|
192
|
+
|
|
193
|
+
pointer = (v1 << (8 * size)) + v2 + POINTER_BASE_VALUES[size]
|
|
194
|
+
[pos + size, decode(base_pos, pointer)[1]]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def decode_array base_pos, pos, size
|
|
198
|
+
ary = Array.new(size) do
|
|
199
|
+
pos, v = decode(base_pos, pos)
|
|
200
|
+
v
|
|
201
|
+
end
|
|
202
|
+
[pos, ary]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def decode_map base_pos, pos, size
|
|
206
|
+
map = {}
|
|
207
|
+
size.times do
|
|
208
|
+
pos, k = decode(base_pos, pos)
|
|
209
|
+
pos, v = decode(base_pos, pos)
|
|
210
|
+
map[k.to_sym] = v
|
|
211
|
+
end
|
|
212
|
+
[pos, map]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IParty
|
|
4
|
+
class MaxMind
|
|
5
|
+
# A fast read-access reader for MaxMindDB (mmdb) files. Reads the database into memory.
|
|
6
|
+
# This creates a higher memory overhead and slower init phase but faster lookup times.
|
|
7
|
+
class EagerReader
|
|
8
|
+
def initialize path
|
|
9
|
+
@data = File.binread(path)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [] pos, length = 1
|
|
13
|
+
raise IOError, "closed stream" unless @data
|
|
14
|
+
|
|
15
|
+
@data.slice(pos, length)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rindex search
|
|
19
|
+
raise IOError, "closed stream" unless @data
|
|
20
|
+
|
|
21
|
+
@data.rindex(search)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def close
|
|
25
|
+
@data = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def closed?
|
|
29
|
+
@data.nil?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IParty
|
|
4
|
+
class MaxMind
|
|
5
|
+
# A low memory file reader for MaxMindDB (mmdb) files. Avoids reading the database into memory.
|
|
6
|
+
# Has a lower memory footprint but slower lookup times.
|
|
7
|
+
class LazyReader
|
|
8
|
+
METADATA_MAX_SIZE = 128 * 1024
|
|
9
|
+
|
|
10
|
+
def initialize path
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
@file = File.open(path, "rb")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [] pos, length = 1
|
|
16
|
+
atomic_read(length, pos)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def close
|
|
20
|
+
@file.close
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def closed?
|
|
24
|
+
@file.closed?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rindex search
|
|
28
|
+
base = [0, @file.size - METADATA_MAX_SIZE].max
|
|
29
|
+
tail = atomic_read(METADATA_MAX_SIZE, base)
|
|
30
|
+
pos = tail.rindex(search)
|
|
31
|
+
base + pos unless pos.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def atomic_read length, pos
|
|
35
|
+
# Prefer `pread` in environments where it is available. `pread` provides atomic file access across processes.
|
|
36
|
+
if @file.respond_to?(:pread)
|
|
37
|
+
@file.pread(length, pos)
|
|
38
|
+
else
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
@file.seek(pos)
|
|
41
|
+
@file.read(length)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IParty
|
|
4
|
+
class MaxMind
|
|
5
|
+
class Result < Hash
|
|
6
|
+
class << self
|
|
7
|
+
def define_attr(name, attribute = name, type: nil, aliases: nil, memoize: nil, export: nil, &transform)
|
|
8
|
+
ivar = :"@#{attribute}"
|
|
9
|
+
transform ||= ->(v) { type.new(v) } if type
|
|
10
|
+
memoize = true if memoize.nil? && transform
|
|
11
|
+
method_names = [name] + Array(aliases)
|
|
12
|
+
|
|
13
|
+
method_names.each do |meth|
|
|
14
|
+
define_method(meth) do
|
|
15
|
+
value = dig(attribute)
|
|
16
|
+
return transform ? instance_exec(value, &transform) : value unless memoize
|
|
17
|
+
return instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
|
18
|
+
|
|
19
|
+
instance_variable_set(ivar, instance_exec(value, &transform))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Array(export).each{|exp| export_attr(name, exp == true ? name : exp) }
|
|
24
|
+
|
|
25
|
+
method_names
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def export_attr name, export
|
|
29
|
+
if self == Result::Asn
|
|
30
|
+
IParty::Address.def_delegator(:asn, name, export)
|
|
31
|
+
elsif self == Result::Geo || self == Result
|
|
32
|
+
IParty::Address.def_delegator(:geo, name, export)
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "you can only export on Result, Geo(*) and Asn (got #{self})"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Location < Result
|
|
40
|
+
define_attr(:accuracy_radius)
|
|
41
|
+
define_attr(:latitude)
|
|
42
|
+
define_attr(:longitude)
|
|
43
|
+
define_attr(:metro_code)
|
|
44
|
+
define_attr(:time_zone)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class NamedLocation < Result
|
|
48
|
+
define_attr(:code)
|
|
49
|
+
define_attr(:geoname_id)
|
|
50
|
+
define_attr(:is_in_european_union, aliases: :in_european_union?)
|
|
51
|
+
define_attr(:iso_code)
|
|
52
|
+
define_attr(:names, type: Result)
|
|
53
|
+
|
|
54
|
+
def name(locale = :en, fallback_locale: :en)
|
|
55
|
+
return unless all = dig(:names)
|
|
56
|
+
|
|
57
|
+
all[locale] || (all[fallback_locale] if fallback_locale)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# dynamic cast
|
|
61
|
+
alias_method :to_s, :name
|
|
62
|
+
alias_method :to_i, :geoname_id
|
|
63
|
+
|
|
64
|
+
# dynamic comparison
|
|
65
|
+
def == other
|
|
66
|
+
case other
|
|
67
|
+
when String
|
|
68
|
+
name && other == name
|
|
69
|
+
when Numeric
|
|
70
|
+
geoname_id && other == geoname_id
|
|
71
|
+
else
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# dynamic inquiry
|
|
77
|
+
define_attr(:inquire_on_name) { name&.downcase&.tr(" ", "_") }
|
|
78
|
+
define_attr(:inquire_on_code) { (iso_code || code)&.downcase }
|
|
79
|
+
|
|
80
|
+
def respond_to_missing? method_name, include_private = false
|
|
81
|
+
method_name.end_with?("?") || super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def method_missing method_name, *arguments
|
|
85
|
+
if method_name.end_with?("?")
|
|
86
|
+
method_name[0..-2] == (method_name.length == 3 ? inquire_on_code : inquire_on_name)
|
|
87
|
+
else
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class City < NamedLocation
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class Continent < NamedLocation
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Country < NamedLocation
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class Subdivision < NamedLocation
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class Subdivisions < Array
|
|
106
|
+
def initialize raw
|
|
107
|
+
super((raw || []).map{|hash| Subdivision.new(hash) })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspect
|
|
111
|
+
"#<#{self.class}:#{format("0x%x", object_id << 1)}: #{super}>"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
alias_method :blank?, :empty?
|
|
115
|
+
|
|
116
|
+
def present?
|
|
117
|
+
!empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def presence
|
|
121
|
+
self if present?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def least_specific
|
|
125
|
+
first || Subdivision.new
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def most_specific
|
|
129
|
+
last || Subdivision.new
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class Postal < Result
|
|
134
|
+
define_attr(:code)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class Traits < Result
|
|
138
|
+
define_attr(:is_anonymous_proxy, aliases: :anonymous_proxy?)
|
|
139
|
+
define_attr(:is_satellite_provider, aliases: :satellite_provider?)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------
|
|
143
|
+
|
|
144
|
+
class Asn < Result
|
|
145
|
+
def initialize(data = {})
|
|
146
|
+
data ? super({ autonomous_system_network: data[:network] }.compact.merge(data)) : super()
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
define_attr(:autonomous_system_network, aliases: :network, export: true)
|
|
150
|
+
define_attr(:autonomous_system_number, aliases: :number, export: true)
|
|
151
|
+
define_attr(:autonomous_system_organization, aliases: :organization, export: true)
|
|
152
|
+
define_attr(:autonomous_system_detailed, aliases: :detailed, export: true) { "AS#{number} #{organization}" if number }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class Geo < Result
|
|
156
|
+
define_attr(:city, type: City, export: true)
|
|
157
|
+
define_attr(:connection_type)
|
|
158
|
+
define_attr(:continent, type: Continent, export: true)
|
|
159
|
+
define_attr(:country, type: Country, export: true)
|
|
160
|
+
define_attr(:location, type: Location)
|
|
161
|
+
define_attr(:network)
|
|
162
|
+
define_attr(:postal, type: Postal)
|
|
163
|
+
define_attr(:registered_country, type: Country)
|
|
164
|
+
define_attr(:represented_country, type: Country)
|
|
165
|
+
define_attr(:subdivisions, type: Subdivisions)
|
|
166
|
+
define_attr(:traits, type: Traits)
|
|
167
|
+
|
|
168
|
+
define_attr(:accuracy_radius, memoize: false) { location.accuracy_radius }
|
|
169
|
+
define_attr(:is_in_european_union, aliases: :in_european_union?, export: :in_european_union?, memoize: false) { country.is_in_european_union }
|
|
170
|
+
define_attr(:latitude, memoize: false, export: true) { location.latitude }
|
|
171
|
+
define_attr(:longitude, memoize: false, export: true) { location.longitude }
|
|
172
|
+
define_attr(:metro_code, memoize: false) { location.metro_code }
|
|
173
|
+
define_attr(:time_zone, memoize: false, export: true) { location.time_zone }
|
|
174
|
+
define_attr(:postal_code, aliases: :zip, export: true, memoize: false) { postal.code }
|
|
175
|
+
|
|
176
|
+
define_attr(:detailed_parts, memoize: false) { [continent.code, country.name, city.name].compact }
|
|
177
|
+
define_attr(:detailed, memoize: false) { detailed_parts.join(" / ") }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class GeoCountry < Geo; end
|
|
181
|
+
class GeoCity < Geo; end
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------
|
|
184
|
+
|
|
185
|
+
def initialize(data = {})
|
|
186
|
+
super()
|
|
187
|
+
merge!(data) if data
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def inspect
|
|
191
|
+
"#<#{self.class}:#{format("0x%x", object_id << 1)}: #{super}>"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
alias_method :blank?, :empty?
|
|
195
|
+
|
|
196
|
+
def present?
|
|
197
|
+
!empty?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def presence
|
|
201
|
+
self if present?
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|