fix rdp clipboard with long text #7728

Signed-off-by: si458 <simonsmith5521@gmail.com>

si458 committed Apr 4, 2026 at 17:16 UTC 0aee9710d9fe3d0ad439d9eff71d2d2737675cd9
1 file changed +107 -28
rdp/protocol/pdu/cliprdr.js
+107 -28
@@ -4,6 +4,11 @@ const caps = require('./caps');
4 const log = require('../../core').log;
5 const data = require('./data');
6
7 +// RDP virtual channel constants (MS-RDPBCGR 3.1.5.2)
8 +const CHANNEL_CHUNK_LENGTH = 1600;
9 +const CHANNEL_FLAG_FIRST = 0x0001;
10 +const CHANNEL_FLAG_LAST = 0x0002;
11 +const CHANNEL_FLAG_SHOW_PROTOCOL = 0x0010;
12
13
14 /**
@@ -54,48 +59,107 @@ class Client extends Cliprdr {
59 this.gccCore = gccCore;
60 this.userId = userId;
61 this.channelId = channelId;
62 + this._fragmentBuffer = null;
63 + this._fragmentMsgType = null;
64 this.transport.once('cliprdr', (s) => {
65 this.recv(s);
66 });
67 }
68
69
70 + /**
71 + * Send a CLIPRDR message, fragmenting into channel chunks if necessary.
72 + * Per MS-RDPBCGR 3.1.5.2, each virtual channel chunk must be <= CHANNEL_CHUNK_LENGTH bytes,
73 + * and the Channel PDU Header flags must reflect fragment position.
74 + */
75 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 - };
76 + const msgBuf = message.toStream().buffer;
77 + const totalLength = msgBuf.length;
78 +
79 + let offset = 0;
80 + while (offset < totalLength) {
81 + const chunkSize = Math.min(CHANNEL_CHUNK_LENGTH, totalLength - offset);
82 + const chunk = msgBuf.slice(offset, offset + chunkSize);
83 +
84 + let flags = CHANNEL_FLAG_SHOW_PROTOCOL;
85 + if (offset === 0) flags |= CHANNEL_FLAG_FIRST;
86 + if (offset + chunkSize >= totalLength) flags |= CHANNEL_FLAG_LAST;
87 +
88 + // Channel PDU Header: totalLength field is always the uncompressed total across all fragments
89 + this.transport.send('cliprdr', new type.Component([
90 + new type.UInt32Le(totalLength),
91 + new type.UInt32Le(flags),
92 + new type.BinaryString(chunk),
93 + ]));
94 +
95 + offset += chunkSize;
96 + }
97 + }
98
99 + /**
100 + * Receive a virtual channel PDU.
101 + * Reads the Channel PDU Header at the current stream offset (not a hardcoded position),
102 + * handles multi-fragment reassembly, then dispatches to the appropriate handler.
103 + */
104 recv(s) {
74 - s.offset = 18;
75 - const pdu = data.clipPDU().read(s), type = data.ClipPDUMsgType;
105 + // Read Channel PDU Header at the current stream position.
106 + // Do NOT hardcode s.offset — the MCS per.readLength encoding is 1 byte for payloads
107 + // < 128 bytes and 2 bytes for larger ones, so the stream offset varies by packet size.
108 + const channelTotalLen = new type.UInt32Le().read(s).value; // eslint-disable-line no-unused-vars
109 + const channelFlags = new type.UInt32Le().read(s).value;
110 +
111 + const isFirst = !!(channelFlags & CHANNEL_FLAG_FIRST);
112 + const isLast = !!(channelFlags & CHANNEL_FLAG_LAST);
113 +
114 + if (!isFirst) {
115 + // Middle or last fragment — accumulate payload data
116 + if (this._fragmentBuffer) {
117 + this._fragmentBuffer = Buffer.concat([this._fragmentBuffer, s.buffer.slice(s.offset)]);
118 + }
119 + if (isLast) {
120 + this._dispatchFragment();
121 + }
122 + this.transport.once('cliprdr', (s) => { this.recv(s); });
123 + return;
124 + }
125 +
126 + // First (or only) fragment — parse the CLIPRDR PDU header
127 + const pdu = data.clipPDU().read(s);
128 + const clipType = data.ClipPDUMsgType;
129 + const msgType = pdu.obj.header.obj.msgType.value;
130 +
131 + if (!isLast) {
132 + // First of multiple fragments — begin reassembly; payload starts at current s.offset
133 + this._fragmentMsgType = msgType;
134 + this._fragmentBuffer = s.buffer.slice(s.offset);
135 + this.transport.once('cliprdr', (s) => { this.recv(s); });
136 + return;
137 + }
138
77 - switch (pdu.obj.header.obj.msgType.value) {
78 - case type.CB_MONITOR_READY:
139 + // Single complete packet — dispatch directly
140 + switch (msgType) {
141 + case clipType.CB_MONITOR_READY:
142 this.recvMonitorReadyPDU(s);
143 break;
81 - case type.CB_FORMAT_LIST:
144 + case clipType.CB_FORMAT_LIST:
145 this.recvFormatListPDU(s);
146 break;
84 - case type.CB_FORMAT_LIST_RESPONSE:
147 + case clipType.CB_FORMAT_LIST_RESPONSE:
148 this.recvFormatListResponsePDU(s);
149 break;
87 - case type.CB_FORMAT_DATA_REQUEST:
150 + case clipType.CB_FORMAT_DATA_REQUEST:
151 this.recvFormatDataRequestPDU(s);
152 break;
90 - case type.CB_FORMAT_DATA_RESPONSE:
153 + case clipType.CB_FORMAT_DATA_RESPONSE:
154 this.recvFormatDataResponsePDU(s);
155 break;
93 - case type.CB_TEMP_DIRECTORY:
156 + case clipType.CB_TEMP_DIRECTORY:
157 break;
95 - case type.CB_CLIP_CAPS:
158 + case clipType.CB_CLIP_CAPS:
159 this.recvClipboardCapsPDU(s);
160 break;
98 - case type.CB_FILECONTENTS_REQUEST:
161 + case clipType.CB_FILECONTENTS_REQUEST:
162 + break;
163 }
164
165 this.transport.once('cliprdr', (s) => {
@@ -103,13 +167,30 @@ class Client extends Cliprdr {
167 });
168 }
169
170 + /**
171 + * Dispatch a fully reassembled multi-fragment CLIPRDR message.
172 + * this._fragmentBuffer contains the raw payload bytes (no CLIPRDR header).
173 + */
174 + _dispatchFragment() {
175 + const buf = this._fragmentBuffer;
176 + const clipType = data.ClipPDUMsgType;
177 + this._fragmentBuffer = null;
178 +
179 + if (this._fragmentMsgType === clipType.CB_FORMAT_DATA_RESPONSE) {
180 + // buf is the UCS-2 encoded text with a null terminator; strip the terminator
181 + const str = buf.toString('ucs2', 0, buf.length - 2);
182 + this.content = str;
183 + this.emit('clipboard', str);
184 + }
185 +
186 + this._fragmentMsgType = null;
187 + }
188 +
189 /**
190 * Receive capabilities from server
191 * @param s {type.Stream}
192 */
193 recvClipboardCapsPDU(s) {
111 - // Start at 18
112 - s.offset = 18;
194 // const pdu = data.clipPDU().read(s);
195 // console.log('recvClipboardCapsPDU', s);
196 }
@@ -120,7 +201,6 @@ class Client extends Cliprdr {
201 * @param s {type.Stream}
202 */
203 recvMonitorReadyPDU(s) {
123 - s.offset = 18;
204 // const pdu = data.clipPDU().read(s);
205 // console.log('recvMonitorReadyPDU', s);
206
@@ -224,7 +304,6 @@ class Client extends Cliprdr {
304 * @param {type.Stream} s
305 */
306 recvFormatListPDU(s) {
227 - s.offset = 18;
307 // const pdu = data.clipPDU().read(s);
308 // console.log('recvFormatListPDU', s);
309 this.sendFormatListResponsePDU();
@@ -250,7 +329,6 @@ class Client extends Cliprdr {
329 * @param s {type.Stream}
330 */
331 recvFormatListResponsePDU(s) {
253 - s.offset = 18;
332 // const pdu = data.clipPDU().read(s);
333 // console.log('recvFormatListResponsePDU', s);
334 // this.sendFormatDataRequestPDU();
@@ -275,7 +353,6 @@ class Client extends Cliprdr {
353 * @param s {type.Stream}
354 */
355 recvFormatDataRequestPDU(s) {
278 - s.offset = 18;
356 // const pdu = data.clipPDU().read(s);
357 // console.log('recvFormatDataRequestPDU', s);
358 this.sendFormatDataResponsePDU();
@@ -300,13 +377,15 @@ class Client extends Cliprdr {
377
378
379 /**
303 - * Receive format data response PDU from server
380 + * Receive format data response PDU from server.
381 + * s.offset is positioned immediately after the CLIPRDR header (channel PDU header and
382 + * CLIPRDR msgType/msgFlags/dataLen were already consumed in recv()), so the UCS-2
383 + * text data starts exactly at s.offset.
384 * @param s {type.Stream}
385 */
386 recvFormatDataResponsePDU(s) {
307 - s.offset = 18;
387 // const pdu = data.clipPDU().read(s);
309 - const str = s.buffer.toString('ucs2', 26, s.buffer.length - 2);
388 + const str = s.buffer.toString('ucs2', s.offset, s.buffer.length - 2);
389 // console.log('recvFormatDataResponsePDU', str);
390 this.content = str;
391 this.emit('clipboard', str)