-
Notifications
You must be signed in to change notification settings - Fork 1
/
evpntoolkit.js
329 lines (281 loc) · 11.9 KB
/
evpntoolkit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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
320
321
322
323
324
325
326
327
328
329
'use strict';
//
// extension evpn-assistant - evpntoolkit.js
// JavaScript Gnome extension for ExpressVPN Shell Control.
//
// @author Stuart J Gilmour
// @copyright Copyright 2022, Stuart J Gilmour.
//
// Reference Materal:
// https://gjs.guide/guides/gio/subprocesses.html
// https://docs.gtk.org/glib/func.spawn_async_with_pipes.html
//
// @license
// The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Clutter = imports.gi.Clutter;
const GLib = imports.gi.GLib;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Utils = Me.imports.utils;
//
// AssistantToolkit
//
// @class AssistantToolkit
// @constructor
// @return {Object} instance
//
var AssistantToolkit = class AssistantToolkit
{
constructor()
{
Utils.logInfo("AssistantToolkit Constructor");
}
//
// Calls the expressvpn cli checking the connected state of the vpn connection
// @method EVpnCommandStatus()
// @return Symbol representing the vpn connection state
//
async EVpnCommandStatus()
{
let connectionState = Symbol.for("Unknown");
var cmdArgs = ['/usr/bin/expressvpn', 'status'];
var cmdType = Symbol.for("CheckVpnStatus");
connectionState = await this._eVpnCommand(cmdArgs, cmdType);
return connectionState;
}
//
// Calls the expressvpn cli and connects to the chosen destination
// @method EVpnCommandConnect()
// @return Symbol representing the vpn connection state
//
async EVpnCommandConnect(vpnDest)
{
var cmdArgs = ['/usr/bin/expressvpn', 'connect'];
cmdArgs.push(vpnDest);
var cmdType = Symbol.for("Connect");
let connectionState = Symbol.for("Unknown");
connectionState = await this._eVpnCommand(cmdArgs, cmdType);
return connectionState;
}
//
// Calls the expressvpn cli and disconnects from the vpn
// @method EVpnCommandDisconnect()
// @return Symbol representing the vpn connection state
//
async EVpnCommandDisconnect()
{
var cmdArgs = ['/usr/bin/expressvpn', 'disconnect'];
var cmdType = Symbol.for("Disconnect");
let connectionState = Symbol.for("Unknown");
connectionState = await this._eVpnCommand(cmdArgs, cmdType);
return connectionState;
}
//
// @method _eVpnCommand
// @description Generic method to execute cmds using a subprocess and process the results
// on stdout
// @private
// @param cmdArgs - array of strings representing the cmd to be executed
// @param cmdType - Symbol to define the kind of cmd being executed.
//
async _eVpnCommand(cmdArgs, cmdType)
{
Utils.logDebug("Executing cmd: " + cmdArgs.join(" ") + ". Cmd type: " + Symbol.keyFor(cmdType).toString());
const [res, pid, stdin, stdout, stderr] =
GLib.spawn_async_with_pipes(null, cmdArgs, null, GLib.SpawnFlags.DO_NOT_REAP_CHILD, null);
GLib.close(stdin);
Utils.logDebug("Pid for cmd " + cmdArgs.join(" ") + ": " + pid);
// Get stdout stream
let stdoutStream = new Gio.DataInputStream(
{
base_stream: new Gio.UnixInputStream({ fd: stdout, close_fd: true }),
close_base_stream: true
});
// Perform the reading of stdout async
let stdoutLines = [];
this._readOutputAsync(stdoutStream, stdoutLines);
// Get stderr stream
let stderrStream = new Gio.DataInputStream(
{
base_stream: new Gio.UnixInputStream({ fd: stderr, close_fd: true }),
close_base_stream: true
});
// Perform the reading of stderr async
let stderrLines = [];
this._readOutputAsync(stderrStream, stderrLines);
var connectionState = Symbol.for("Unknown");
try
{
// Setup a watch on the child process and wait for completion
await this._waitOnSubProcessToComplete(cmdArgs, pid, stdoutLines, stdoutStream, stderrStream);
// Process the VPN status check results from stdout, if the cmdType is for a 'CheckVpnStatus'
//if (Symbol.keyFor(cmdType).toString() === "CheckVpnStatus" || Symbol.keyFor(cmdType).toString() === "Connect")
connectionState = await this._processVpnStatus(stdoutLines);
}
catch (e)
{
// If an error occurred, we can report it using reject()
logError("Unable to determine VPN status. Child process most likely failed to read results from stdout. Is ExpressVPN installed?");
}
return connectionState;
}
//
// Recursive function to read stream output asynchronously
//
// @method readOutputAsync
// @param {String} stream for which text is to be read [stdout, stdin, stderr]
// @return {Object} an array of strings from the stream output
//
_readOutputAsync(stream, lineBuffer)
{
stream.read_line_async(0, null, (stream, async_res) =>
{
try
{
let data, len;
[data, len] = stream.read_line_finish_utf8(async_res);
if (data !== null && len > 0)
{
// Collect the output
lineBuffer.push(data);
// Keep calm and carry on recursively reading until done
this._readOutputAsync(stream, lineBuffer);
}
}
catch (e)
{
logError(e);
}
return lineBuffer;
});
}
//
// @method _waitOnSubProcessToComplete
//
// @description Generic method to wait on a subprocess to complete. The method being waiting on
// is 'readOutputAsync'. This method lets us know when the subprocess has completed.
// It is at this point that we know the underlying buffer with the stdout lines is
// now ready to be processed.
//
// @private method
//
// @param cmdArgs - array of strings representing the cmd to be executed
// @param pid - The pid to be monitored for completion
// @param stdoutLines - An array of strings representing the stdout returned from the cmd execution
// @param stdoutStream - The underlying stdout stream
// @param stderrStream - The underlying stderr stream
//
async _waitOnSubProcessToComplete(cmdArgs, pid, stdoutLines, stdoutStream, stderrStream)
{
return new Promise((resolve, reject) =>
{
GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) =>
{
// If the child process was successful
if (status === 0)
{
Utils.logDebug("Subprocess callback for cmd " + cmdArgs.join(" ") + " pid: " + pid);
let count = stdoutLines.length;
Utils.logDebug("Lines read: " + count);
stdoutLines.forEach(element =>
{
Utils.logDebug(element);
});
resolve(stdoutLines);
}
else
{
//logError("Child process failed with status: " + status);
reject("Unable to process stdout")
}
Utils.logDebug("Closing streams and pid");
// Close the streams and process
stdoutStream.close(null);
stderrStream.close(null);
GLib.spawn_close_pid(pid);
});
})
}
//
// @method _processVpnStatus
// @private
//
// @description This callback processes the current state of the express VPN connection.
//
// The chain of events start from when the eVpnCommandStatus method call is made. This method
// typically sets up a subprocess to execute the expressVPN status command as you would in shell.
// The output of the subprocess is handled in an asynchronous way with a watch added to the subprocess
// using the process identifer. The eVpnCommandStatus has an anonymous function which gets called
// to notify that all the stdout has been read and the subprocess has completed.
//
// This method is registered as a callback within the eVpnCommandStatus and is called from the internal
// anonymous function to return back the array of lines from reading the stdout.
//
// The callback is now free to process the results and set the state for the extension
//
// @param {Object} lineBuffer
// @returns {Object}
//
async _processVpnStatus(lineBuffer)
{
return new Promise((resolve, reject) =>
{
if (lineBuffer.count === 0)
return reject(Symbol.for("Unknown"));
lineBuffer.forEach(line =>
{
if (line.includes("Connected"))
{
Utils.logInfo("*** Connected ***");
return resolve(Symbol.for("Connected"));
}
if (line.includes("Not connected"))
{
Utils.logInfo("*** Not Connected ***");
return resolve(Symbol.for("NotConnected"));
}
if (line.includes("Disconnected"))
{
Utils.logInfo("*** Disconnected ***");
return resolve(Symbol.for("Disconnected"));
}
if (line.includes('Connecting') || line.includes('Disconnecting') || line.includes('Reconnecting'))
{
Utils.logInfo("*** Negotiating Connection Status ***");
return resolve(Symbol.for("Pending"));
}
if (line.includes("A new version is available"))
{
Utils.logInfo("*** Update Available ***");
}
if (line.includes("- To protect your privacy if your VPN connection unexpectedly drops"))
{
Utils.logInfo("*** Hints & Tips Available ***");
}
});
Utils.logNewline();
return resolve(Symbol.for("Unknown"));
});
}
}