Fixed terminal support of recoverycore

Bryan Roe committed Apr 23, 2020 at 02:15 UTC d1ff8e5b62dc62819fea7b2671a64b356fb116e5
2 files changed +136 -28
agents/recoverycore.js
+128 -24
@@ -116,8 +116,80 @@ function onTunnelUpgrade(response, s, head) {
116 }
117 }
118
119 +// Called when receiving control data on websocket
120 +function onTunnelControlData(data, ws)
121 +{
122 + var obj;
123 + if (ws == null) { ws = this; }
124 + if (typeof data == 'string') { try { obj = JSON.parse(data); } catch (e) { sendConsoleText('Invalid control JSON: ' + data); return; } }
125 + else if (typeof data == 'object') { obj = data; } else { return; }
126 + //sendConsoleText('onTunnelControlData(' + ws.httprequest.protocol + '): ' + JSON.stringify(data));
127 + //console.log('onTunnelControlData: ' + JSON.stringify(data));
128 +
129 + if (obj.action)
130 + {
131 + switch (obj.action)
132 + {
133 + case 'lock': {
134 + // Lock the current user out of the desktop
135 + try
136 + {
137 + if (process.platform == 'win32')
138 + {
139 + MeshServerLog("Locking remote user out of desktop", ws.httprequest);
140 + var child = require('child_process');
141 + child.execFile(process.env['windir'] + '\\system32\\cmd.exe', ['/c', 'RunDll32.exe user32.dll,LockWorkStation'], { type: 1 });
142 + }
143 + } catch (e) { }
144 + break;
145 + }
146 + default:
147 + // Unknown action, ignore it.
148 + break;
149 + }
150 + return;
151 + }
152 +
153 + switch (obj.type)
154 + {
155 + case 'options': {
156 + // These are additional connection options passed in the control channel.
157 + //sendConsoleText('options: ' + JSON.stringify(obj));
158 + delete obj.type;
159 + ws.httprequest.xoptions = obj;
160 +
161 + // Set additional user consent options if present
162 + if ((obj != null) && (typeof obj.consent == 'number')) { ws.httprequest.consent |= obj.consent; }
163 +
164 + break;
165 + }
166 + case 'close': {
167 + // We received the close on the websocket
168 + //sendConsoleText('Tunnel #' + ws.tunnel.index + ' WebSocket control close');
169 + try { ws.close(); } catch (e) { }
170 + break;
171 + }
172 + case 'termsize': {
173 + // Indicates a change in terminal size
174 + if (process.platform == 'win32')
175 + {
176 + if (ws.httprequest._dispatcher == null) return;
177 + if (ws.httprequest._dispatcher.invoke) { ws.httprequest._dispatcher.invoke('resizeTerminal', [obj.cols, obj.rows]); }
178 + }
179 + else
180 + {
181 + if (ws.httprequest.process == null || ws.httprequest.process.pty == 0) return;
182 + if (ws.httprequest.process.tcsetsize) { ws.httprequest.process.tcsetsize(obj.rows, obj.cols); }
183 + }
184 + break;
185 + }
186 + }
187 +}
188 +
189 +
190 require('MeshAgent').AddCommandHandler(function (data) {
120 - if (typeof data == 'object') {
191 + if (typeof data == 'object')
192 + {
193 // If this is a console command, parse it and call the console handler
194 switch (data.action) {
195 case 'msg':
@@ -171,20 +243,60 @@ require('MeshAgent').AddCommandHandler(function (data) {
243 if ((data == 'c') || (data == 'cr')) { this.httprequest.state = 1; sendConsoleText("Tunnel #" + this.httprequest.index + " now active", this.httprequest.sessionid); }
244 } else {
245 // Handle tunnel data
174 - if (this.httprequest.protocol == 0) {
246 + if (this.httprequest.protocol == 0)
247 + {
248 + if ((data.length > 3) && (data[0] == '{')) { onTunnelControlData(data, this); return; }
249 // Take a look at the protocol
250 this.httprequest.protocol = parseInt(data);
251 if (typeof this.httprequest.protocol != 'number') { this.httprequest.protocol = 0; }
252 if (this.httprequest.protocol == 1) {
253 // Remote terminal using native pipes
180 - if (process.platform == "win32") {
181 - this.httprequest._term = require('win-terminal').Start(80, 25);
182 - this.httprequest._term.pipe(this, { dataTypeSkip: 1 });
183 - this.pipe(this.httprequest._term, { dataTypeSkip: 1, end: false });
184 - this.prependListener('end', function () { this.httprequest._term.end(function () { sendConsoleText('Terminal was closed'); }); });
254 + if (process.platform == "win32")
255 + {
256 + var cols = 80, rows = 25;
257 + if (this.httprequest.xoptions)
258 + {
259 + if (this.httprequest.xoptions.rows) { rows = this.httprequest.xoptions.rows; }
260 + if (this.httprequest.xoptions.cols) { cols = this.httprequest.xoptions.cols; }
261 + }
262 +
263 + // Admin Terminal
264 + if (require('win-virtual-terminal').supported)
265 + {
266 + // ConPTY PseudoTerminal
267 + // this.httprequest._term = require('win-virtual-terminal')[this.httprequest.protocol == 6 ? 'StartPowerShell' : 'Start'](80, 25);
268 +
269 + // The above line is commented out, because there is a bug with ClosePseudoConsole() API, so this is the workaround
270 + this.httprequest._dispatcher = require('win-dispatcher').dispatch({ modules: [{ name: 'win-virtual-terminal', script: getJSModule('win-virtual-terminal') }], launch: { module: 'win-virtual-terminal', method: 'Start', args: [cols, rows] } });
271 + this.httprequest._dispatcher.ws = this;
272 + this.httprequest._dispatcher.on('connection', function (c)
273 + {
274 + this.ws._term = c;
275 + c.pipe(this.ws, { dataTypeSkip: 1 });
276 + this.ws.pipe(c, { dataTypeSkip: 1 });
277 + });
278 + }
279 + else
280 + {
281 + // Legacy Terminal
282 + this.httprequest._term = require('win-terminal').Start(80, 25);
283 + this.httprequest._term.pipe(this, { dataTypeSkip: 1 });
284 + this.pipe(this.httprequest._term, { dataTypeSkip: 1, end: false });
285 + this.prependListener('end', function () { this.httprequest._term.end(function () { sendConsoleText('Terminal was closed'); }); });
286 + }
287 }
186 - else {
187 - this.httprequest.process = childProcess.execFile("/bin/sh", ["sh"], { type: childProcess.SpawnTypes.TERM });
288 + else
289 + {
290 + var env = { HISTCONTROL: 'ignoreboth' };
291 + if (this.httprequest.xoptions)
292 + {
293 + if (this.httprequest.xoptions.rows) { env.LINES = ('' + this.httprequest.xoptions.rows); }
294 + if (this.httprequest.xoptions.cols) { env.COLUMNS = ('' + this.httprequest.xoptions.cols); }
295 + }
296 + var options = { type: childProcess.SpawnTypes.TERM, env: env };
297 + this.httprequest.process = childProcess.execFile('/bin/bash', ['bash'], options); // Start bash
298 + // Spaces at the beginning of lines are needed to hide commands from the command history
299 + if (process.platform == 'linux') { this.httprequest.process.stdin.write(' alias ls=\'ls --color=auto\';clear\n'); }
300 this.httprequest.process.tunnel = this;
301 this.httprequest.process.on('exit', function (ecode, sig) { this.tunnel.end(); });
302 this.httprequest.process.stderr.on('data', function (chunk) { this.parent.tunnel.write(chunk); });
@@ -192,18 +304,6 @@ require('MeshAgent').AddCommandHandler(function (data) {
304 this.pipe(this.httprequest.process.stdin, { dataTypeSkip: 1, end: false }); // 0 = Binary, 1 = Text.
305 this.prependListener('end', function () { this.httprequest.process.kill(); });
306 }
195 -
196 - this.on('end', function () {
197 - if (process.platform == "win32") {
198 - // Unpipe the web socket
199 - this.unpipe(this.httprequest._term);
200 - this.httprequest._term.unpipe(this);
201 -
202 - // Clean up
203 - this.httprequest._term.end();
204 - this.httprequest._term = null;
205 - }
206 - });
307 }
308 }
309 else if (this.httprequest.protocol == 5) {
@@ -318,11 +418,15 @@ function processConsoleCommand(cmd, args, rights, sessionid) {
418 break;
419 case 'osinfo': { // Return the operating system information
420 var i = 1;
321 - if (args['_'].length > 0) { i = parseInt(args['_'][0]); if (i > 8) { i = 8; } response = "Calling " + i + " times."; }
322 - for (var j = 0; j < i; j++) {
421 + if (args['_'].length > 0) { i = parseInt(args['_'][0]); if (i > 8) { i = 8; } response = 'Calling ' + i + ' times.'; }
422 + for (var j = 0; j < i; j++)
423 + {
424 var pr = require('os').name();
425 pr.sessionid = sessionid;
325 - pr.then(function (v) { sendConsoleText("OS: " + v, this.sessionid); });
426 + pr.then(function (v)
427 + {
428 + sendConsoleText("OS: " + v + (process.platform == 'win32' ? (require('win-virtual-terminal').supported ? ' [ConPTY: YES]' : ' [ConPTY: NO]') : ''), this.sessionid);
429 + });
430 }
431 break;
432 }
meshcentral.js
+8 -4
@@ -1901,15 +1901,19 @@ function CreateMeshCentralServer(config, args) {
1901 }
1902
1903 // Merge this module to recovery modules if needed
1904 - if (modulesAdd['windows-recovery'] != null) {
1905 - if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal')) {
1904 + if (modulesAdd['windows-recovery'] != null)
1905 + {
1906 + if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal'))
1907 + {
1908 modulesAdd['windows-recovery'].push(...moduleData);
1909 }
1910 }
1911
1912 // Merge this module to agent recovery modules if needed
1911 - if (modulesAdd['windows-agentrecovery'] != null) {
1912 - if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal')) {
1913 + if (modulesAdd['windows-agentrecovery'] != null)
1914 + {
1915 + if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal'))
1916 + {
1917 modulesAdd['windows-agentrecovery'].push(...moduleData);
1918 }
1919 }