Class: WifiWand::MacOsModel
- Defined in:
- lib/wifi-wand/models/mac_os_model.rb
Constant Summary collapse
- AIRPORT_CMD =
'/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
Instance Attribute Summary
Attributes inherited from BaseModel
Instance Method Summary collapse
-
#airport_command ⇒ Object
Although at this time the airport command utility is predictable, allow putting it elsewhere in the path for overriding and easier fix if that location should change.
-
#available_network_info ⇒ Object
Returns data pertaining to available wireless networks.
-
#available_network_names ⇒ Object
The Mac OS airport utility (at /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport) outputs the network names right padded with spaces so there is no way to differentiate a network name with leading space(s) from one without:.
-
#connected_network_name ⇒ Object
Returns the network currently connected to, or nil if none.
-
#detect_wifi_port ⇒ Object
Identifies the (first) wireless network hardware port in the system, e.g.
-
#disconnect ⇒ Object
Disconnects from the currently connected network.
-
#initialize(options = OpenStruct.new) ⇒ MacOsModel
constructor
Takes an OpenStruct containing options such as verbose mode and port name.
-
#ip_address ⇒ Object
Returns the IP address assigned to the wifi port, or nil if none.
-
#is_wifi_port?(port) ⇒ Boolean
Returns whether or not the specified interface is a WiFi interfae.
-
#mac_address ⇒ Object
TODO: Add capability to change the MAC address using a command in the form of: sudo ifconfig en0 ether aa:bb:cc:dd:ee:ff However, the MAC address will be set to the real hardware address on restart.
- #nameservers_using_networksetup ⇒ Object
-
#nameservers_using_resolv_conf ⇒ Object
Though this is strictly not OS-agnostic, it will be used by most OS’s, and can be overridden by subclasses (e.g. Windows).
- #nameservers_using_scutil ⇒ Object
- #open_application(application_name) ⇒ Object
- #open_resource(resource_url) ⇒ Object
-
#os_level_connect(network_name, password = nil) ⇒ Object
This method is called by BaseModel#connect to do the OS-specific connection logic.
-
#os_level_preferred_network_password(preferred_network_name) ⇒ Object
@return: If the network is in the preferred networks list If a password is associated w/this network, return the password If not, return nil else raise an error.
-
#preferred_networks ⇒ Object
Returns data pertaining to “preferred” networks, many/most of which will probably not be available.
- #remove_preferred_network(network_name) ⇒ Object
- #set_nameservers(nameservers) ⇒ Object
-
#wifi_info ⇒ Object
Returns some useful wifi-related information.
-
#wifi_off ⇒ Object
Turns wifi off.
-
#wifi_on ⇒ Object
Turns wifi on.
-
#wifi_on? ⇒ Boolean
Returns true if wifi is on, else false.
Methods inherited from BaseModel
#connect, #connected_to?, #connected_to_internet?, #cycle_network, #preferred_network_password, #public_ip_address_info, #random_mac_address, #remove_preferred_networks, #run_os_command, #till, #try_os_command_until
Constructor Details
#initialize(options = OpenStruct.new) ⇒ MacOsModel
Takes an OpenStruct containing options such as verbose mode and port name.
16 17 18 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 16 def initialize( = OpenStruct.new) super end |
Instance Method Details
#airport_command ⇒ Object
Although at this time the airport command utility is predictable, allow putting it elsewhere in the path for overriding and easier fix if that location should change.
24 25 26 27 28 29 30 31 32 33 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 24 def airport_command airport_in_path = `which airport`.chomp if ! airport_in_path.empty? airport_in_path elsif File.exist?(AIRPORT_CMD) AIRPORT_CMD else raise Error.new("Airport command not found.") end end |
#available_network_info ⇒ Object
Returns data pertaining to available wireless networks. For some reason, this often returns no results, so I’ve put the operation in a loop. I was unable to detect a sort strategy in the airport utility’s output, so I sort the lines alphabetically, to show duplicates and for easier lookup.
Sample Output:
> [“SSID BSSID RSSI CHANNEL HT CC SECURITY (auth/unicast/group)”,
"ByCO-U00tRzUzMEg 64:6c:b2:db:f3:0c -56 6 Y -- NONE",
"Chancery 0a:18:d6:0b:b9:c3 -82 11 Y -- NONE",
"Chancery 2a:a4:3c:03:33:99 -59 60,+1 Y -- NONE",
"DIRECT-sq-BRAVIA 02:71:cc:87:4a:8c -76 6 Y -- WPA2(PSK/AES/AES) ", #
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 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 74 def available_network_info return nil unless wifi_on? # no need to try command = "#{airport_command} -s | iconv -f macroman -t utf-8" max_attempts = 50 reformat_line = ->(line) do ssid = line[0..31].strip "%-32.32s%s" % [ssid, line[32..-1]] end signal_strength = ->(line) { (line[50..54] || '').to_i } sort_in_place_by_signal_strength = ->(lines) do lines.sort! { |x,y| signal_strength.(y) <=> signal_strength.(x) } end process_tabular_data = ->(output) do lines = output.split("\n") header_line = lines[0] data_lines = lines[1..-1] data_lines.map! do |line| # Reformat the line so that the name is left instead of right justified reformat_line.(line) end sort_in_place_by_signal_strength.(data_lines) [reformat_line.(header_line)] + data_lines end output = try_os_command_until(command, ->(output) do ! ([nil, ''].include?(output)) end) if output process_tabular_data.(output) else raise Error.new("Unable to get available network information after #{max_attempts} attempts.") end end |
#available_network_names ⇒ Object
The Mac OS airport utility (at /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport) outputs the network names right padded with spaces so there is no way to differentiate a network name with leading space(s) from one without:
SSID BSSID RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
ngHub_319442NL0293C 04:a1:51:58:5b:05 -65 11 Y US WPA2(PSK/AES/AES)
NETGEAR89_2GEXT 9c:3d:cf:11:69:b4 -67 8 Y US NONE
To remedy this, they offer a “-x” option that outputs the information in (pseudo) XML. This XML has ‘dict’ elements that contain many elements. The SSID can be found in the XML element <string> which immediately follows an XML element whose text is “SSID_STR”. Unfortunately, since there is no way to connect the two other than their physical location, the key is rather useless for XML parsing.
I tried extracting the arrays of keys and strings, and finding the string element at the same position in the string array as the ‘SSID_STR’ was in the keys array. However, not all keys had string elements, so the index in the key array was the wrong index. Here is an excerpt from the XML output:
<key>RSSI</key> <integer>-91</integer> <key>SSID</key> <data> TkVUR0VBUjY1 </data> <key>SSID_STR</key> <string>NETGEAR65</string>
The kludge I came up with was that the ssid was always the 2nd value in the <string> element array, so that’s what is used here.
But now even that approach has been superseded by the XPath approach now used.
REXML is used here to avoid the need for the user to install Nokogiri.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 149 def available_network_names return nil unless wifi_on? # no need to try # For some reason, the airport command very often returns nothing, so we need to try until # we get data in the response: command = "#{airport_command} -s -x | iconv -f macroman -t utf-8" stop_condition = ->(response) { ! [nil, ''].include?(response) } output = try_os_command_until(command, stop_condition) doc = REXML::Document.new(output) xpath = '//key[text() = "SSID_STR"][1]/following-sibling::*[1]' # provided by @ScreenStaring on Twitter REXML::XPath.match(doc, xpath) \ .map(&:text) \ .sort { |x,y| x.casecmp(y) } \ .uniq end |
#connected_network_name ⇒ Object
Returns the network currently connected to, or nil if none.
266 267 268 269 270 271 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 266 def connected_network_name return nil unless wifi_on? # no need to try lines = run_os_command("#{airport_command} -I").split("\n") ssid_lines = lines.grep(/ SSID:/) ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.lstrip end |
#detect_wifi_port ⇒ Object
Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1 This may not detect wifi ports with nonstandard names, such as USB wifi devices.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 38 def detect_wifi_port lines = run_os_command("networksetup -listallhardwareports").split("\n") # Produces something like this: # Hardware Port: Wi-Fi # Device: en0 # Ethernet Address: ac:bc:32:b9:a9:9d # # Hardware Port: Bluetooth PAN # Device: en3 # Ethernet Address: ac:bc:32:b9:a9:9e wifi_port_line_num = (0...lines.size).detect do |index| /: Wi-Fi$/.match(lines[index]) end if wifi_port_line_num.nil? raise Error.new(%Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports}) else lines[wifi_port_line_num + 1].split(': ').last end end |
#disconnect ⇒ Object
Disconnects from the currently connected network. Does not turn off wifi.
275 276 277 278 279 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 275 def disconnect return nil unless wifi_on? # no need to try run_os_command("sudo #{airport_command} -z") nil end |
#ip_address ⇒ Object
Returns the IP address assigned to the wifi port, or nil if none.
244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 244 def ip_address return nil unless wifi_on? # no need to try begin run_os_command("ipconfig getifaddr #{wifi_port}").chomp rescue OsCommandError => error if error.exitstatus == 1 nil else raise end end end |
#is_wifi_port?(port) ⇒ Boolean
Returns whether or not the specified interface is a WiFi interfae.
183 184 185 186 187 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 183 def is_wifi_port?(port) run_os_command("networksetup -listpreferredwirelessnetworks #{port} 2>/dev/null") exit_status = $?.exitstatus exit_status != 10 end |
#mac_address ⇒ Object
TODO: Add capability to change the MAC address using a command in the form of:
sudo ifconfig en0 ether aa:bb:cc:dd:ee:ff
However, the MAC address will be set to the real hardware address on restart. One way to implement this is to have an optional address argument, then this method returns the current address if none is provided, but sets to the specified address if it is.
288 289 290 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 288 def mac_address run_os_command("ifconfig #{wifi_port} | awk '/ether/{print $2}'").chomp end |
#nameservers_using_networksetup ⇒ Object
401 402 403 404 405 406 407 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 401 def nameservers_using_networksetup output = run_os_command("networksetup -getdnsservers Wi-Fi") if output == "There aren't any DNS Servers set on Wi-Fi.\n" output = '' end output.split("\n") end |
#nameservers_using_resolv_conf ⇒ Object
Though this is strictly not OS-agnostic, it will be used by most OS’s, and can be overridden by subclasses (e.g. Windows).
383 384 385 386 387 388 389 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 383 def nameservers_using_resolv_conf begin File.readlines('/etc/resolv.conf').grep(/^nameserver /).map { |line| line.split.last } rescue Errno::ENOENT nil end end |
#nameservers_using_scutil ⇒ Object
392 393 394 395 396 397 398 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 392 def nameservers_using_scutil output = run_os_command('scutil --dns') nameserver_lines_scoped_and_unscoped = output.split("\n").grep(/^\s*nameserver\[/) unique_nameserver_lines = nameserver_lines_scoped_and_unscoped.uniq # take the union nameservers = unique_nameserver_lines.map { |line| line.split(' : ').last.strip } nameservers end |
#open_application(application_name) ⇒ Object
354 355 356 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 354 def open_application(application_name) run_os_command('open -a ' + application_name) end |
#open_resource(resource_url) ⇒ Object
359 360 361 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 359 def open_resource(resource_url) run_os_command('open ' + resource_url) end |
#os_level_connect(network_name, password = nil) ⇒ Object
This method is called by BaseModel#connect to do the OS-specific connection logic.
214 215 216 217 218 219 220 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 214 def os_level_connect(network_name, password = nil) command = "networksetup -setairportnetwork #{wifi_port} " + "#{Shellwords.shellescape(network_name)}" if password command << ' ' << Shellwords.shellescape(password) end run_os_command(command) end |
#os_level_preferred_network_password(preferred_network_name) ⇒ Object
@return:
If the network is in the preferred networks list
If a password is associated w/this network, return the password
If not, return nil
else
raise an error
229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 229 def os_level_preferred_network_password(preferred_network_name) command = %Q{security find-generic-password -D "AirPort network password" -a "#{preferred_network_name}" -w 2>&1} begin return run_os_command(command).chomp rescue OsCommandError => error if error.exitstatus == 44 # network has no password stored nil else raise end end end |
#preferred_networks ⇒ Object
Returns data pertaining to “preferred” networks, many/most of which will probably not be available.
168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 168 def preferred_networks lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_port}").split("\n") # Produces something like this, unsorted, and with leading tabs: # Preferred networks on en0: # LibraryWiFi # @thePAD/Magma lines.delete_at(0) # remove title line lines.map! { |line| line.gsub("\t", '') } # remove leading tabs lines.sort! { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively lines end |
#remove_preferred_network(network_name) ⇒ Object
258 259 260 261 262 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 258 def remove_preferred_network(network_name) network_name = network_name.to_s run_os_command("sudo networksetup -removepreferredwirelessnetwork " + "#{wifi_port} #{Shellwords.shellescape(network_name)}") end |
#set_nameservers(nameservers) ⇒ Object
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 329 def set_nameservers(nameservers) arg = if nameservers == :clear 'empty' else bad_addresses = nameservers.reject do |ns| begin IPAddr.new(ns).ipv4? true rescue => e puts e false end end unless bad_addresses.empty? raise Error.new("Bad IP addresses provided: #{bad_addresses.join(', ')}") end nameservers.join(' ') end # end assignment to arg variable run_os_command("networksetup -setdnsservers Wi-Fi #{arg}") nameservers end |
#wifi_info ⇒ Object
Returns some useful wifi-related information.
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 294 def wifi_info connected = begin connected_to_internet? rescue false end info = { 'wifi_on' => wifi_on?, 'internet_on' => connected, 'port' => wifi_port, 'network' => connected_network_name, 'ip_address' => ip_address, 'mac_address' => mac_address, 'nameservers' => nameservers_using_scutil, 'timestamp' => Time.now, } more_output = run_os_command(airport_command + " -I") more_info = colon_output_to_hash(more_output) info.merge!(more_info) info.delete('AirPort') # will be here if off, but info is already in wifi_on key if info['internet_on'] begin info['public_ip'] = public_ip_address_info rescue => e puts "Error obtaining public IP address info, proceeding with everything else:" puts e.to_s end end info end |
#wifi_off ⇒ Object
Turns wifi off.
206 207 208 209 210 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 206 def wifi_off return unless wifi_on? run_os_command("networksetup -setairportpower #{wifi_port} off") wifi_on? ? Error.new(raise("Wifi could not be disabled.")) : nil end |
#wifi_on ⇒ Object
Turns wifi on.
198 199 200 201 202 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 198 def wifi_on return if wifi_on? run_os_command("networksetup -setairportpower #{wifi_port} on") wifi_on? ? nil : Error.new(raise("Wifi could not be enabled.")) end |
#wifi_on? ⇒ Boolean
Returns true if wifi is on, else false.
191 192 193 194 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 191 def wifi_on? lines = run_os_command("#{airport_command} -I").split("\n") lines.grep("AirPort: Off").none? end |