class WifiWand::MacOsModel

Constants

AIRPORT_CMD

Public Class Methods

new(options = OpenStruct.new) click to toggle source

Takes an OpenStruct containing options such as verbose mode and port name.

Calls superclass method WifiWand::BaseModel::new
# File lib/wifi-wand/models/mac_os_model.rb, line 16
def initialize(options = OpenStruct.new)
  super
end

Public Instance Methods

airport_command() click to toggle source

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
available_network_info() click to toggle source

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
available_network_names() click to toggle source

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
connected_network_name() click to toggle source

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
detect_wifi_port() click to toggle source

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
disconnect() click to toggle source

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
ip_address() click to toggle source

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
is_wifi_port?(port) click to toggle source

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
mac_address() click to toggle source

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
nameservers_using_networksetup() click to toggle source
# 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() click to toggle source

@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
nameservers_using_scutil() click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source

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
os_level_preferred_network_password(preferred_network_name) click to toggle source

@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
preferred_networks() click to toggle source

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
remove_preferred_network(network_name) click to toggle source
# 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) click to toggle source
# 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() click to toggle source

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
wifi_off() click to toggle source

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
wifi_on() click to toggle source

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
wifi_on?() click to toggle source

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

colon_output_to_hash(output) click to toggle source

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