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)