macker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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