First pass at adding RDP clipboard support, #3810.

Ylian Saint-Hilaire committed May 14, 2022 at 23:00 UTC af052ddfe7bfde2c8d1286831a27c2e495566bc0
10 files changed +446 -21
MeshCentralServer.njsproj
+1
@@ -198,6 +198,7 @@
198 <Compile Include="rdp\protocol\index.js" />
199 <Compile Include="rdp\protocol\nla.js" />
200 <Compile Include="rdp\protocol\pdu\caps.js" />
201 + <Compile Include="rdp\protocol\pdu\cliprdr.js" />
202 <Compile Include="rdp\protocol\pdu\data.js" />
203 <Compile Include="rdp\protocol\pdu\global.js" />
204 <Compile Include="rdp\protocol\pdu\index.js" />
apprelays.js
+6 -1
@@ -152,7 +152,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
152 obj.wsClient._socket.pause();
153 try {
154 obj.relaySocket.write(data, function () {
155 - try { obj.wsClient._socket.resume(); } catch (ex) { console.log(ex); }
155 + if (obj.wsClient && obj.wsClient._socket) { try { obj.wsClient._socket.resume(); } catch (ex) { console.log(ex); } }
156 });
157 } catch (ex) { console.log(ex); obj.close(); }
158 }
@@ -201,6 +201,10 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
201 try { ws.send(bitmap.data); } catch (ex) { } // Send the bitmap data as binary
202 delete bitmap.data;
203 send(['rdp-bitmap', bitmap]); // Send the bitmap metadata seperately, without bitmap data.
204 + }).on('clipboard', function (content) {
205 + // Clipboard data changed
206 + console.log('RDP clipboard recv', content);
207 + send(['rdp-clipboard', content]);
208 }).on('close', function () {
209 send(['rdp-close']);
210 }).on('error', function (err) {
@@ -317,6 +321,7 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
321 }
322 case 'mouse': { if (rdpClient && (obj.viewonly != true)) { rdpClient.sendPointerEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
323 case 'wheel': { if (rdpClient && (obj.viewonly != true)) { rdpClient.sendWheelEvent(msg[1], msg[2], msg[3], msg[4]); } break; }
324 + case 'clipboard': { rdpClient.setClipboardData(msg[1]); break; }
325 case 'scancode': {
326 if (obj.limitedinput == true) { // Limit keyboard input
327 var ok = false, k = msg[1];
public/scripts/agent-rdp-0.0.1.js
+15 -3
@@ -83,6 +83,10 @@ var CreateRDPDesktop = function (canvasid) {
83 obj.Stop();
84 break;
85 }
86 + case 'rdp-clipboard': {
87 + console.log('clipboard', msg[1]);
88 + break;
89 + }
90 case 'ping': { obj.socket.send('["pong"]'); break; }
91 case 'pong': { break; }
92 }
@@ -99,7 +103,15 @@ var CreateRDPDesktop = function (canvasid) {
103 obj.Canvas.fillRect(0, 0, obj.ScreenWidth, obj.ScreenHeight);
104 if (obj.socket) { obj.socket.close(); }
105 }
102 -
106 +
107 + obj.m.setClipboard = function (content) {
108 + console.log('s1');
109 + if (obj.socket) {
110 + console.log('s2', content);
111 + obj.socket.send(JSON.stringify(['clipboard', content]));
112 + }
113 + }
114 +
115 function changeState(newstate) {
116 if (obj.State == newstate) return;
117 obj.State = newstate;
@@ -153,14 +165,14 @@ var CreateRDPDesktop = function (canvasid) {
165 }
166 obj.m.handleKeyUp = function (e) {
167 if (!obj.socket || (obj.State != 3)) return;
156 - console.log('handleKeyUp', Mstsc.scancode(e));
168 + //console.log('handleKeyUp', Mstsc.scancode(e));
169 obj.socket.send(JSON.stringify(['scancode', Mstsc.scancode(e), false]));
170 e.preventDefault();
171 return false;
172 }
173 obj.m.handleKeyDown = function (e) {
174 if (!obj.socket || (obj.State != 3)) return;
163 - console.log('handleKeyDown', Mstsc.scancode(e));
175 + //console.log('handleKeyDown', Mstsc.scancode(e));
176 obj.socket.send(JSON.stringify(['scancode', Mstsc.scancode(e), true]));
177 e.preventDefault();
178 return false;
rdp/protocol/pdu/cliprdr.js new
+327
@@ -0,0 +1,327 @@
1 +const type = require('../../core').type;
2 +const EventEmitter = require('events').EventEmitter;
3 +const caps = require('./caps');
4 +const log = require('../../core').log;
5 +const data = require('./data');
6 +
7 +
8 +
9 +/**
10 + * Cliprdr channel for all clipboard
11 + * capabilities exchange
12 + */
13 +class Cliprdr extends EventEmitter {
14 +
15 + constructor(transport) {
16 + super();
17 + this.transport = transport;
18 + // must be init via connect event
19 + this.userId = 0;
20 + this.serverCapabilities = [];
21 + this.clientCapabilities = [];
22 + }
23 +
24 +}
25 +
26 +
27 +/**
28 + * Client side of Cliprdr channel automata
29 + * @param transport
30 + */
31 +class Client extends Cliprdr {
32 +
33 + constructor(transport, fastPathTransport) {
34 +
35 + super(transport, fastPathTransport);
36 +
37 + this.transport.once('connect', (gccCore, userId, channelId) => {
38 + this.connect(gccCore, userId, channelId);
39 + }).on('close', () => {
40 + this.emit('close');
41 + }).on('error', (err) => {
42 + this.emit('error', err);
43 + });
44 +
45 + this.content = '';
46 +
47 + }
48 +
49 + /**
50 + * connect function
51 + * @param gccCore {type.Component(clientCoreData)}
52 + */
53 + connect(gccCore, userId, channelId) {
54 + this.gccCore = gccCore;
55 + this.userId = userId;
56 + this.channelId = channelId;
57 + this.transport.once('cliprdr', (s) => {
58 + this.recv(s);
59 + });
60 + }
61 +
62 +
63 + send(message) {
64 + this.transport.send('cliprdr', new type.Component([
65 + // Channel PDU Header
66 + new type.UInt32Le(message.size()),
67 + // CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST | CHANNEL_FLAG_SHOW_PROTOCOL
68 + new type.UInt32Le(0x13),
69 + message
70 + ]));
71 + };
72 +
73 + recv(s) {
74 + s.offset = 18;
75 + const pdu = data.clipPDU().read(s), type = data.ClipPDUMsgType;
76 +
77 + switch (pdu.obj.header.obj.msgType.value) {
78 + case type.CB_MONITOR_READY:
79 + this.recvMonitorReadyPDU(s);
80 + break;
81 + case type.CB_FORMAT_LIST:
82 + this.recvFormatListPDU(s);
83 + break;
84 + case type.CB_FORMAT_LIST_RESPONSE:
85 + this.recvFormatListResponsePDU(s);
86 + break;
87 + case type.CB_FORMAT_DATA_REQUEST:
88 + this.recvFormatDataRequestPDU(s);
89 + break;
90 + case type.CB_FORMAT_DATA_RESPONSE:
91 + this.recvFormatDataResponsePDU(s);
92 + break;
93 + case type.CB_TEMP_DIRECTORY:
94 + break;
95 + case type.CB_CLIP_CAPS:
96 + this.recvClipboardCapsPDU(s);
97 + break;
98 + case type.CB_FILECONTENTS_REQUEST:
99 + }
100 +
101 + this.transport.once('cliprdr', (s) => {
102 + this.recv(s);
103 + });
104 + }
105 +
106 + /**
107 + * Receive capabilities from server
108 + * @param s {type.Stream}
109 + */
110 + recvClipboardCapsPDU(s) {
111 + // Start at 18
112 + s.offset = 18;
113 + // const pdu = data.clipPDU().read(s);
114 + // console.log('recvClipboardCapsPDU', s);
115 + }
116 +
117 +
118 + /**
119 + * Receive monitor ready from server
120 + * @param s {type.Stream}
121 + */
122 + recvMonitorReadyPDU(s) {
123 + s.offset = 18;
124 + // const pdu = data.clipPDU().read(s);
125 + // console.log('recvMonitorReadyPDU', s);
126 +
127 + this.sendClipboardCapsPDU();
128 + // this.sendClientTemporaryDirectoryPDU();
129 + this.sendFormatListPDU();
130 + }
131 +
132 +
133 + /**
134 + * Send clipboard capabilities PDU
135 + */
136 + sendClipboardCapsPDU() {
137 + this.send(new type.Component({
138 + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_CLIP_CAPS),
139 + msgFlags: new type.UInt16Le(0x00),
140 + dataLen: new type.UInt32Le(0x10),
141 + cCapabilitiesSets: new type.UInt16Le(0x01),
142 + pad1: new type.UInt16Le(0x00),
143 + capabilitySetType: new type.UInt16Le(0x01),
144 + lengthCapability: new type.UInt16Le(0x0c),
145 + version: new type.UInt32Le(0x02),
146 + capabilityFlags: new type.UInt32Le(0x02)
147 + }));
148 + }
149 +
150 +
151 + /**
152 + * Send client temporary directory PDU
153 + */
154 + sendClientTemporaryDirectoryPDU(path = '') {
155 + // TODO
156 + this.send(new type.Component({
157 + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_TEMP_DIRECTORY),
158 + msgFlags: new type.UInt16Le(0x00),
159 + dataLen: new type.UInt32Le(0x0208),
160 + wszTempDir: new type.BinaryString(Buffer.from('D:\\Vectors' + Array(251).join('\x00'), 'ucs2'), { readLength : new type.CallableValue(520)})
161 + }));
162 + }
163 +
164 +
165 + /**
166 + * Send format list PDU
167 + */
168 + sendFormatListPDU() {
169 + this.send(new type.Component({
170 + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST),
171 + msgFlags: new type.UInt16Le(0x00),
172 +
173 + dataLen: new type.UInt32Le(0x24),
174 +
175 + formatId6: new type.UInt32Le(0xc004),
176 + formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}),
177 +
178 + formatId8: new type.UInt32Le(0x0d),
179 + formatName8: new type.UInt16Le(0x00),
180 +
181 + formatId9: new type.UInt32Le(0x10),
182 + formatName9: new type.UInt16Le(0x00),
183 +
184 + formatId0: new type.UInt32Le(0x01),
185 + formatName0: new type.UInt16Le(0x00),
186 +
187 + // dataLen: new type.UInt32Le(0xe0),
188 +
189 + // formatId1: new type.UInt32Le(0xc08a),
190 + // formatName1: new type.BinaryString(Buffer.from('Rich Text Format\x00' , 'ucs2'), { readLength : new type.CallableValue(34)}),
191 +
192 + // formatId2: new type.UInt32Le(0xc145),
193 + // formatName2: new type.BinaryString(Buffer.from('Rich Text Format Without Objects\x00' , 'ucs2'), { readLength : new type.CallableValue(66)}),
194 +
195 + // formatId3: new type.UInt32Le(0xc143),
196 + // formatName3: new type.BinaryString(Buffer.from('RTF As Text\x00' , 'ucs2'), { readLength : new type.CallableValue(24)}),
197 +
198 + // formatId4: new type.UInt32Le(0x01),
199 + // formatName4: new type.BinaryString(0x00),
200 +
201 + formatId5: new type.UInt32Le(0x07),
202 + formatName5: new type.UInt16Le(0x00),
203 +
204 + // formatId6: new type.UInt32Le(0xc004),
205 + // formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}),
206 +
207 + // formatId7: new type.UInt32Le(0xc00e),
208 + // formatName7: new type.BinaryString(Buffer.from('Object Descriptor\x00' , 'ucs2'), { readLength : new type.CallableValue(36)}),
209 +
210 + // formatId8: new type.UInt32Le(0x03),
211 + // formatName8: new type.UInt16Le(0x00),
212 +
213 + // formatId9: new type.UInt32Le(0x10),
214 + // formatName9: new type.UInt16Le(0x00),
215 +
216 + // formatId0: new type.UInt32Le(0x07),
217 + // formatName0: new type.UInt16Le(0x00),
218 + }));
219 +
220 + }
221 +
222 + /**
223 + * Recvie format list PDU from server
224 + * @param {type.Stream} s
225 + */
226 + recvFormatListPDU(s) {
227 + s.offset = 18;
228 + // const pdu = data.clipPDU().read(s);
229 + // console.log('recvFormatListPDU', s);
230 + this.sendFormatListResponsePDU();
231 + }
232 +
233 +
234 + /**
235 + * Send format list reesponse
236 + */
237 + sendFormatListResponsePDU() {
238 + this.send(new type.Component({
239 + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST_RESPONSE),
240 + msgFlags: new type.UInt16Le(0x01),
241 + dataLen: new type.UInt32Le(0x00),
242 + }));
243 +
244 + this.sendFormatDataRequestPDU();
245 + }
246 +
247 +
248 + /**
249 + * Receive format list response from server
250 + * @param s {type.Stream}
251 + */
252 + recvFormatListResponsePDU(s) {
253 + s.offset = 18;
254 + // const pdu = data.clipPDU().read(s);
255 + // console.log('recvFormatListResponsePDU', s);
256 + // this.sendFormatDataRequestPDU();
257 + }
258 +
259 +
260 + /**
261 + * Send format data request PDU
262 + */
263 + sendFormatDataRequestPDU(formartId = 0x0d) {
264 + this.send(new type.Component({
265 + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_REQUEST),
266 + msgFlags: new type.UInt16Le(0x00),
267 + dataLen: new type.UInt32Le(0x04),
268 + requestedFormatId: new type.UInt32Le(formartId),
269 + }));
270 + }
271 +
272 +
273 + /**
274 + * Receive format data request PDU from server
275 + * @param s {type.Stream}
276 + */
277 + recvFormatDataRequestPDU(s) {
278 + s.offset = 18;
279 + // const pdu = data.clipPDU().read(s);
280 + // console.log('recvFormatDataRequestPDU', s);
281 + this.sendFormatDataResponsePDU();
282 + }
283 +
284 +
285 + /**
286 + * Send format data reesponse PDU
287 + */
288 + sendFormatDataResponsePDU() {
289 +
290 + const bufs = Buffer.from(this.content + '\x00' , 'ucs2');
291 +
292 + this.send(new type.Component({
293 + msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_RESPONSE),
294 + msgFlags: new type.UInt16Le(0x01),
295 + dataLen: new type.UInt32Le(bufs.length),
296 + requestedFormatData: new type.BinaryString(bufs, { readLength : new type.CallableValue(bufs.length)})
297 + }));
298 +
299 + }
300 +
301 +
302 + /**
303 + * Receive format data response PDU from server
304 + * @param s {type.Stream}
305 + */
306 + recvFormatDataResponsePDU(s) {
307 + s.offset = 18;
308 + // const pdu = data.clipPDU().read(s);
309 + const str = s.buffer.toString('ucs2', 26, s.buffer.length-2);
310 + // console.log('recvFormatDataResponsePDU', str);
311 + this.content = str;
312 + this.emit('clipboard', str)
313 + }
314 +
315 +
316 +// =====================================================================================
317 + setClipboardData(content) {
318 + this.content = content;
319 + this.sendFormatListPDU();
320 + }
321 +
322 +}
323 +
324 +
325 +module.exports = {
326 + Client
327 +}
rdp/protocol/pdu/data.js
+33 -1
@@ -1040,6 +1040,36 @@ function pdu(userId, pduMessage, opt) {
1040 return new type.Component(self, opt);
1041 }
1042
1043 +
1044 +const ClipPDUMsgType = {
1045 + CB_MONITOR_READY: 0x0001,
1046 + CB_FORMAT_LIST: 0x0002,
1047 + CB_FORMAT_LIST_RESPONSE: 0x0003,
1048 + CB_FORMAT_DATA_REQUEST: 0x0004,
1049 + CB_FORMAT_DATA_RESPONSE: 0x0005,
1050 + CB_TEMP_DIRECTORY: 0x0006,
1051 + CB_CLIP_CAPS: 0x0007,
1052 + CB_FILECONTENTS_REQUEST: 0x0008
1053 +}
1054 +
1055 +/**
1056 + * @returns {type.Component}
1057 + */
1058 +function clipPDU() {
1059 + const self = {
1060 + header: new type.Factory(function (s) {
1061 + self.header = new type.Component({
1062 + msgType: new type.UInt16Le().read(s),
1063 + msgFlags: new type.UInt16Le().read(s),
1064 + dataLen: new type.UInt32Le().read(s)
1065 + })
1066 + })
1067 +
1068 + }
1069 + return new type.Component(self);
1070 +}
1071 +
1072 +
1073 /**
1074 * @see http://msdn.microsoft.com/en-us/library/dd306368.aspx
1075 * @param opt {object} type option
@@ -1147,5 +1177,7 @@ module.exports = {
1177 updateDataPDU : updateDataPDU,
1178 dataPDU : dataPDU,
1179 fastPathBitmapUpdateDataPDU : fastPathBitmapUpdateDataPDU,
1150 - fastPathUpdatePDU : fastPathUpdatePDU
1180 + fastPathUpdatePDU: fastPathUpdatePDU,
1181 + clipPDU: clipPDU,
1182 + ClipPDUMsgType: ClipPDUMsgType
1183 };
\ No newline at end of file
rdp/protocol/pdu/index.js
+6 -4
@@ -21,10 +21,12 @@ var lic = require('./lic');
21 var sec = require('./sec');
22 var global = require('./global');
23 var data = require('./data');
24 +var cliprdr = require('./cliprdr');
25
26 module.exports = {
26 - lic : lic,
27 - sec : sec,
28 - global : global,
29 - data : data
27 + lic: lic,
28 + sec: sec,
29 + global: global,
30 + data: data,
31 + cliprdr: cliprdr
32 };
rdp/protocol/rdp.js
+12
@@ -87,6 +87,7 @@ function RdpClient(config) {
87 this.x224 = new x224.Client(this.tpkt, config);
88 this.mcs = new t125.mcs.Client(this.x224);
89 this.sec = new pdu.sec.Client(this.mcs, this.tpkt);
90 + this.cliprdr = new pdu.cliprdr.Client(this.mcs);
91 this.global = new pdu.global.Client(this.sec, this.sec);
92
93 // config log level
@@ -145,6 +146,9 @@ function RdpClient(config) {
146 this.mcs.clientCoreData.obj.kbdLayout.value = t125.gcc.KeyboardLayout.US;
147 }
148
149 + this.cliprdr.on('clipboard', (content) => {
150 + this.emit('clipboard', content)
151 + });
152
153 //bind all events
154 var self = this;
@@ -328,6 +332,14 @@ RdpClient.prototype.sendWheelEvent = function (x, y, step, isNegative, isHorizon
332 this.global.sendInputEvents([event]);
333 }
334
335 +/**
336 + * Clipboard event
337 + * @param data {String} content for clipboard
338 + */
339 +RdpClient.prototype.setClipboardData = function (content) {
340 + this.cliprdr.setClipboardData(content);
341 +}
342 +
343 function createClient(config) {
344 return new RdpClient(config);
345 };
rdp/protocol/t125/mcs.js
+35 -6
@@ -25,6 +25,7 @@ var error = require('../../core').error;
25 var gcc = require('./gcc');
26 var per = require('./per');
27 var asn1 = require('../../asn1');
28 +var cliprdr = require('../pdu/cliprdr');
29
30 var Message = {
31 MCS_TYPE_CONNECT_INITIAL : 0x65,
@@ -43,10 +44,33 @@ var DomainMCSPDU = {
44 };
45
46 var Channel = {
46 - MCS_GLOBAL_CHANNEL : 1003,
47 - MCS_USERCHANNEL_BASE : 1001
47 + MCS_GLOBAL_CHANNEL: 1003,
48 + MCS_USERCHANNEL_BASE: 1001,
49 + MCS_CLIPRDR_CHANNEL: 1005
50 };
51
52 +/**
53 + * Channel Definde
54 + */
55 +const RdpdrChannelDef = new type.Component({
56 + name: new type.BinaryString(Buffer.from('rdpdr' + '\x00\x00\x00', 'binary'), { readLength: new type.CallableValue(8) }),
57 + options: new type.UInt32Le(0x80800000)
58 +});
59 +
60 +const RdpsndChannelDef = new type.Component({
61 + name: new type.BinaryString(Buffer.from('rdpsnd' + '\x00\x00', 'binary'), { readLength: new type.CallableValue(8) }),
62 + options: new type.UInt32Le(0xc0000000)
63 +});
64 +
65 +const CliprdrChannelDef = new type.Component({
66 + name: new type.BinaryString(Buffer.from('cliprdr' + '\x00', 'binary'), { readLength: new type.CallableValue(8) }),
67 + // CHANNEL_OPTION_INITIALIZED |
68 + // CHANNEL_OPTION_ENCRYPT_RDP |
69 + // CHANNEL_OPTION_COMPRESS_RDP |
70 + // CHANNEL_OPTION_SHOW_PROTOCOL
71 + options: new type.UInt32Le(0xc0a00000)
72 +});
73 +
74 /**
75 * @see http://www.itu.int/rec/T-REC-T.125-199802-I/en page 25
76 * @returns {asn1.univ.Sequence}
@@ -126,7 +150,10 @@ function MCS(transport, recvOpCode, sendOpCode) {
150 this.transport = transport;
151 this.recvOpCode = recvOpCode;
152 this.sendOpCode = sendOpCode;
129 - this.channels = [{id : Channel.MCS_GLOBAL_CHANNEL, name : 'global'}];
153 + this.channels = [
154 + { id: Channel.MCS_GLOBAL_CHANNEL, name: 'global' },
155 + { id: Channel.MCS_CLIPRDR_CHANNEL, name: 'cliprdr' }
156 + ];
157 this.channels.find = function(callback) {
158 for(var i in this) {
159 if(callback(this[i])) return this[i];
@@ -207,8 +234,9 @@ function Client(transport) {
234 this.channelsConnected = 0;
235
236 // init gcc information
210 - this.clientCoreData = gcc.clientCoreData();
211 - this.clientNetworkData = gcc.clientNetworkData(new type.Component([]));
237 + this.clientCoreData = gcc.clientCoreData();
238 + // cliprdr channel
239 + this.clientNetworkData = gcc.clientNetworkData(new type.Component([RdpdrChannelDef, CliprdrChannelDef, RdpsndChannelDef]));
240 this.clientSecurityData = gcc.clientSecurityData();
241
242 // must be readed from protocol
@@ -317,7 +345,8 @@ Client.prototype.recvChannelJoinConfirm = function(s) {
345
346 var channelId = per.readInteger16(s);
347
320 - if ((confirm !== 0) && (channelId === Channel.MCS_GLOBAL_CHANNEL || channelId === this.userId)) {
348 + //if ((confirm !== 0) && (channelId === Channel.MCS_GLOBAL_CHANNEL || channelId === this.userId)) {
349 + if ((confirm !== 0) && (channelId === Channel.MCS_CLIPRDR_CHANNEL || channelId === Channel.MCS_GLOBAL_CHANNEL || channelId === this.userId)) {
350 throw new error.UnexpectedFatalError('NODE_RDP_PROTOCOL_T125_MCS_SERVER_MUST_CONFIRM_STATIC_CHANNEL');
351 }
352
rdp/security/md4.js
+1 -1
@@ -121,7 +121,7 @@
121 } else if (message.length === undefined) {
122 return method(message);
123 }
124 - return crypto.createHash('md4').update(new Buffer(message)).digest('hex');
124 + return crypto.createHash('md4').update(Buffer.from(message)).digest('hex');
125 };
126 return nodeMethod;
127 };
views/default.handlebars
+10 -5
@@ -7566,7 +7566,7 @@
7566 if ((navigator.clipboard != null) && (navigator.clipboard.readText != null)) {
7567 try {
7568 navigator.clipboard.readText().then(function(text) {
7569 - meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text });
7569 + if (desktop.m.setClipboard) { desktop.m.setClipboard(text); } else { meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); }
7570 }).catch(function(err) { console.log(err); });
7571 } catch (ex) { console.log(ex); }
7572 }
@@ -8332,7 +8332,8 @@
8332 var hwonline = ((currentNode.conn & 6) != 0); // If CIRA (2) or AMT (4) connected, enable hardware terminal
8333 QE('connectbutton1h', hwonline);
8334 QV('deskFocusBtn', (desktop != null) && (desktop.contype == 2) && (deskState != 0) && (desktopsettings.showfocus));
8335 - QE('DeskClip', (deskState == 3) && (desktop.contype != 4));
8335 + QE('DeskClip', deskState == 3);
8336 + //QE('DeskClip', (deskState == 3) && (desktop.contype != 4));
8337 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
8338 QE('DeskESC', (deskState == 3) && (desktop.contype != 4));
8339 QV('DeskESC', browserfullscreen && inputAllowed);
@@ -8737,7 +8738,7 @@
8738 try {
8739 navigator.clipboard.readText().then(function(text) {
8740 if ((text != null) && (deskLastClipboardSent != text)) {
8740 - meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text });
8741 + if (desktop.m.setClipboard) { desktop.m.setClipboard(text); } else { meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: text }); }
8742 deskLastClipboardSent = text;
8743 }
8744 }).catch(function(err) { });
@@ -9323,8 +9324,12 @@
9324
9325 function showDeskClipSet() {
9326 if (desktop == null || desktop.State != 3) return;
9326 - meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: Q('d2clipText').value });
9327 - QV('linuxClipWarn', currentNode && currentNode.agent && (currentNode.agent.id > 4) && (currentNode.agent.id != 21) && (currentNode.agent.id != 22) && (currentNode.agent.id != 34));
9327 + if (desktop.m.setClipboard) {
9328 + desktop.m.setClipboard(Q('d2clipText').value);
9329 + } else {
9330 + meshserver.send({ action: 'msg', type: 'setclip', nodeid: currentNode._id, data: Q('d2clipText').value });
9331 + QV('linuxClipWarn', currentNode && currentNode.agent && (currentNode.agent.id > 4) && (currentNode.agent.id != 21) && (currentNode.agent.id != 22) && (currentNode.agent.id != 34));
9332 + }
9333 }
9334
9335 // Send CTRL-ALT-DEL