First pass at adding RDP clipboard support, #3810.

This commit is contained in:
Ylian Saint-Hilaire 2022-05-14 23:00:57 -07:00
parent 97ff7a1669
commit af052ddfe7
10 changed files with 446 additions and 21 deletions

View File

@ -198,6 +198,7 @@
<Compile Include="rdp\protocol\index.js" />
<Compile Include="rdp\protocol\nla.js" />
<Compile Include="rdp\protocol\pdu\caps.js" />
<Compile Include="rdp\protocol\pdu\cliprdr.js" />
<Compile Include="rdp\protocol\pdu\data.js" />
<Compile Include="rdp\protocol\pdu\global.js" />
<Compile Include="rdp\protocol\pdu\index.js" />

View File

@ -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];

View File

@ -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; }
}
@ -100,6 +104,14 @@ var CreateRDPDesktop = function (canvasid) {
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;

327
rdp/protocol/pdu/cliprdr.js Normal file
View File

@ -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
}

View File

@ -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
};

View File

@ -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
};

View File

@ -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);
};

View File

@ -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');
}

View File

@ -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;
};

View File

@ -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