class WifiWand::MacOsModel
Constants
- AIRPORT_CMD
Public Class Methods
Takes an OpenStruct containing options such as verbose mode and port name.
WifiWand::BaseModel::new
# File lib/wifi-wand/models/mac_os_model.rb, line 16 def initialize(options = OpenStruct.new) super end
Public Instance Methods
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.
# 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
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) ", #
# 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
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.
# 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
Returns the network currently connected to, or nil if none.
# 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
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.
# 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
Disconnects from the currently connected network. Does not turn off wifi.
# 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
Returns the IP address assigned to the wifi port, or nil if none.
# 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
Returns whether or not the specified interface is a WiFi interfae.
# 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
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.
# 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
# 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
@return array of nameserver IP addresses from /etc/resolv.conf, or nil if not found Though this is strictly not OS-agnostic, it will be used by most OS's, and can be overridden by subclasses (e.g. Windows).
# 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
# 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
# File lib/wifi-wand/models/mac_os_model.rb, line 354 def open_application(application_name) run_os_command('open -a ' + application_name) end
# File lib/wifi-wand/models/mac_os_model.rb, line 359 def open_resource(resource_url) run_os_command('open ' + resource_url) end
This method is called by BaseModel#connect
to do the OS-specific connection logic.
# 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
@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
# 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
Returns data pertaining to “preferred” networks, many/most of which will probably not be available.
# 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
# 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
# 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
Returns some useful wifi-related information.
# 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
Turns wifi off.
# 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
Turns wifi on.
# 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
Returns true if wifi is on, else false.
# 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
Private Instance Methods
Parses output like the text below into a hash: SSID: Pattara211 MCS: 5 channel: 7
# File lib/wifi-wand/models/mac_os_model.rb, line 368 def colon_output_to_hash(output) lines = output.split("\n") lines.each_with_object({}) do |line, new_hash| key, value = line.split(': ') key = key.strip value.strip! if value new_hash[key] = value end end