Added self-update capability

Bryan Roe committed Jan 14, 2021 at 17:08 UTC 522836eacce1a5f7733644972cefc321b49a1f58
1 file changed +246 -21
agents/recoverycore.js
+246 -21
@@ -6,11 +6,210 @@ var nextTunnelIndex = 1;
6 var tunnels = {};
7 var fs = require('fs');
8
9 +if (require('MeshAgent').ARCHID == null)
10 +{
11 + var id = null;
12 + switch (process.platform)
13 + {
14 + case 'win32':
15 + id = require('_GenericMarshal').PointerSize == 4 ? 3 : 4;
16 + break;
17 + case 'freebsd':
18 + id = require('_GenericMarshal').PointerSize == 4 ? 31 : 30;
19 + break;
20 + case 'darwin':
21 + id = require('os').arch() == 'x64' ? 16 : 29;
22 + break;
23 + }
24 + if (id != null) { Object.defineProperty(require('MeshAgent'), 'ARCHID', { value: id }); }
25 +}
26 +
27 //attachDebugger({ webport: 9994, wait: 1 }).then(function (p) { console.log('Debug on port: ' + p); });
28
11 -function sendConsoleText(msg) {
12 - require('MeshAgent').SendCommand({ action: 'msg', type: 'console', value: msg });
29 +function sendConsoleText(msg, sessionid)
30 +{
31 + if (sessionid != null)
32 + {
33 + require('MeshAgent').SendCommand({ action: 'msg', type: 'console', value: msg, sessionid: sessionid });
34 + }
35 + else
36 + {
37 + require('MeshAgent').SendCommand({ action: 'msg', type: 'console', value: msg });
38 + }
39 +}
40 +
41 +function sendAgentMessage(msg, icon)
42 +{
43 + if (sendAgentMessage.messages == null)
44 + {
45 + sendAgentMessage.messages = {};
46 + sendAgentMessage.nextid = 1;
47 + }
48 + sendAgentMessage.messages[sendAgentMessage.nextid++] = { msg: msg, icon: icon };
49 + require('MeshAgent').SendCommand({ action: 'sessions', type: 'msg', value: sendAgentMessage.messages });
50 }
51 +
52 +function agentUpdate_Start(updateurl, updateoptions)
53 +{
54 + var sessionid = updateoptions != null ? updateoptions.session : null;
55 +
56 + if (this._selfupdate != null)
57 + {
58 + if (sessionid != null) { sendConsoleText('Self update already in progress...', sessionid); }
59 + }
60 + else
61 + {
62 + if (require('MeshAgent').ARCHID == null && updateurl == null)
63 + {
64 + if (sessionid != null) { sendConsoleText('Unable to initiate update, agent ARCHID is not defined', sessionid); }
65 + }
66 + else
67 + {
68 + var agentfilename = process.execPath.split(process.platform == 'win32' ? '\\' : '/').pop();
69 + var name = require('MeshAgent').serviceName;
70 + if (name == null) { name = process.platform == 'win32' ? 'Mesh Agent' : 'meshagent'; }
71 + try
72 + {
73 + var s = require('service-manager').manager.getService(name);
74 + if (!s.isMe())
75 + {
76 + if (process.platform == 'win32') { s.close(); }
77 + if (sessionid != null) { sendConsoleText('Service check FAILED', sessionid); }
78 + return;
79 + }
80 + if (process.platform == 'win32') { s.close(); }
81 + }
82 + catch (zz)
83 + {
84 + if (sessionid != null) { sendConsoleText('Service check FAILED', sessionid); }
85 + else
86 + {
87 + sendAgentMessage('Self Update Failed, because this agent is not running as a service', 3);
88 + }
89 + return;
90 + }
91 +
92 + if (sessionid != null) { sendConsoleText('Downloading update...', sessionid); }
93 + var options = require('http').parseUri(updateurl != null ? updateurl : require('MeshAgent').ServerUrl);
94 + options.protocol = 'https:';
95 + if (updateurl == null) { options.path = ('/meshagents?id=' + require('MeshAgent').ARCHID); }
96 + options.rejectUnauthorized = false;
97 + options.checkServerIdentity = function checkServerIdentity(certs)
98 + {
99 + // If the tunnel certificate matches the control channel certificate, accept the connection
100 + try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.digest == certs[0].digest) return; } catch (ex) { }
101 + try { if (require('MeshAgent').ServerInfo.ControlChannelCertificate.fingerprint == certs[0].fingerprint) return; } catch (ex) { }
102 +
103 + // Check that the certificate is the one expected by the server, fail if not.
104 + if (checkServerIdentity.servertlshash == null)
105 + {
106 + if(sessionid!=null)
107 + {
108 + sendConsoleText('Self Update failed, because the url cannot be verified', sessionid);
109 + }
110 + else
111 + {
112 + sendAgentMessage('Self Update failed, because the url cannot be verified', 3);
113 + }
114 + throw new Error('BadCert');
115 + }
116 + if ((checkServerIdentity.servertlshash != null) && (checkServerIdentity.servertlshash.toLowerCase() != certs[0].digest.split(':').join('').toLowerCase()))
117 + {
118 + if (sessionid != null)
119 + {
120 + sendConsoleText('Self Update failed, because the supplied certificate does not match', sessionid);
121 + }
122 + else
123 + {
124 + sendAgentMessage('Self Update failed, because the supplied certificate does not match', 3);
125 + }
126 + throw new Error('BadCert')
127 + }
128 + }
129 + options.checkServerIdentity.servertlshash = (updateoptions != null ? updateoptions.tlshash : null);
130 + this._selfupdate = require('https').get(options);
131 + this._selfupdate.on('error', function (e)
132 + {
133 + if (sessionid != null) { sendConsoleText('Error fetching update', sessionid); }
134 + else
135 + {
136 + sendAgentMessage('Self Update failed, because there was a problem trying to download the update', 3);
137 + }
138 + });
139 + this._selfupdate.on('response', function (img)
140 + {
141 + this._file = require('fs').createWriteStream(agentfilename + '.update', { flags: 'wb' });
142 + this._filehash = require('SHA384Stream').create();
143 + this._filehash.on('hash', function (h)
144 + {
145 + if (updateoptions != null && updateoptions.hash != null)
146 + {
147 + if (updateoptions.hash.toLowerCase() == h.toString('hex').toLowerCase())
148 + {
149 + if (sessionid != null) { sendConsoleText('Download complete. HASH verified.', sessionid); }
150 + }
151 + else
152 + {
153 + if (sessionid != null) { sendConsoleText('Download complete. HASH FAILED.', sessionid); }
154 + else
155 + {
156 + sendAgentMessage('Self Update FAILED because the downloaded agent FAILED hash check', 3);
157 + }
158 + return;
159 + }
160 + }
161 + else
162 + {
163 + if (sessionid != null) { sendConsoleText('Download complete. HASH=' + h.toString('hex'), sessionid); }
164 + }
165 +
166 + if (sessionid != null) { sendConsoleText('Updating and restarting agent...', sessionid); }
167 + if (process.platform == 'win32')
168 + {
169 + this.child = require('child_process').execFile(process.env['windir'] + '\\system32\\cmd.exe',
170 + ['/C wmic service "' + name + '" call stopservice && copy "' + process.cwd() + agentfilename + '.update" "' + process.execPath + '" && wmic service "' + name + '" call startservice && erase "' + process.cwd() + agentfilename + '.update"'], { type: 4 | 0x8000 });
171 + }
172 + else
173 + {
174 + // remove binary
175 + require('fs').unlinkSync(process.execPath);
176 +
177 + // copy update
178 + require('fs').copyFileSync(process.cwd() + agentfilename + '.update', process.execPath);
179 +
180 + // erase update
181 + require('fs').unlinkSync(process.cwd() + agentfilename + '.update');
182 +
183 + // add execute permissions
184 + var m = require('fs').statSync(process.execPath).mode;
185 + m |= (require('fs').CHMOD_MODES.S_IXUSR | require('fs').CHMOD_MODES.S_IXGRP | require('fs').CHMOD_MODES.S_IXOTH);
186 + require('fs').chmodSync(process.execPath, m);
187 +
188 + if (sessionid != null) { sendConsoleText('Restarting service...', sessionid); }
189 + try
190 + {
191 + // restart service
192 + var s = require('service-manager').manager.getService(name);
193 + s.restart();
194 + }
195 + catch (zz)
196 + {
197 + if (sessionid != null) { sendConsoleText('Error restarting service', sessionid); }
198 + else
199 + {
200 + sendAgentMessage('Self Update encountered an error trying to restart service', 3);
201 + }
202 + }
203 + }
204 + });
205 + img.pipe(this._file);
206 + img.pipe(this._filehash);
207 + });
208 + }
209 + }
210 +}
211 +
212 +
213 // Return p number of spaces
214 function addPad(p, ret) { var r = ''; for (var i = 0; i < p; i++) { r += ret; } return r; }
215
@@ -187,16 +386,23 @@ function onTunnelControlData(data, ws)
386 }
387
388
190 -require('MeshAgent').AddCommandHandler(function (data) {
389 +require('MeshAgent').AddCommandHandler(function (data)
390 +{
391 if (typeof data == 'object')
392 {
393 // If this is a console command, parse it and call the console handler
194 - switch (data.action) {
394 + switch (data.action)
395 + {
396 + case 'agentupdate':
397 + agentUpdate_Start(data.url, { hash: data.hash, tlshash: data.servertlshash });
398 + break;
399 case 'msg':
400 {
197 - switch (data.type) {
401 + switch (data.type)
402 + {
403 case 'console': { // Process a console command
199 - if (data.value && data.sessionid) {
404 + if (data.value && data.sessionid)
405 + {
406 var args = splitArgs(data.value);
407 processConsoleCommand(args[0].toLowerCase(), parseArgs(args), data.rights, data.sessionid);
408 }
@@ -204,19 +410,23 @@ require('MeshAgent').AddCommandHandler(function (data) {
410 }
411 case 'tunnel':
412 {
207 - if (data.value != null) { // Process a new tunnel connection request
413 + if (data.value != null)
414 + { // Process a new tunnel connection request
415 // Create a new tunnel object
416 var xurl = getServerTargetUrlEx(data.value);
210 - if (xurl != null) {
417 + if (xurl != null)
418 + {
419 var woptions = http.parseUri(xurl);
420 woptions.rejectUnauthorized = 0;
421 //sendConsoleText(JSON.stringify(woptions));
422 var tunnel = http.request(woptions);
215 - tunnel.on('upgrade', function (response, s, head) {
423 + tunnel.on('upgrade', function (response, s, head)
424 + {
425 this.s = s;
426 s.httprequest = this;
427 s.tunnel = this;
219 - s.on('end', function () {
428 + s.on('end', function ()
429 + {
430 if (tunnels[this.httprequest.index] == null) return; // Stop duplicate calls.
431
432 // If there is a upload or download active on this connection, close the file
@@ -230,18 +440,22 @@ require('MeshAgent').AddCommandHandler(function (data) {
440 // Clean up WebSocket
441 this.removeAllListeners('data');
442 });
233 - s.on('data', function (data) {
443 + s.on('data', function (data)
444 + {
445 // If this is upload data, save it to file
235 - if (this.httprequest.uploadFile) {
446 + if (this.httprequest.uploadFile)
447 + {
448 try { fs.writeSync(this.httprequest.uploadFile, data); } catch (e) { this.write(Buffer.from(JSON.stringify({ action: 'uploaderror' }))); return; } // Write to the file, if there is a problem, error out.
449 this.write(Buffer.from(JSON.stringify({ action: 'uploadack', reqid: this.httprequest.uploadFileid }))); // Ask for more data
450 return;
451 }
452
241 - if (this.httprequest.state == 0) {
453 + if (this.httprequest.state == 0)
454 + {
455 // Check if this is a relay connection
456 if ((data == 'c') || (data == 'cr')) { this.httprequest.state = 1; sendConsoleText("Tunnel #" + this.httprequest.index + " now active", this.httprequest.sessionid); }
244 - } else {
457 + } else
458 + {
459 // Handle tunnel data
460 if (this.httprequest.protocol == 0)
461 {
@@ -296,7 +510,7 @@ require('MeshAgent').AddCommandHandler(function (data) {
510 }
511 var options = { type: childProcess.SpawnTypes.TERM, env: env };
512
299 - if(require('fs').existsSync('/bin/bash'))
513 + if (require('fs').existsSync('/bin/bash'))
514 {
515 this.httprequest.process = childProcess.execFile('/bin/bash', ['bash'], options); // Start bash
516 }
@@ -316,7 +530,8 @@ require('MeshAgent').AddCommandHandler(function (data) {
530 }
531 }
532 }
319 - else if (this.httprequest.protocol == 5) {
533 + else if (this.httprequest.protocol == 5)
534 + {
535 // Process files commands
536 var cmd = null;
537 try { cmd = JSON.parse(data); } catch (e) { };
@@ -329,7 +544,8 @@ require('MeshAgent').AddCommandHandler(function (data) {
544
545 if ((cmd.path != null) && (process.platform != 'win32') && (cmd.path[0] != '/')) { cmd.path = '/' + cmd.path; } // Add '/' to paths on non-windows
546 //console.log(objToString(cmd, 0, ' '));
332 - switch (cmd.action) {
547 + switch (cmd.action)
548 + {
549 case 'ls':
550 // Send the folder content to the browser
551 var response = getDirectoryInfo(cmd.path);
@@ -343,7 +559,8 @@ require('MeshAgent').AddCommandHandler(function (data) {
559 }
560 case 'rm': {
561 // Delete, possibly recursive delete
346 - for (var i in cmd.delfiles) {
562 + for (var i in cmd.delfiles)
563 + {
564 try { deleteFolderRecursive(path.join(cmd.path, cmd.delfiles[i]), cmd.rec); } catch (e) { }
565 }
566 break;
@@ -367,7 +584,8 @@ require('MeshAgent').AddCommandHandler(function (data) {
584 }
585 case 'copy': {
586 // Copy a bunch of files from scpath to dspath
370 - for (var i in cmd.names) {
587 + for (var i in cmd.names)
588 + {
589 var sc = path.join(cmd.scpath, cmd.names[i]), ds = path.join(cmd.dspath, cmd.names[i]);
590 if (sc != ds) { try { fs.copyFileSync(sc, ds); } catch (e) { } }
591 }
@@ -375,7 +593,8 @@ require('MeshAgent').AddCommandHandler(function (data) {
593 }
594 case 'move': {
595 // Move a bunch of files from scpath to dspath
378 - for (var i in cmd.names) {
596 + for (var i in cmd.names)
597 + {
598 var sc = path.join(cmd.scpath, cmd.names[i]), ds = path.join(cmd.dspath, cmd.names[i]);
599 if (sc != ds) { try { fs.copyFileSync(sc, ds); fs.unlinkSync(sc); } catch (e) { } }
600 }
@@ -424,7 +643,13 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
643 var response = null;
644 switch (cmd) {
645 case 'help':
427 - response = "Available commands are: osinfo, dbkeys, dbget, dbset, dbcompact, netinfo.";
646 + response = "Available commands are: osinfo, dbkeys, dbget, dbset, dbcompact, netinfo, versions.";
647 + break;
648 + case 'versions':
649 + response = JSON.stringify(process.versions, null, ' ');
650 + break;
651 + case 'agentupdate':
652 + agentUpdate_Start(null, { session: sessionid });
653 break;
654 case 'osinfo': { // Return the operating system information
655 var i = 1;