mac-wifi 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|