From af052ddfe7bfde2c8d1286831a27c2e495566bc0 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 14 May 2022 23:00:57 -0700 Subject: [PATCH] First pass at adding RDP clipboard support, #3810. --- MeshCentralServer.njsproj | 1 + apprelays.js | 7 +- public/scripts/agent-rdp-0.0.1.js | 18 +- rdp/protocol/pdu/cliprdr.js | 327 ++++++++++++++++++++++++++++++ rdp/protocol/pdu/data.js | 34 +++- rdp/protocol/pdu/index.js | 10 +- rdp/protocol/rdp.js | 12 ++ rdp/protocol/t125/mcs.js | 41 +++- rdp/security/md4.js | 2 +- views/default.handlebars | 15 +- 10 files changed, 446 insertions(+), 21 deletions(-) create mode 100644 rdp/protocol/pdu/cliprdr.js diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 0f0a2e52..f458c404 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -198,6 +198,7 @@ + diff --git a/apprelays.js b/apprelays.js index 8c75eb9f..3a8d561b 100644 --- a/apprelays.js +++ b/apprelays.js @@ -152,7 +152,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) { obj.wsClient._socket.pause(); try { obj.relaySocket.write(data, function () { - try { obj.wsClient._socket.resume(); } catch (ex) { console.log(ex); } + if (obj.wsClient && obj.wsClient._socket) { try { obj.wsClient._socket.resume(); } catch (ex) { console.log(ex); } } }); } catch (ex) { console.log(ex); obj.close(); } } @@ -201,6 +201,10 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) { try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary delete bitmap.data; send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data. + }).on('clipboard', function (content) { + // Clipboard data changed + console.log('RDP clipboard recv', content); + send(['rdp-clipboard', content]); }).on('close', function () { send(['rdp-close']); }).on('error', function (err) { @@ -317,6 +321,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) { } case 'mouse': { if (rdpClient && (obj.viewonly != true)) { rdpClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; } case 'wheel': { if (rdpClient && (obj.viewonly != true)) { rdpClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; } + case 'clipboard': { rdpClient.setClipboardData(msg[1]); break; } case 'scancode': { if (obj.limitedinput == true) { // Limit keyboard input var ok = false, k = msg[1]; diff --git a/public/scripts/agent-rdp-0.0.1.js b/public/scripts/agent-rdp-0.0.1.js index 698ee7e0..436d9aa8 100644 --- a/public/scripts/agent-rdp-0.0.1.js +++ b/public/scripts/agent-rdp-0.0.1.js @@ -83,6 +83,10 @@ var CreateRDPDesktop = function (canvasid) { obj.Stop(); break; } + case 'rdp-clipboard': { + console.log('clipboard', msg[1]); + break; + } case 'ping': { obj.socket.send('["pong"]'); break; } case 'pong': { break; } } @@ -99,7 +103,15 @@ var CreateRDPDesktop = function (canvasid) { obj.Canvas.fillRect(0, 0, obj.ScreenWidth, obj.ScreenHeight); if (obj.socket) { obj.socket.close(); } } - + + obj.m.setClipboard = function (content) { + console.log('s1'); + if (obj.socket) { + console.log('s2', content); + obj.socket.send(JSON.stringify(['clipboard', content])); + } + } + function changeState(newstate) { if (obj.State == newstate) return; obj.State = newstate; @@ -153,14 +165,14 @@ var CreateRDPDesktop = function (canvasid) { } obj.m.handleKeyUp = function (e) { if (!obj.socket || (obj.State != 3)) return; - console.log('handleKeyUp', Mstsc.scancode(e)); + //console.log('handleKeyUp', Mstsc.scancode(e)); obj.socket.send(JSON.stringify(['scancode', Mstsc.scancode(e), false])); e.preventDefault(); return false; } obj.m.handleKeyDown = function (e) { if (!obj.socket || (obj.State != 3)) return; - console.log('handleKeyDown', Mstsc.scancode(e)); + //console.log('handleKeyDown', Mstsc.scancode(e)); obj.socket.send(JSON.stringify(['scancode', Mstsc.scancode(e), true])); e.preventDefault(); return false; diff --git a/rdp/protocol/pdu/cliprdr.js b/rdp/protocol/pdu/cliprdr.js new file mode 100644 index 00000000..15e3b86f --- /dev/null +++ b/rdp/protocol/pdu/cliprdr.js @@ -0,0 +1,327 @@ +const type = require('../../core').type; +const EventEmitter = require('events').EventEmitter; +const caps = require('./caps'); +const log = require('../../core').log; +const data = require('./data'); + + + +/** + * Cliprdr channel for all clipboard + * capabilities exchange + */ +class Cliprdr extends EventEmitter { + + constructor(transport) { + super(); + this.transport = transport; + // must be init via connect event + this.userId = 0; + this.serverCapabilities = []; + this.clientCapabilities = []; + } + +} + + +/** + * Client side of Cliprdr channel automata + * @param transport + */ +class Client extends Cliprdr { + + constructor(transport, fastPathTransport) { + + super(transport, fastPathTransport); + + this.transport.once('connect', (gccCore, userId, channelId) => { + this.connect(gccCore, userId, channelId); + }).on('close', () => { + this.emit('close'); + }).on('error', (err) => { + this.emit('error', err); + }); + + this.content = ''; + + } + + /** + * connect function + * @param gccCore {type.Component(clientCoreData)} + */ + connect(gccCore, userId, channelId) { + this.gccCore = gccCore; + this.userId = userId; + this.channelId = channelId; + this.transport.once('cliprdr', (s) => { + this.recv(s); + }); + } + + + send(message) { + this.transport.send('cliprdr', new type.Component([ + // Channel PDU Header + new type.UInt32Le(message.size()), + // CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST | CHANNEL_FLAG_SHOW_PROTOCOL + new type.UInt32Le(0x13), + message + ])); + }; + + recv(s) { + s.offset = 18; + const pdu = data.clipPDU().read(s), type = data.ClipPDUMsgType; + + switch (pdu.obj.header.obj.msgType.value) { + case type.CB_MONITOR_READY: + this.recvMonitorReadyPDU(s); + break; + case type.CB_FORMAT_LIST: + this.recvFormatListPDU(s); + break; + case type.CB_FORMAT_LIST_RESPONSE: + this.recvFormatListResponsePDU(s); + break; + case type.CB_FORMAT_DATA_REQUEST: + this.recvFormatDataRequestPDU(s); + break; + case type.CB_FORMAT_DATA_RESPONSE: + this.recvFormatDataResponsePDU(s); + break; + case type.CB_TEMP_DIRECTORY: + break; + case type.CB_CLIP_CAPS: + this.recvClipboardCapsPDU(s); + break; + case type.CB_FILECONTENTS_REQUEST: + } + + this.transport.once('cliprdr', (s) => { + this.recv(s); + }); + } + + /** + * Receive capabilities from server + * @param s {type.Stream} + */ + recvClipboardCapsPDU(s) { + // Start at 18 + s.offset = 18; + // const pdu = data.clipPDU().read(s); + // console.log('recvClipboardCapsPDU', s); + } + + + /** + * Receive monitor ready from server + * @param s {type.Stream} + */ + recvMonitorReadyPDU(s) { + s.offset = 18; + // const pdu = data.clipPDU().read(s); + // console.log('recvMonitorReadyPDU', s); + + this.sendClipboardCapsPDU(); + // this.sendClientTemporaryDirectoryPDU(); + this.sendFormatListPDU(); + } + + + /** + * Send clipboard capabilities PDU + */ + sendClipboardCapsPDU() { + this.send(new type.Component({ + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_CLIP_CAPS), + msgFlags: new type.UInt16Le(0x00), + dataLen: new type.UInt32Le(0x10), + cCapabilitiesSets: new type.UInt16Le(0x01), + pad1: new type.UInt16Le(0x00), + capabilitySetType: new type.UInt16Le(0x01), + lengthCapability: new type.UInt16Le(0x0c), + version: new type.UInt32Le(0x02), + capabilityFlags: new type.UInt32Le(0x02) + })); + } + + + /** + * Send client temporary directory PDU + */ + sendClientTemporaryDirectoryPDU(path = '') { + // TODO + this.send(new type.Component({ + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_TEMP_DIRECTORY), + msgFlags: new type.UInt16Le(0x00), + dataLen: new type.UInt32Le(0x0208), + wszTempDir: new type.BinaryString(Buffer.from('D:\\Vectors' + Array(251).join('\x00'), 'ucs2'), { readLength : new type.CallableValue(520)}) + })); + } + + + /** + * Send format list PDU + */ + sendFormatListPDU() { + this.send(new type.Component({ + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST), + msgFlags: new type.UInt16Le(0x00), + + dataLen: new type.UInt32Le(0x24), + + formatId6: new type.UInt32Le(0xc004), + formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}), + + formatId8: new type.UInt32Le(0x0d), + formatName8: new type.UInt16Le(0x00), + + formatId9: new type.UInt32Le(0x10), + formatName9: new type.UInt16Le(0x00), + + formatId0: new type.UInt32Le(0x01), + formatName0: new type.UInt16Le(0x00), + + // dataLen: new type.UInt32Le(0xe0), + + // formatId1: new type.UInt32Le(0xc08a), + // formatName1: new type.BinaryString(Buffer.from('Rich Text Format\x00' , 'ucs2'), { readLength : new type.CallableValue(34)}), + + // formatId2: new type.UInt32Le(0xc145), + // formatName2: new type.BinaryString(Buffer.from('Rich Text Format Without Objects\x00' , 'ucs2'), { readLength : new type.CallableValue(66)}), + + // formatId3: new type.UInt32Le(0xc143), + // formatName3: new type.BinaryString(Buffer.from('RTF As Text\x00' , 'ucs2'), { readLength : new type.CallableValue(24)}), + + // formatId4: new type.UInt32Le(0x01), + // formatName4: new type.BinaryString(0x00), + + formatId5: new type.UInt32Le(0x07), + formatName5: new type.UInt16Le(0x00), + + // formatId6: new type.UInt32Le(0xc004), + // formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}), + + // formatId7: new type.UInt32Le(0xc00e), + // formatName7: new type.BinaryString(Buffer.from('Object Descriptor\x00' , 'ucs2'), { readLength : new type.CallableValue(36)}), + + // formatId8: new type.UInt32Le(0x03), + // formatName8: new type.UInt16Le(0x00), + + // formatId9: new type.UInt32Le(0x10), + // formatName9: new type.UInt16Le(0x00), + + // formatId0: new type.UInt32Le(0x07), + // formatName0: new type.UInt16Le(0x00), + })); + + } + + /** + * Recvie format list PDU from server + * @param {type.Stream} s + */ + recvFormatListPDU(s) { + s.offset = 18; + // const pdu = data.clipPDU().read(s); + // console.log('recvFormatListPDU', s); + this.sendFormatListResponsePDU(); + } + + + /** + * Send format list reesponse + */ + sendFormatListResponsePDU() { + this.send(new type.Component({ + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST_RESPONSE), + msgFlags: new type.UInt16Le(0x01), + dataLen: new type.UInt32Le(0x00), + })); + + this.sendFormatDataRequestPDU(); + } + + + /** + * Receive format list response from server + * @param s {type.Stream} + */ + recvFormatListResponsePDU(s) { + s.offset = 18; + // const pdu = data.clipPDU().read(s); + // console.log('recvFormatListResponsePDU', s); + // this.sendFormatDataRequestPDU(); + } + + + /** + * Send format data request PDU + */ + sendFormatDataRequestPDU(formartId = 0x0d) { + this.send(new type.Component({ + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_REQUEST), + msgFlags: new type.UInt16Le(0x00), + dataLen: new type.UInt32Le(0x04), + requestedFormatId: new type.UInt32Le(formartId), + })); + } + + + /** + * Receive format data request PDU from server + * @param s {type.Stream} + */ + recvFormatDataRequestPDU(s) { + s.offset = 18; + // const pdu = data.clipPDU().read(s); + // console.log('recvFormatDataRequestPDU', s); + this.sendFormatDataResponsePDU(); + } + + + /** + * Send format data reesponse PDU + */ + sendFormatDataResponsePDU() { + + const bufs = Buffer.from(this.content + '\x00' , 'ucs2'); + + this.send(new type.Component({ + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_RESPONSE), + msgFlags: new type.UInt16Le(0x01), + dataLen: new type.UInt32Le(bufs.length), + requestedFormatData: new type.BinaryString(bufs, { readLength : new type.CallableValue(bufs.length)}) + })); + + } + + + /** + * Receive format data response PDU from server + * @param s {type.Stream} + */ + recvFormatDataResponsePDU(s) { + s.offset = 18; + // const pdu = data.clipPDU().read(s); + const str = s.buffer.toString('ucs2', 26, s.buffer.length-2); + // console.log('recvFormatDataResponsePDU', str); + this.content = str; + this.emit('clipboard', str) + } + + +// ===================================================================================== + setClipboardData(content) { + this.content = content; + this.sendFormatListPDU(); + } + +} + + +module.exports = { + Client +} diff --git a/rdp/protocol/pdu/data.js b/rdp/protocol/pdu/data.js index 58b2fb42..7c79012d 100644 --- a/rdp/protocol/pdu/data.js +++ b/rdp/protocol/pdu/data.js @@ -1040,6 +1040,36 @@ function pdu(userId, pduMessage, opt) { return new type.Component(self, opt); } + +const ClipPDUMsgType = { + CB_MONITOR_READY: 0x0001, + CB_FORMAT_LIST: 0x0002, + CB_FORMAT_LIST_RESPONSE: 0x0003, + CB_FORMAT_DATA_REQUEST: 0x0004, + CB_FORMAT_DATA_RESPONSE: 0x0005, + CB_TEMP_DIRECTORY: 0x0006, + CB_CLIP_CAPS: 0x0007, + CB_FILECONTENTS_REQUEST: 0x0008 +} + +/** + * @returns {type.Component} + */ +function clipPDU() { + const self = { + header: new type.Factory(function (s) { + self.header = new type.Component({ + msgType: new type.UInt16Le().read(s), + msgFlags: new type.UInt16Le().read(s), + dataLen: new type.UInt32Le().read(s) + }) + }) + + } + return new type.Component(self); +} + + /** * @see http://msdn.microsoft.com/en-us/library/dd306368.aspx * @param opt {object} type option @@ -1147,5 +1177,7 @@ module.exports = { updateDataPDU : updateDataPDU, dataPDU : dataPDU, fastPathBitmapUpdateDataPDU : fastPathBitmapUpdateDataPDU, - fastPathUpdatePDU : fastPathUpdatePDU + fastPathUpdatePDU: fastPathUpdatePDU, + clipPDU: clipPDU, + ClipPDUMsgType: ClipPDUMsgType }; \ No newline at end of file diff --git a/rdp/protocol/pdu/index.js b/rdp/protocol/pdu/index.js index dd714709..6afbeccd 100644 --- a/rdp/protocol/pdu/index.js +++ b/rdp/protocol/pdu/index.js @@ -21,10 +21,12 @@ var lic = require('./lic'); var sec = require('./sec'); var global = require('./global'); var data = require('./data'); +var cliprdr = require('./cliprdr'); module.exports = { - lic : lic, - sec : sec, - global : global, - data : data + lic: lic, + sec: sec, + global: global, + data: data, + cliprdr: cliprdr }; diff --git a/rdp/protocol/rdp.js b/rdp/protocol/rdp.js index f9fac6f8..97cb25a6 100644 --- a/rdp/protocol/rdp.js +++ b/rdp/protocol/rdp.js @@ -87,6 +87,7 @@ function RdpClient(config) { this.x224 = new x224.Client(this.tpkt, config); this.mcs = new t125.mcs.Client(this.x224); this.sec = new pdu.sec.Client(this.mcs, this.tpkt); + this.cliprdr = new pdu.cliprdr.Client(this.mcs); this.global = new pdu.global.Client(this.sec, this.sec); // config log level @@ -145,6 +146,9 @@ function RdpClient(config) { this.mcs.clientCoreData.obj.kbdLayout.value = t125.gcc.KeyboardLayout.US; } + this.cliprdr.on('clipboard', (content) => { + this.emit('clipboard', content) + }); //bind all events var self = this; @@ -328,6 +332,14 @@ RdpClient.prototype.sendWheelEvent = function (x, y, step, isNegative, isHorizon this.global.sendInputEvents([event]); } +/** + * Clipboard event + * @param data {String} content for clipboard + */ +RdpClient.prototype.setClipboardData = function (content) { + this.cliprdr.setClipboardData(content); +} + function createClient(config) { return new RdpClient(config); }; diff --git a/rdp/protocol/t125/mcs.js b/rdp/protocol/t125/mcs.js index 1077a2a8..338aae01 100644 --- a/rdp/protocol/t125/mcs.js +++ b/rdp/protocol/t125/mcs.js @@ -25,6 +25,7 @@ var error = require('../../core').error; var gcc = require('./gcc'); var per = require('./per'); var asn1 = require('../../asn1'); +var cliprdr = require('../pdu/cliprdr'); var Message = { MCS_TYPE_CONNECT_INITIAL : 0x65, @@ -43,10 +44,33 @@ var DomainMCSPDU = { }; var Channel = { - MCS_GLOBAL_CHANNEL : 1003, - MCS_USERCHANNEL_BASE : 1001 + MCS_GLOBAL_CHANNEL: 1003, + MCS_USERCHANNEL_BASE: 1001, + MCS_CLIPRDR_CHANNEL: 1005 }; +/** + * Channel Definde + */ +const RdpdrChannelDef = new type.Component({ + name: new type.BinaryString(Buffer.from('rdpdr' + '\x00\x00\x00', 'binary'), { readLength: new type.CallableValue(8) }), + options: new type.UInt32Le(0x80800000) +}); + +const RdpsndChannelDef = new type.Component({ + name: new type.BinaryString(Buffer.from('rdpsnd' + '\x00\x00', 'binary'), { readLength: new type.CallableValue(8) }), + options: new type.UInt32Le(0xc0000000) +}); + +const CliprdrChannelDef = new type.Component({ + name: new type.BinaryString(Buffer.from('cliprdr' + '\x00', 'binary'), { readLength: new type.CallableValue(8) }), + // CHANNEL_OPTION_INITIALIZED | + // CHANNEL_OPTION_ENCRYPT_RDP | + // CHANNEL_OPTION_COMPRESS_RDP | + // CHANNEL_OPTION_SHOW_PROTOCOL + options: new type.UInt32Le(0xc0a00000) +}); + /** * @see http://www.itu.int/rec/T-REC-T.125-199802-I/en page 25 * @returns {asn1.univ.Sequence} @@ -126,7 +150,10 @@ function MCS(transport, recvOpCode, sendOpCode) { this.transport = transport; this.recvOpCode = recvOpCode; this.sendOpCode = sendOpCode; - this.channels = [{id : Channel.MCS_GLOBAL_CHANNEL, name : 'global'}]; + this.channels = [ + { id: Channel.MCS_GLOBAL_CHANNEL, name: 'global' }, + { id: Channel.MCS_CLIPRDR_CHANNEL, name: 'cliprdr' } + ]; this.channels.find = function(callback) { for(var i in this) { if(callback(this[i])) return this[i]; @@ -207,8 +234,9 @@ function Client(transport) { this.channelsConnected = 0; // init gcc information - this.clientCoreData = gcc.clientCoreData(); - this.clientNetworkData = gcc.clientNetworkData(new type.Component([])); + this.clientCoreData = gcc.clientCoreData(); + // cliprdr channel + this.clientNetworkData = gcc.clientNetworkData(new type.Component([RdpdrChannelDef, CliprdrChannelDef, RdpsndChannelDef])); this.clientSecurityData = gcc.clientSecurityData(); // must be readed from protocol @@ -317,7 +345,8 @@ Client.prototype.recvChannelJoinConfirm = function(s) { var channelId = per.readInteger16(s); - if ((confirm !== 0) && (channelId === Channel.MCS_GLOBAL_CHANNEL || channelId === this.userId)) { + //if ((confirm !== 0) && (channelId === Channel.MCS_GLOBAL_CHANNEL || channelId === this.userId)) { + if ((confirm !== 0) && (channelId === Channel.MCS_CLIPRDR_CHANNEL || channelId === Channel.MCS_GLOBAL_CHANNEL || channelId === this.userId)) { throw new error.UnexpectedFatalError('NODE_RDP_PROTOCOL_T125_MCS_SERVER_MUST_CONFIRM_STATIC_CHANNEL'); } diff --git a/rdp/security/md4.js b/rdp/security/md4.js index 38c0ba9b..ae9ab1e4 100644 --- a/rdp/security/md4.js +++ b/rdp/security/md4.js @@ -121,7 +121,7 @@ } else if (message.length === undefined) { return method(message); } - return crypto.createHash('md4').update(new Buffer(message)).digest('hex'); + return crypto.createHash('md4').update(Buffer.from(message)).digest('hex'); }; return nodeMethod; }; diff --git a/views/default.handlebars b/views/default.handlebars index 64699de7..b62b6083 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -7566,7 +7566,7 @@ if ((navigator.clipboard != null) && (navigator.clipboard.readText != null)) { try { navigator.clipboard.readText().then(function(text) { - meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); + if (desktop.m.setClipboard) { desktop.m.setClipboard(text); } else { meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); } }).catch(function(err) { console.log(err); }); } catch (ex) { console.log(ex); } } @@ -8332,7 +8332,8 @@ var hwonline = ((currentNode.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable hardware terminal QE('connectbutton1h', hwonline); QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (deskState != 0) && (desktopsettings.showfocus)); - QE('DeskClip', (deskState == 3) && (desktop.contype != 4)); + QE('DeskClip', deskState == 3); + //QE('DeskClip', (deskState == 3) && (desktop.contype != 4)); QV('DeskClip', (inputAllowed) && (currentNode.agent) && ((features2 & 0x1800) != 0x1800) && (currentNode.agent.id != 11) && (currentNode.agent.id != 16) && ((desktop == null) || (desktop.contype != 2)) && ((desktopsettings.autoclipboard != true) || (navigator.clipboard == null) || (navigator.clipboard.readText == null))); // Clipboard not supported on macOS QE('DeskESC', (deskState == 3) && (desktop.contype != 4)); QV('DeskESC', browserfullscreen && inputAllowed); @@ -8737,7 +8738,7 @@ try { navigator.clipboard.readText().then(function(text) { if ((text != null) && (deskLastClipboardSent != text)) { - meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); + if (desktop.m.setClipboard) { desktop.m.setClipboard(text); } else { meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); } deskLastClipboardSent = text; } }).catch(function(err) { }); @@ -9323,8 +9324,12 @@ function showDeskClipSet() { if (desktop == null || desktop.State != 3) return; - meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: Q('d2clipText').value }); - QV('linuxClipWarn', currentNode && currentNode.agent && (currentNode.agent.id > 4) && (currentNode.agent.id != 21) && (currentNode.agent.id != 22) && (currentNode.agent.id != 34)); + if (desktop.m.setClipboard) { + desktop.m.setClipboard(Q('d2clipText').value); + } else { + meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: Q('d2clipText').value }); + QV('linuxClipWarn', currentNode && currentNode.agent && (currentNode.agent.id > 4) && (currentNode.agent.id != 21) && (currentNode.agent.id != 22) && (currentNode.agent.id != 34)); + } } // Send CTRL-ALT-DEL