macker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.travis.yml +26 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/Rakefile +16 -0
- data/data/oui_1499079803.txt +141330 -0
- data/lib/macker.rb +394 -0
- data/lib/macker/address.rb +137 -0
- data/lib/macker/config.rb +32 -0
- data/lib/macker/version.rb +7 -0
- data/macker.gemspec +30 -0
- data/test/maker_test.rb +7 -0
- metadata +114 -0
data/lib/macker.rb
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'macker/version'
|
6
|
+
require 'macker/config'
|
7
|
+
require 'macker/address'
|
8
|
+
|
9
|
+
# Macker namespace
|
10
|
+
module Macker
|
11
|
+
# Invalid cache, file not found or cache empty
|
12
|
+
class InvalidCache < StandardError; end
|
13
|
+
# Invalid raw data, file not found or cache empty
|
14
|
+
class InvalidRawData < StandardError; end
|
15
|
+
# Invalid options, invalid options given to method
|
16
|
+
class InvalidOptions < StandardError; end
|
17
|
+
# Invalid OUI vendor, vendor not found in table
|
18
|
+
class NotFoundOuiVendor < StandardError; end
|
19
|
+
|
20
|
+
# Extend instance methods to class methods
|
21
|
+
extend self
|
22
|
+
|
23
|
+
# Proc timestamp accessor to set external timestamp
|
24
|
+
# @param value [Time] proc timestamp
|
25
|
+
# @return [Time] proc timestamp
|
26
|
+
attr_accessor :proc_timestamp
|
27
|
+
|
28
|
+
# Get the timestamp of vendor list in memory
|
29
|
+
# @return [Time] time object or nil
|
30
|
+
attr_reader :mem_timestamp
|
31
|
+
|
32
|
+
# Get or initialize Macker config.
|
33
|
+
# @return [OpenStruct] Macker configuration
|
34
|
+
def config
|
35
|
+
@config ||= Macker::Config.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set configuration of Macker in a block.
|
39
|
+
# @return [OpenStruct] Macker configuration
|
40
|
+
def configure
|
41
|
+
yield config if block_given?
|
42
|
+
end
|
43
|
+
|
44
|
+
# Update all OUI tables
|
45
|
+
# @param straight [Boolean] true for straight, default is careful
|
46
|
+
# @return [Time] timestamp of the update
|
47
|
+
def update(straight = false)
|
48
|
+
@prefix_table = {}
|
49
|
+
@iso_code_table = {}
|
50
|
+
@vendor_table = {}
|
51
|
+
vendor_list(straight)
|
52
|
+
@mem_timestamp = if config.cache.is_a?(Proc)
|
53
|
+
proc_timestamp
|
54
|
+
else
|
55
|
+
file_timestamp
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Lookup for a vendor with given MAC address.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# lookup('00:04:A9:6D:B8:AC')
|
63
|
+
# lookup(20022409388)
|
64
|
+
#
|
65
|
+
# @param mac [Address,Integer,String] MAC address
|
66
|
+
# @param opts [Hash] options for the method
|
67
|
+
# @return [Address] MAC address with vendor data
|
68
|
+
def lookup(mac, opts = {})
|
69
|
+
expire! if config.auto_expiration
|
70
|
+
data = prefix_table[Address.new(mac).prefix]
|
71
|
+
if data.nil?
|
72
|
+
opts[:raising] ? raise(NotFoundOuiVendor, "OUI not found for MAC: #{mac}") : (return nil)
|
73
|
+
end
|
74
|
+
Address.new(mac, data)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Lookup for a vendor with given MAC address.
|
78
|
+
# Raises an error if no vendor found.
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# lookup!('00:04:A9:6D:B8:AC')
|
82
|
+
# lookup!('80:47:FB:B2:9E:D6')
|
83
|
+
#
|
84
|
+
# @param mac [Address,Integer,String] MAC address
|
85
|
+
# @return [Address] MAC address with vendor data
|
86
|
+
def lookup!(mac)
|
87
|
+
lookup(mac, raising: true)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Generate a MAC address.
|
91
|
+
# - No options for random MAC.
|
92
|
+
# - Vendor option to get a valid OUI MAC.
|
93
|
+
# - Vendor Name option to get a random MAC from vendor.
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# generate
|
97
|
+
# generate(vendor: true)
|
98
|
+
# generate(vendor: 'IEEE Registration Authority')
|
99
|
+
#
|
100
|
+
# @param opts [Hash] options for the method
|
101
|
+
# @return [Address] MAC address with data
|
102
|
+
def generate(opts = {})
|
103
|
+
expire! if config.auto_expiration
|
104
|
+
return generate_by_iso_code(opts.delete(:iso_code), opts) if opts[:iso_code] && opts[:iso_code].length == 2
|
105
|
+
vendor = opts.delete(:vendor)
|
106
|
+
case vendor
|
107
|
+
when nil, false
|
108
|
+
Address.new(rand(2**48))
|
109
|
+
when true
|
110
|
+
generate_by_vendor(prefix_table[prefix_table.keys.shuffle.sample][:name], opts)
|
111
|
+
when String
|
112
|
+
generate_by_vendor(vendor, opts)
|
113
|
+
else
|
114
|
+
raise(InvalidOptions, "Incompatible option vendor for generate: #{vendor.class}")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Generate a MAC address.
|
119
|
+
# - No options for random MAC address.
|
120
|
+
# - Vendor option to get a valid OUI MAC address.
|
121
|
+
# - Vendor name option to get a random MAC address from vendor.
|
122
|
+
# Raises an error if an error occurs.
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# generate
|
126
|
+
# generate(vendor: true)
|
127
|
+
# generate(vendor: 'No vendor')
|
128
|
+
#
|
129
|
+
# @param opts [Hash] options for the method
|
130
|
+
# @return [Address] MAC address with vendor data
|
131
|
+
def generate!(opts = {})
|
132
|
+
generate(opts.merge(raising: true))
|
133
|
+
end
|
134
|
+
|
135
|
+
# Vendor table with all base16 MAC prefixes as keys
|
136
|
+
# @return [Hash] vendor prefixes table
|
137
|
+
def prefix_table
|
138
|
+
update unless @prefix_table
|
139
|
+
@prefix_table
|
140
|
+
end
|
141
|
+
|
142
|
+
# Vendor table with all country iso codes as keys
|
143
|
+
# @return [Hash] vendor iso codes table
|
144
|
+
def iso_code_table
|
145
|
+
update unless @iso_code_table
|
146
|
+
@iso_code_table
|
147
|
+
end
|
148
|
+
|
149
|
+
# Vendor table with all country vendor names as keys
|
150
|
+
# @return [Hash] vendor names table
|
151
|
+
def vendor_table
|
152
|
+
update unless @vendor_table
|
153
|
+
@vendor_table
|
154
|
+
end
|
155
|
+
|
156
|
+
# Fetch new vendor list if cached list is expired or stale
|
157
|
+
# @return [Boolean] true if vendor list is expired and updated from remote
|
158
|
+
def expire!
|
159
|
+
if expired?
|
160
|
+
update(true)
|
161
|
+
true
|
162
|
+
elsif stale?
|
163
|
+
update
|
164
|
+
true
|
165
|
+
else
|
166
|
+
false
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Check if vendor list is expired
|
171
|
+
# @return [Boolean] true if vendor list is expired
|
172
|
+
def expired?
|
173
|
+
Time.now > vendors_expiration
|
174
|
+
end
|
175
|
+
|
176
|
+
# Check if vendor list is stale
|
177
|
+
# Stale is true if vendor list is updated straight by another thread.
|
178
|
+
# The actual thread has always old vendor list in memory store.
|
179
|
+
# @return [Boolean] true if vendor list is stale
|
180
|
+
def stale?
|
181
|
+
if config.cache.is_a?(Proc)
|
182
|
+
proc_timestamp != mem_timestamp
|
183
|
+
else
|
184
|
+
file_timestamp != mem_timestamp
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Get vendor list expiration time based on ttl
|
189
|
+
# @return [Time] vendor list expiration time
|
190
|
+
def vendors_expiration
|
191
|
+
if config.cache.is_a?(Proc)
|
192
|
+
proc_timestamp + config.ttl_in_seconds
|
193
|
+
else
|
194
|
+
file_timestamp + config.ttl_in_seconds
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
protected
|
199
|
+
|
200
|
+
# Generate a MAC address by vendor.
|
201
|
+
#
|
202
|
+
# @param vendor [String] name of vendor
|
203
|
+
# @param opts [Hash] options for the method
|
204
|
+
# @return [Address] MAC address with vendor data
|
205
|
+
def generate_by_vendor(vendor, opts = {})
|
206
|
+
ouis = vendor_table[vendor]
|
207
|
+
if ouis.nil? || ouis.empty?
|
208
|
+
opts[:raising] ? raise(NotFoundOuiVendor, "OUI not found for vendor: #{vendor}") : (return nil)
|
209
|
+
end
|
210
|
+
oui = ouis[rand(ouis.size)]
|
211
|
+
m1 = Address.new(oui[:prefix]).to_i
|
212
|
+
m2 = rand(2**24)
|
213
|
+
mac = m1 + m2
|
214
|
+
Address.new(mac,
|
215
|
+
name: vendor,
|
216
|
+
address: oui[:address],
|
217
|
+
iso_code: oui[:iso_code])
|
218
|
+
end
|
219
|
+
|
220
|
+
# Generate a MAC address by iso code.
|
221
|
+
#
|
222
|
+
# @param iso_code [String] iso code
|
223
|
+
# @param opts [Hash] options for the method
|
224
|
+
# @return [Address] MAC address with vendor data
|
225
|
+
def generate_by_iso_code(iso_code, opts = {})
|
226
|
+
ouis = iso_code_table[iso_code]
|
227
|
+
if ouis.nil? || ouis.empty?
|
228
|
+
opts[:raising] ? raise(NotFoundOuiVendor, "OUI not found for iso code #{iso_code}") : (return nil)
|
229
|
+
end
|
230
|
+
oui = ouis[rand(ouis.size)]
|
231
|
+
m1 = Address.new(oui[:prefix]).to_i
|
232
|
+
m2 = rand(2**24)
|
233
|
+
mac = m1 + m2
|
234
|
+
Address.new(mac,
|
235
|
+
name: oui[:name],
|
236
|
+
address: oui[:address],
|
237
|
+
iso_code: iso_code)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Get vendor list with different strategies.
|
241
|
+
# Parse and read in the content.
|
242
|
+
#
|
243
|
+
# @example
|
244
|
+
# vendor_list(true)
|
245
|
+
# vendor_list
|
246
|
+
#
|
247
|
+
# @param straight [Boolean] true for straight, default is careful
|
248
|
+
# @return [Hash] vendor list with all base16 MAC prefixes as keys
|
249
|
+
def vendor_list(straight = false)
|
250
|
+
raw_vendor_list = if straight
|
251
|
+
raw_vendor_list_straight
|
252
|
+
else
|
253
|
+
raw_vendor_list_careful
|
254
|
+
end
|
255
|
+
vendor_list = raw_vendor_list.gsub(/\r\n/, "\n").gsub(/\t+/, "\t").split(/\n\n/)
|
256
|
+
vendor_list[1..-1].each do |vendor|
|
257
|
+
base16_fields = vendor.strip.split("\n")[1].split("\t")
|
258
|
+
mac_prefix = Address.new(base16_fields[0].strip[0..5]).prefix
|
259
|
+
address = vendor.strip.delete("\t").split("\n")
|
260
|
+
next unless @prefix_table[mac_prefix].nil?
|
261
|
+
@prefix_table[mac_prefix] = { name: base16_fields[-1]
|
262
|
+
.strip
|
263
|
+
.gsub(/\s+/, ' ')
|
264
|
+
.split(/ |\_|\-/)
|
265
|
+
.map(&:capitalize)
|
266
|
+
.join(' '),
|
267
|
+
address: address[2..-1].map { |a| a
|
268
|
+
.strip
|
269
|
+
.gsub(/\s+/, ' ')
|
270
|
+
.gsub(/,(?![\s])/, ', ')
|
271
|
+
.gsub(/\,+$/, '')
|
272
|
+
.split(/ |\_|\-/)
|
273
|
+
.map(&:capitalize)
|
274
|
+
.join(' ')
|
275
|
+
},
|
276
|
+
iso_code: address[-1].strip.upcase
|
277
|
+
}
|
278
|
+
end
|
279
|
+
@iso_code_table = @prefix_table.each_with_object({}) { |(key, value), out| (out[value[:iso_code]] ||= []) << value.merge(prefix: key) }
|
280
|
+
@vendor_table = @prefix_table.each_with_object({}) { |(key, value), out| (out[value[:name]] ||= []) << value.merge(prefix: key) }
|
281
|
+
@prefix_table
|
282
|
+
end
|
283
|
+
|
284
|
+
# Get raw vendor list from cache and then from url
|
285
|
+
# @param rescue_straight [Boolean] true for rescue straight, default true
|
286
|
+
# @return [String] text content
|
287
|
+
def raw_vendor_list_careful(rescue_straight = true)
|
288
|
+
res = read_from_cache
|
289
|
+
raise if res.to_s.empty?
|
290
|
+
res
|
291
|
+
rescue
|
292
|
+
rescue_straight ? raw_vendor_list_straight : ''
|
293
|
+
end
|
294
|
+
|
295
|
+
# Get raw vendor list from url
|
296
|
+
# @return [String] text content
|
297
|
+
def raw_vendor_list_straight
|
298
|
+
res = read_from_url
|
299
|
+
raise if res.to_s.empty?
|
300
|
+
res
|
301
|
+
rescue
|
302
|
+
raw_vendor_list_careful(false)
|
303
|
+
end
|
304
|
+
|
305
|
+
# Store the provided text data by calling the proc method provided
|
306
|
+
# for the cache, or write to the cache file.
|
307
|
+
#
|
308
|
+
# @example
|
309
|
+
# store_in_cache("E0-43-DB (hex) Shenzhen ViewAt Technology Co.,Ltd.
|
310
|
+
# E043DB (base 16) Shenzhen ViewAt Technology Co.,Ltd.
|
311
|
+
# 9A,Microprofit,6th Gaoxin South...
|
312
|
+
# shenzhen guangdong 518057
|
313
|
+
# CN
|
314
|
+
# ")
|
315
|
+
#
|
316
|
+
# @param text [String] text content
|
317
|
+
# @return [Integer] normally 0
|
318
|
+
def store_in_cache(text)
|
319
|
+
if config.cache.is_a?(Proc)
|
320
|
+
config.cache.call(text)
|
321
|
+
elsif config.cache.is_a?(String) || config.cache.is_a?(Pathname)
|
322
|
+
write_to_file(text)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Writes content to file cache
|
327
|
+
# @param text [String] text content
|
328
|
+
# @return [Integer] normally 0
|
329
|
+
def write_to_file(text)
|
330
|
+
open(file_path, 'w') do |f|
|
331
|
+
f.write(text)
|
332
|
+
end
|
333
|
+
file_update
|
334
|
+
rescue Errno::ENOENT
|
335
|
+
raise InvalidCache
|
336
|
+
end
|
337
|
+
|
338
|
+
# Read from cache when exist
|
339
|
+
# @return [Proc,String] text content
|
340
|
+
def read_from_cache
|
341
|
+
if config.cache.is_a?(Proc)
|
342
|
+
config.cache.call(nil)
|
343
|
+
elsif (config.cache.is_a?(String) || config.cache.is_a?(Pathname)) &&
|
344
|
+
File.exist?(file_path)
|
345
|
+
open(file_path).read
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Get remote content and store in cache
|
350
|
+
# @return [String] text content
|
351
|
+
def read_from_url
|
352
|
+
text = open_url
|
353
|
+
store_in_cache(text) if text && config.cache
|
354
|
+
text
|
355
|
+
end
|
356
|
+
|
357
|
+
# Opens an URL and reads the content
|
358
|
+
# @return [String] text content
|
359
|
+
def open_url
|
360
|
+
opts = [config.oui_full_url]
|
361
|
+
opts << { 'User-Agent' => config.user_agent } if config.user_agent
|
362
|
+
open(*opts).read
|
363
|
+
rescue OpenURI::HTTPError
|
364
|
+
''
|
365
|
+
end
|
366
|
+
|
367
|
+
# Get file path with timestamp
|
368
|
+
# @return [String] file path
|
369
|
+
def file_path
|
370
|
+
Dir.glob(config.cache).first || File.join(File.dirname(config.cache),
|
371
|
+
File.basename(config.cache).gsub(/_.+\.txt/, '_0.txt'))
|
372
|
+
end
|
373
|
+
|
374
|
+
# Get file name with timestamp
|
375
|
+
# @return [String] file name
|
376
|
+
def file_name
|
377
|
+
File.basename(file_path)
|
378
|
+
end
|
379
|
+
|
380
|
+
# Get file timestamp
|
381
|
+
# @return [Time] file timestamp
|
382
|
+
def file_timestamp
|
383
|
+
timestamp = file_name.scan(/\d+/).first
|
384
|
+
timestamp ? Time.at(timestamp.to_i) : Time.at(0)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Update file name timestamp
|
388
|
+
# @return [Integer] normally 0
|
389
|
+
def file_update
|
390
|
+
File.rename(file_path,
|
391
|
+
File.join(File.dirname(file_path),
|
392
|
+
File.basename(file_path).gsub(/_\d+\.txt/, "_#{Time.now.to_i}.txt")))
|
393
|
+
end
|
394
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Macker namespace
|
4
|
+
module Macker
|
5
|
+
# Invalid address, mac address format not valid
|
6
|
+
class InvalidAddress < StandardError; end
|
7
|
+
# MAC address class
|
8
|
+
class Address
|
9
|
+
include Comparable
|
10
|
+
|
11
|
+
# Get the value of name, address or iso code
|
12
|
+
# @return [String] content of the value
|
13
|
+
attr_reader :name, :address, :iso_code
|
14
|
+
|
15
|
+
# Initialize Address object
|
16
|
+
# @param mac [Address,Integer,String] a MAC address
|
17
|
+
# @param opts [Hash] options for the method
|
18
|
+
# @return [Address] the initialized object
|
19
|
+
def initialize(mac, opts = {})
|
20
|
+
case mac
|
21
|
+
when Address
|
22
|
+
@val = mac.to_i
|
23
|
+
@name = mac.name
|
24
|
+
@address = mac.address
|
25
|
+
@iso_code = mac.iso_code
|
26
|
+
when Integer
|
27
|
+
@val = mac
|
28
|
+
when String
|
29
|
+
@val = cleanup(mac).to_i(16)
|
30
|
+
else
|
31
|
+
raise(InvalidAddress, "Incompatible type for address initialization: #{mac.class}")
|
32
|
+
end
|
33
|
+
raise(InvalidAddress, "Invalid MAC address: #{self.to_s}") unless valid?
|
34
|
+
@name ||= opts.fetch(:name, nil)
|
35
|
+
@address ||= opts.fetch(:address, nil)
|
36
|
+
@iso_code ||= opts.fetch(:iso_code, nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Format MAC address to integer
|
40
|
+
# @return [Integer] integer MAC address
|
41
|
+
def to_i
|
42
|
+
@val
|
43
|
+
end
|
44
|
+
|
45
|
+
# Format MAC address to string
|
46
|
+
# @param sep [String] separator, default is ':'
|
47
|
+
# @return [String] formatted MAC address
|
48
|
+
def to_s(sep = ':')
|
49
|
+
@val.to_s(16)
|
50
|
+
.rjust(12, '0')
|
51
|
+
.insert(10, sep)
|
52
|
+
.insert(8, sep)
|
53
|
+
.insert(6, sep)
|
54
|
+
.insert(4, sep)
|
55
|
+
.insert(2, sep)
|
56
|
+
.upcase
|
57
|
+
end
|
58
|
+
|
59
|
+
# Compare two MAC addresses
|
60
|
+
# @param other [Address] MAC address object
|
61
|
+
# @return [Boolean] true if the same, else false
|
62
|
+
def <=>(other)
|
63
|
+
@val <=> other.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if MAC address is an OUI valid address
|
67
|
+
# @return [Boolean] true if valid, else false
|
68
|
+
def oui?
|
69
|
+
!@name.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if MAC address is a valid address
|
73
|
+
# @return [Boolean] true if valid, else false
|
74
|
+
def valid?
|
75
|
+
@val.between?(0, 2**48 - 1)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Check if MAC address is a broadcast address
|
79
|
+
# @return [Boolean] true if broadcast, else false
|
80
|
+
def broadcast?
|
81
|
+
@val == 2**48 - 1
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check if MAC address is an unicast address
|
85
|
+
# @return [Boolean] true if unicast, else false
|
86
|
+
def unicast?
|
87
|
+
!multicast?
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if MAC address is a multicast address
|
91
|
+
# @return [Boolean] true if multicast, else false
|
92
|
+
def multicast?
|
93
|
+
mask = 1 << (5 * 8)
|
94
|
+
(mask & @val) != 0
|
95
|
+
end
|
96
|
+
|
97
|
+
# Check if MAC address is a global uniq address
|
98
|
+
# @return [Boolean] true if uniq, else false
|
99
|
+
def global_uniq?
|
100
|
+
!local_admin?
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check if MAC address is a local address
|
104
|
+
# @return [Boolean] true if local, else false
|
105
|
+
def local_admin?
|
106
|
+
mask = 2 << (5 * 8)
|
107
|
+
(mask & @val) != 0
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get next MAC address from actual address
|
111
|
+
# @return [Adress] next MAC address
|
112
|
+
def next
|
113
|
+
Address.new((@val + 1) % 2**48)
|
114
|
+
end
|
115
|
+
alias succ next
|
116
|
+
|
117
|
+
# Get the prefix base16 MAC address
|
118
|
+
# @return [Adress] MAC prefix
|
119
|
+
def prefix
|
120
|
+
to_s('')[0..5]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get the full vendor address
|
124
|
+
# @return [String] full vendor address string
|
125
|
+
def full_address
|
126
|
+
address.join(', ')
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Clean up a MAC string from special characters
|
132
|
+
# @return [String] cleaned MAC address
|
133
|
+
def cleanup(mac)
|
134
|
+
mac.strip.upcase.gsub(/^0[xX]/, '').gsub(/[^0-9A-F]/, '').ljust(12, '0')
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|