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.
@@ -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
@@ -0,0 +1,5 @@
1
+ module MacWifi
2
+
3
+ VERSION = '2.1.0'
4
+
5
+ end
data/lib/mac-wifi.rb ADDED
@@ -0,0 +1,3 @@
1
+ require_relative 'mac-wifi/version'
2
+
3
+ require_relative 'mac-wifi/main' # recursively requires the other files
data/mac-wifi.gemspec CHANGED
@@ -1,10 +1,12 @@
1
1
  # coding: utf-8
2
- # When changing the version, also change the version and the help text in the README.
3
- VERSION = '2.0.0'
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.0.0
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-14 00:00:00.000000000 Z
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.6.14
100
+ rubygems_version: 2.7.3
95
101
  signing_key:
96
102
  specification_version: 4
97
103
  summary: Mac wifi utility