mac-wifi 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +67 -59
- data/RELEASE_NOTES.md +8 -0
- data/exe/mac-wifi +1 -1052
- data/lib/mac-wifi/base_model.rb +221 -0
- data/lib/mac-wifi/command_line_interface.rb +417 -0
- data/lib/mac-wifi/mac_os_model.rb +297 -0
- data/lib/mac-wifi/main.rb +69 -0
- data/lib/mac-wifi/version.rb +5 -0
- data/lib/mac-wifi.rb +3 -0
- data/mac-wifi.gemspec +8 -4
- metadata +9 -3
@@ -0,0 +1,297 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
require_relative 'base_model'
|
4
|
+
|
5
|
+
module MacWifi
|
6
|
+
|
7
|
+
class MacOsModel < BaseModel
|
8
|
+
|
9
|
+
AIRPORT_CMD = '/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport'
|
10
|
+
|
11
|
+
def initialize(verbose = false)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# This is determined by whether or not a line like the following appears in the output of `netstat -nr`:
|
17
|
+
# 0/1 10.137.0.41 UGSc 15 0 utun1
|
18
|
+
def vpn_running?
|
19
|
+
run_os_command('netstat -nr').split("\n").grep(/^0\/1.*utun1/).any?
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Identifies the (first) wireless network hardware port in the system, e.g. en0 or en1
|
24
|
+
def wifi_hardware_port
|
25
|
+
@wifi_hardware_port ||= begin
|
26
|
+
lines = run_os_command("networksetup -listallhardwareports").split("\n")
|
27
|
+
# Produces something like this:
|
28
|
+
# Hardware Port: Wi-Fi
|
29
|
+
# Device: en0
|
30
|
+
# Ethernet Address: ac:bc:32:b9:a9:9d
|
31
|
+
#
|
32
|
+
# Hardware Port: Bluetooth PAN
|
33
|
+
# Device: en3
|
34
|
+
# Ethernet Address: ac:bc:32:b9:a9:9e
|
35
|
+
wifi_port_line_num = (0...lines.size).detect do |index|
|
36
|
+
/: Wi-Fi$/.match(lines[index])
|
37
|
+
end
|
38
|
+
if wifi_port_line_num.nil?
|
39
|
+
raise %Q{Wifi port (e.g. "en0") not found in output of: networksetup -listallhardwareports}
|
40
|
+
else
|
41
|
+
lines[wifi_port_line_num + 1].split(': ').last
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Returns data pertaining to available wireless networks.
|
48
|
+
# For some reason, this often returns no results, so I've put the operation in a loop.
|
49
|
+
# I was unable to detect a sort strategy in the airport utility's output, so I sort
|
50
|
+
# the lines alphabetically, to show duplicates and for easier lookup.
|
51
|
+
def available_network_info
|
52
|
+
return nil unless wifi_on? # no need to try
|
53
|
+
command = "#{AIRPORT_CMD} -s"
|
54
|
+
max_attempts = 50
|
55
|
+
|
56
|
+
|
57
|
+
reformat_line = ->(line) do
|
58
|
+
ssid = line[0..31].strip
|
59
|
+
"%-32.32s%s" % [ssid, line[32..-1]]
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
process_tabular_data = ->(output) do
|
64
|
+
lines = output.split("\n")
|
65
|
+
header_line = lines[0]
|
66
|
+
data_lines = lines[1..-1]
|
67
|
+
data_lines.map! do |line|
|
68
|
+
# Reformat the line so that the name is left instead of right justified
|
69
|
+
reformat_line.(line)
|
70
|
+
end
|
71
|
+
data_lines.sort!
|
72
|
+
[reformat_line.(header_line)] + data_lines
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
output = try_os_command_until(command, ->(output) do
|
77
|
+
! ([nil, ''].include?(output))
|
78
|
+
end)
|
79
|
+
|
80
|
+
if output
|
81
|
+
process_tabular_data.(output)
|
82
|
+
else
|
83
|
+
raise "Unable to get available network information after #{max_attempts} attempts."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def parse_network_names(info)
|
89
|
+
if info.nil?
|
90
|
+
nil
|
91
|
+
else
|
92
|
+
info[1..-1] \
|
93
|
+
.map { |line| line[0..32].rstrip } \
|
94
|
+
.uniq \
|
95
|
+
.sort { |s1, s2| s1.casecmp(s2) }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# @return an array of unique available network names only, sorted alphabetically
|
101
|
+
# Kludge alert: the tabular data does not differentiate between strings with and without leading whitespace
|
102
|
+
# Therefore, we get the data once in tabular format, and another time in XML format.
|
103
|
+
# The XML element will include any leading whitespace. However, it includes all <string> elements,
|
104
|
+
# many of which are not network names.
|
105
|
+
# As an improved approximation of the correct result, for each network name found in tabular mode,
|
106
|
+
# we look to see if there is a corresponding string element with leading whitespace, and, if so,
|
107
|
+
# replace it.
|
108
|
+
#
|
109
|
+
# This will not behave correctly if a given name has occurrences with different amounts of whitespace,
|
110
|
+
# e.g. ' x' and ' x'.
|
111
|
+
#
|
112
|
+
# The reason we don't use an XML parser to get the exactly correct result is that we don't want
|
113
|
+
# users to need to install any external dependencies in order to run this script.
|
114
|
+
def available_network_names
|
115
|
+
|
116
|
+
# Parses the XML text (using grep, not XML parsing) to find
|
117
|
+
# <string> elements, and extracts the network name candidates
|
118
|
+
# containing leading spaces from it.
|
119
|
+
get_leading_space_names = ->(text) do
|
120
|
+
text.split("\n") \
|
121
|
+
.grep(%r{<string>}) \
|
122
|
+
.sort \
|
123
|
+
.uniq \
|
124
|
+
.map { |line| line.gsub("<string>", '').gsub('</string>', '').gsub("\t", '') } \
|
125
|
+
.select { |s| s[0] == ' ' }
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
output_is_valid = ->(output) { ! ([nil, ''].include?(output)) }
|
130
|
+
tabular_data = try_os_command_until("#{AIRPORT_CMD} -s", output_is_valid)
|
131
|
+
xml_data = try_os_command_until("#{AIRPORT_CMD} -s -x", output_is_valid)
|
132
|
+
|
133
|
+
if tabular_data.nil? || xml_data.nil?
|
134
|
+
raise "Unable to get available network information; please try again."
|
135
|
+
end
|
136
|
+
|
137
|
+
tabular_data_lines = tabular_data[1..-1] # omit header line
|
138
|
+
names_no_spaces = parse_network_names(tabular_data_lines.split("\n")).map(&:strip)
|
139
|
+
names_maybe_spaces = get_leading_space_names.(xml_data)
|
140
|
+
|
141
|
+
names = names_no_spaces.map do |name_no_spaces|
|
142
|
+
match = names_maybe_spaces.detect do |name_maybe_spaces|
|
143
|
+
%r{[ \t]?#{name_no_spaces}$}.match(name_maybe_spaces)
|
144
|
+
end
|
145
|
+
|
146
|
+
match ? match : name_no_spaces
|
147
|
+
end
|
148
|
+
|
149
|
+
names.sort { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
# Returns data pertaining to "preferred" networks, many/most of which will probably not be available.
|
154
|
+
def preferred_networks
|
155
|
+
lines = run_os_command("networksetup -listpreferredwirelessnetworks #{wifi_hardware_port}").split("\n")
|
156
|
+
# Produces something like this, unsorted, and with leading tabs:
|
157
|
+
# Preferred networks on en0:
|
158
|
+
# LibraryWiFi
|
159
|
+
# @thePAD/Magma
|
160
|
+
|
161
|
+
lines.delete_at(0) # remove title line
|
162
|
+
lines.map! { |line| line.gsub("\t", '') } # remove leading tabs
|
163
|
+
lines.sort! { |s1, s2| s1.casecmp(s2) } # sort alphabetically, case insensitively
|
164
|
+
lines
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
# Returns true if wifi is on, else false.
|
169
|
+
def wifi_on?
|
170
|
+
lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
|
171
|
+
lines.grep("AirPort: Off").none?
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
# Turns wifi on.
|
176
|
+
def wifi_on
|
177
|
+
return if wifi_on?
|
178
|
+
run_os_command("networksetup -setairportpower #{wifi_hardware_port} on")
|
179
|
+
wifi_on? ? nil : raise("Wifi could not be enabled.")
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# Turns wifi off.
|
184
|
+
def wifi_off
|
185
|
+
return unless wifi_on?
|
186
|
+
run_os_command("networksetup -setairportpower #{wifi_hardware_port} off")
|
187
|
+
wifi_on? ? raise("Wifi could not be disabled.") : nil
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
def connected_network_name
|
192
|
+
wifi_info['SSID']
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
# This method is called by BaseModel#connect to do the OS-specific connection logic.
|
197
|
+
def os_level_connect(network_name, password = nil)
|
198
|
+
command = "networksetup -setairportnetwork #{wifi_hardware_port} " + "#{Shellwords.shellescape(network_name)}"
|
199
|
+
if password
|
200
|
+
command << ' ' << Shellwords.shellescape(password)
|
201
|
+
end
|
202
|
+
run_os_command(command)
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
# @return:
|
207
|
+
# If the network is in the preferred networks list
|
208
|
+
# If a password is associated w/this network, return the password
|
209
|
+
# If not, return nil
|
210
|
+
# else
|
211
|
+
# raise an error
|
212
|
+
def os_level_preferred_network_password(preferred_network_name)
|
213
|
+
command = %Q{security find-generic-password -D "AirPort network password" -a "#{preferred_network_name}" -w 2>&1}
|
214
|
+
begin
|
215
|
+
return run_os_command(command).chomp
|
216
|
+
rescue OsCommandError => error
|
217
|
+
if error.exitstatus == 44 # network has no password stored
|
218
|
+
nil
|
219
|
+
else
|
220
|
+
raise
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
# Returns the IP address assigned to the wifi port, or nil if none.
|
227
|
+
def ip_address
|
228
|
+
begin
|
229
|
+
run_os_command("ipconfig getifaddr #{wifi_hardware_port}").chomp
|
230
|
+
rescue OsCommandError => error
|
231
|
+
if error.exitstatus == 1
|
232
|
+
nil
|
233
|
+
else
|
234
|
+
raise
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
def remove_preferred_network(network_name)
|
241
|
+
network_name = network_name.to_s
|
242
|
+
run_os_command("sudo networksetup -removepreferredwirelessnetwork " +
|
243
|
+
"#{wifi_hardware_port} #{Shellwords.shellescape(network_name)}")
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
# Returns the network currently connected to, or nil if none.
|
248
|
+
def current_network
|
249
|
+
lines = run_os_command("#{AIRPORT_CMD} -I").split("\n")
|
250
|
+
ssid_lines = lines.grep(/ SSID:/)
|
251
|
+
ssid_lines.empty? ? nil : ssid_lines.first.split('SSID: ').last.strip
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
# Disconnects from the currently connected network. Does not turn off wifi.
|
256
|
+
def disconnect
|
257
|
+
run_os_command("sudo #{AIRPORT_CMD} -z")
|
258
|
+
nil
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
# Returns some useful wifi-related information.
|
263
|
+
def wifi_info
|
264
|
+
|
265
|
+
info = {
|
266
|
+
wifi_on: wifi_on?,
|
267
|
+
internet_on: connected_to_internet?,
|
268
|
+
vpn_on: vpn_running?,
|
269
|
+
port: wifi_hardware_port,
|
270
|
+
network: current_network,
|
271
|
+
ip_address: ip_address,
|
272
|
+
timestamp: Time.now,
|
273
|
+
}
|
274
|
+
more_output = run_os_command(AIRPORT_CMD + " -I")
|
275
|
+
more_info = colon_output_to_hash(more_output)
|
276
|
+
info.merge!(more_info)
|
277
|
+
info.delete('AirPort') # will be here if off, but info is already in wifi_on key
|
278
|
+
info
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
# Parses output like the text below into a hash:
|
283
|
+
# SSID: Pattara211
|
284
|
+
# MCS: 5
|
285
|
+
# channel: 7
|
286
|
+
def colon_output_to_hash(output)
|
287
|
+
lines = output.split("\n")
|
288
|
+
lines.each_with_object({}) do |line, new_hash|
|
289
|
+
key, value = line.split(': ')
|
290
|
+
key = key.strip
|
291
|
+
value.strip! if value
|
292
|
+
new_hash[key] = value
|
293
|
+
end
|
294
|
+
end
|
295
|
+
private :colon_output_to_hash
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'command_line_interface'
|
2
|
+
|
3
|
+
module MacWifi
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'optparse'
|
7
|
+
require 'ostruct'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
class Main
|
11
|
+
|
12
|
+
def assert_os_is_mac_os
|
13
|
+
host_os = RbConfig::CONFIG["host_os"]
|
14
|
+
unless /darwin/.match(host_os) # e.g. "darwin16.4.0"
|
15
|
+
raise "This program currently works only on Mac OS. Platform is '#{host_os}'."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Parses the command line with Ruby's internal 'optparse'.
|
21
|
+
# Looks for "-v" flag to set verbosity to true.
|
22
|
+
# optparse removes what it processes from ARGV, which simplifies our command parsing.
|
23
|
+
def parse_command_line
|
24
|
+
options = OpenStruct.new
|
25
|
+
OptionParser.new do |parser|
|
26
|
+
parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
27
|
+
options.verbose = v
|
28
|
+
end
|
29
|
+
|
30
|
+
parser.on("-s", "--shell", "Start interactive shell") do |v|
|
31
|
+
options.interactive_mode = true
|
32
|
+
end
|
33
|
+
|
34
|
+
parser.on("-o", "--output_format FORMAT", "Format output data") do |v|
|
35
|
+
|
36
|
+
transformers = {
|
37
|
+
'i' => ->(object) { object.inspect },
|
38
|
+
'j' => ->(object) { JSON.pretty_generate(object) },
|
39
|
+
'p' => ->(object) { sio = StringIO.new; sio.puts(object); sio.string },
|
40
|
+
'y' => ->(object) { object.to_yaml }
|
41
|
+
}
|
42
|
+
|
43
|
+
choice = v[0].downcase
|
44
|
+
|
45
|
+
unless transformers.keys.include?(choice)
|
46
|
+
raise %Q{Output format "#{choice}" not in list of available formats} +
|
47
|
+
" (#{transformers.keys.inspect})."
|
48
|
+
end
|
49
|
+
|
50
|
+
options.post_processor = transformers[choice]
|
51
|
+
end
|
52
|
+
|
53
|
+
parser.on("-h", "--help", "Show help") do |_help_requested|
|
54
|
+
ARGV << 'h' # pass on the request to the command processor
|
55
|
+
end
|
56
|
+
end.parse!
|
57
|
+
options
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def call
|
62
|
+
assert_os_is_mac_os
|
63
|
+
|
64
|
+
options = parse_command_line
|
65
|
+
|
66
|
+
MacWifi::CommandLineInterface.new(options).call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/mac-wifi.rb
ADDED
data/mac-wifi.gemspec
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
require_relative 'lib/mac-wifi/version'
|
4
|
+
|
5
|
+
puts MacWifi::VERSION
|
4
6
|
|
5
7
|
Gem::Specification.new do |spec|
|
6
8
|
spec.name = "mac-wifi"
|
7
|
-
spec.version = VERSION
|
9
|
+
spec.version = MacWifi::VERSION
|
8
10
|
spec.authors = ["Keith Bennett"]
|
9
11
|
spec.email = ["keithrbennett@gmail.com"]
|
10
12
|
spec.description = %q{A command line interface for managing wifi on a Mac.}
|
@@ -15,8 +17,10 @@ Gem::Specification.new do |spec|
|
|
15
17
|
spec.files = `git ls-files`.split($/)
|
16
18
|
spec.bindir = 'exe'
|
17
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
18
23
|
# spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
# spec.require_paths = ['lib']
|
20
24
|
|
21
25
|
spec.add_development_dependency "bundler", "~> 1.16"
|
22
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mac-wifi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keith Bennett
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-01-
|
11
|
+
date: 2018-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,12 @@ files:
|
|
66
66
|
- README.md
|
67
67
|
- RELEASE_NOTES.md
|
68
68
|
- exe/mac-wifi
|
69
|
+
- lib/mac-wifi.rb
|
70
|
+
- lib/mac-wifi/base_model.rb
|
71
|
+
- lib/mac-wifi/command_line_interface.rb
|
72
|
+
- lib/mac-wifi/mac_os_model.rb
|
73
|
+
- lib/mac-wifi/main.rb
|
74
|
+
- lib/mac-wifi/version.rb
|
69
75
|
- mac-wifi.gemspec
|
70
76
|
- sample-avail-network-data.xml
|
71
77
|
- sample-available-networks.json
|
@@ -91,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
97
|
version: '0'
|
92
98
|
requirements: []
|
93
99
|
rubyforge_project:
|
94
|
-
rubygems_version: 2.
|
100
|
+
rubygems_version: 2.7.3
|
95
101
|
signing_key:
|
96
102
|
specification_version: 4
|
97
103
|
summary: Mac wifi utility
|