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_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 |
# 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" max_attempts = 50 reformat_line = ->(line) do ssid = line[0..31].strip "%-32.32s%s" % [ssid, line[32..-1]] 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 # TODO: Need to sort case insensitively?: data_lines.sort! [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.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 147 def available_network_names # 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" 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.
261 262 263 264 265 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 261 def connected_network_name 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.
269 270 271 272 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 269 def disconnect 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.
240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 240 def ip_address 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.
179 180 181 182 183 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 179 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.
281 282 283 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 281 def mac_address run_os_command("ifconfig #{wifi_port} | awk '/ether/{print $2}'") end |
#nameservers_using_networksetup ⇒ Object
388 389 390 391 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 388 def nameservers_using_networksetup output = run_os_command("networksetup -getdnsservers Wi-Fi") 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).
370 371 372 373 374 375 376 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 370 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
379 380 381 382 383 384 385 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 379 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_resource(resource_url) ⇒ Object
346 347 348 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 346 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.
210 211 212 213 214 215 216 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 210 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
225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 225 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.
164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 164 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
253 254 255 256 257 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 253 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
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 322 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 run_os_command("networksetup -setdnsservers Wi-Fi #{arg}") nameservers end |
#wifi_info ⇒ Object
Returns some useful wifi-related information.
287 288 289 290 291 292 293 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 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 287 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.
202 203 204 205 206 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 202 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.
194 195 196 197 198 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 194 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.
187 188 189 190 |
# File 'lib/wifi-wand/models/mac_os_model.rb', line 187 def wifi_on? lines = run_os_command("#{airport_command} -I").split("\n") lines.grep("AirPort: Off").none? end |