autodev-cli 1.4.0 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/autodev.js +0 -0
- package/out/agentBackup/archive.d.ts +44 -0
- package/out/agentBackup/archive.js +131 -0
- package/out/agentBackup/archive.js.map +1 -0
- package/out/agentBackup/export.d.ts +18 -0
- package/out/agentBackup/export.js +92 -0
- package/out/agentBackup/export.js.map +1 -0
- package/out/agentBackup/import.d.ts +21 -0
- package/out/agentBackup/import.js +40 -0
- package/out/agentBackup/import.js.map +1 -0
- package/out/agentBackup/index.d.ts +6 -0
- package/out/agentBackup/index.js +11 -0
- package/out/agentBackup/index.js.map +1 -0
- package/out/agentBackup/layout.d.ts +30 -0
- package/out/agentBackup/layout.js +126 -0
- package/out/agentBackup/layout.js.map +1 -0
- package/out/agentBackup/manifest.d.ts +24 -0
- package/out/agentBackup/manifest.js +70 -0
- package/out/agentBackup/manifest.js.map +1 -0
- package/out/agentBackup/opencodeDb.d.ts +20 -0
- package/out/agentBackup/opencodeDb.js +213 -0
- package/out/agentBackup/opencodeDb.js.map +1 -0
- package/out/agentBackup/sessionProviders.d.ts +35 -0
- package/out/agentBackup/sessionProviders.js +263 -0
- package/out/agentBackup/sessionProviders.js.map +1 -0
- package/out/agentBackup/upload.d.ts +9 -0
- package/out/agentBackup/upload.js +121 -0
- package/out/agentBackup/upload.js.map +1 -0
- package/out/cli.d.ts +1 -0
- package/out/cli.js +8 -0
- package/out/cli.js.map +1 -1
- package/out/cliExit.d.ts +34 -0
- package/out/cliExit.js +159 -0
- package/out/cliExit.js.map +1 -0
- package/out/commands/config.d.ts +2 -0
- package/out/commands/config.js +7 -7
- package/out/commands/config.js.map +1 -1
- package/out/commands/connect.d.ts +2 -0
- package/out/commands/connect.js +11 -0
- package/out/commands/connect.js.map +1 -1
- package/out/commands/export.d.ts +2 -0
- package/out/commands/export.js +79 -0
- package/out/commands/export.js.map +1 -0
- package/out/commands/import.d.ts +2 -0
- package/out/commands/import.js +92 -0
- package/out/commands/import.js.map +1 -0
- package/out/commands/init.d.ts +16 -0
- package/out/commands/init.js +9 -5
- package/out/commands/init.js.map +1 -1
- package/out/commands/resume.d.ts +2 -0
- package/out/commands/resume.js +65 -0
- package/out/commands/resume.js.map +1 -0
- package/out/commands/sessions.d.ts +2 -0
- package/out/commands/sessions.js +64 -0
- package/out/commands/sessions.js.map +1 -0
- package/out/commands/start.d.ts +2 -0
- package/out/commands/start.js +40 -7
- package/out/commands/start.js.map +1 -1
- package/out/commands/status.d.ts +2 -0
- package/out/commands/status.js +3 -3
- package/out/commands/status.js.map +1 -1
- package/out/commands/tailOutput.d.ts +12 -0
- package/out/commands/up.d.ts +3 -0
- package/out/configManager.d.ts +42 -0
- package/out/configManager.js +430 -0
- package/out/configManager.js.map +1 -0
- package/out/connect.d.ts +4 -0
- package/out/connect.js +7 -7
- package/out/connect.js.map +1 -1
- package/out/core/adapters.d.ts +34 -0
- package/out/core/adapters.js +84 -0
- package/out/core/adapters.js.map +1 -0
- package/out/core/commandHelpers.d.ts +12 -0
- package/out/core/commandHelpers.js +96 -0
- package/out/core/commandHelpers.js.map +1 -0
- package/out/core/projectMcp.d.ts +25 -0
- package/out/core/projectMcp.js +144 -0
- package/out/core/projectMcp.js.map +1 -0
- package/out/core/provider/BaseProvider.d.ts +14 -0
- package/out/core/provider/BaseProvider.js +25 -0
- package/out/core/provider/BaseProvider.js.map +1 -0
- package/out/core/provider/ProviderRegistry.d.ts +12 -0
- package/out/core/provider/ProviderRegistry.js +40 -0
- package/out/core/provider/ProviderRegistry.js.map +1 -0
- package/out/core/provider/contract.d.ts +62 -0
- package/out/core/provider/contract.js +9 -0
- package/out/core/provider/contract.js.map +1 -0
- package/out/core/provider/implementations.d.ts +54 -0
- package/out/core/provider/implementations.js +147 -0
- package/out/core/provider/implementations.js.map +1 -0
- package/out/core/settingsLoader.d.ts +221 -0
- package/out/core/settingsLoader.js +176 -0
- package/out/core/settingsLoader.js.map +1 -0
- package/out/discordGateway.d.ts +26 -0
- package/out/discordGateway.js +230 -0
- package/out/discordGateway.js.map +1 -0
- package/out/discordPoller.d.ts +28 -0
- package/out/discordPoller.js +247 -0
- package/out/discordPoller.js.map +1 -0
- package/out/dispatcher.d.ts +12 -0
- package/out/dispatcher.js +214 -0
- package/out/dispatcher.js.map +1 -0
- package/out/emailPoller.d.ts +42 -0
- package/out/emailPoller.js +221 -0
- package/out/emailPoller.js.map +1 -0
- package/out/git/gitService.d.ts +36 -0
- package/out/git/gitService.js +165 -0
- package/out/git/gitService.js.map +1 -0
- package/out/hookEventNormalizer.d.ts +39 -0
- package/out/hookEventNormalizer.js +397 -0
- package/out/hookEventNormalizer.js.map +1 -0
- package/out/hooksManager.d.ts +25 -0
- package/out/hooksManager.js +471 -0
- package/out/hooksManager.js.map +1 -0
- package/out/launchIde.d.ts +14 -0
- package/out/logger.d.ts +12 -0
- package/out/mcpEmailTest.d.ts +29 -0
- package/out/mcpEmailTest.js +245 -0
- package/out/mcpEmailTest.js.map +1 -0
- package/out/mcpInstallCheck.d.ts +23 -0
- package/out/mcpInstallCheck.js +219 -0
- package/out/mcpInstallCheck.js.map +1 -0
- package/out/mcpManager.d.ts +35 -0
- package/out/mcpManager.js +371 -0
- package/out/mcpManager.js.map +1 -0
- package/out/messageBuilder.d.ts +54 -0
- package/out/messageBuilder.js +373 -0
- package/out/messageBuilder.js.map +1 -0
- package/out/openCodeHooksManager.d.ts +23 -0
- package/out/openCodeHooksManager.js +511 -0
- package/out/openCodeHooksManager.js.map +1 -0
- package/out/periodicActions.d.ts +63 -0
- package/out/periodicActions.js +237 -0
- package/out/periodicActions.js.map +1 -0
- package/out/profileBuilder.d.ts +29 -0
- package/out/profileBuilder.js +366 -0
- package/out/profileBuilder.js.map +1 -0
- package/out/prompt.d.ts +12 -0
- package/out/prompt.js +18 -0
- package/out/prompt.js.map +1 -0
- package/out/protocolSections.d.ts +26 -0
- package/out/protocolSections.js +209 -0
- package/out/protocolSections.js.map +1 -0
- package/out/providers/claudeCliProvider.d.ts +71 -0
- package/out/providers/claudeCliProvider.js +425 -0
- package/out/providers/claudeCliProvider.js.map +1 -0
- package/out/providers/claudeTuiProvider.d.ts +23 -0
- package/out/providers/claudeTuiProvider.js +296 -0
- package/out/providers/claudeTuiProvider.js.map +1 -0
- package/out/providers/copilotCliProvider.d.ts +16 -0
- package/out/providers/copilotCliProvider.js +44 -0
- package/out/providers/copilotCliProvider.js.map +1 -0
- package/out/providers/copilotSdkProvider.d.ts +12 -0
- package/out/providers/copilotSdkProvider.js +445 -0
- package/out/providers/copilotSdkProvider.js.map +1 -0
- package/out/providers/grokTuiProvider.d.ts +14 -0
- package/out/providers/grokTuiProvider.js +271 -0
- package/out/providers/grokTuiProvider.js.map +1 -0
- package/out/providers/opencodeCliProvider.d.ts +29 -0
- package/out/providers/opencodeCliProvider.js +199 -0
- package/out/providers/opencodeCliProvider.js.map +1 -0
- package/out/providers/opencodeSdkProvider.d.ts +22 -0
- package/out/providers/opencodeSdkProvider.js +557 -0
- package/out/providers/opencodeSdkProvider.js.map +1 -0
- package/out/providers.d.ts +9 -0
- package/out/providers.js +44 -0
- package/out/providers.js.map +1 -0
- package/out/rateLimit.d.ts +18 -0
- package/out/rateLimit.js +90 -0
- package/out/rateLimit.js.map +1 -0
- package/out/rdp/auth.d.ts +55 -0
- package/out/rdp/auth.js +197 -0
- package/out/rdp/auth.js.map +1 -0
- package/out/rdp/bridge.d.ts +86 -0
- package/out/rdp/bridge.js +1398 -0
- package/out/rdp/bridge.js.map +1 -0
- package/out/rdp/constants.d.ts +86 -0
- package/out/rdp/constants.js +182 -0
- package/out/rdp/constants.js.map +1 -0
- package/out/rdp/index.d.ts +7 -0
- package/out/rdp/index.js +14 -0
- package/out/rdp/index.js.map +1 -0
- package/out/rdp/session.d.ts +30 -0
- package/out/rdp/session.js +196 -0
- package/out/rdp/session.js.map +1 -0
- package/out/rdp/types.d.ts +27 -0
- package/out/rdp/types.js +6 -0
- package/out/rdp/types.js.map +1 -0
- package/out/sdk/index.d.ts +22 -0
- package/out/sdk/index.js +81 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sessionState.d.ts +54 -0
- package/out/sessionState.js +284 -0
- package/out/sessionState.js.map +1 -0
- package/out/sessions.d.ts +11 -0
- package/out/sessions.js +32 -0
- package/out/sessions.js.map +1 -0
- package/out/taskLoop.d.ts +152 -0
- package/out/taskLoop.js +2505 -0
- package/out/taskLoop.js.map +1 -0
- package/out/todo.d.ts +42 -0
- package/out/todo.js +311 -0
- package/out/todo.js.map +1 -0
- package/out/todoWriteManager.d.ts +26 -0
- package/out/todoWriteManager.js +44 -0
- package/out/todoWriteManager.js.map +1 -0
- package/out/vnc/auth.d.ts +52 -0
- package/out/vnc/auth.js +181 -0
- package/out/vnc/auth.js.map +1 -0
- package/out/vnc/bridge.d.ts +40 -0
- package/out/vnc/bridge.js +540 -0
- package/out/vnc/bridge.js.map +1 -0
- package/out/vnc/constants.d.ts +8 -0
- package/out/vnc/constants.js +34 -0
- package/out/vnc/constants.js.map +1 -0
- package/out/vnc/des.d.ts +6 -0
- package/out/vnc/des.js +93 -0
- package/out/vnc/des.js.map +1 -0
- package/out/vnc/index.d.ts +7 -0
- package/out/vnc/index.js +13 -0
- package/out/vnc/index.js.map +1 -0
- package/out/vnc/session.d.ts +18 -0
- package/out/vnc/session.js +193 -0
- package/out/vnc/session.js.map +1 -0
- package/out/vnc/types.d.ts +16 -0
- package/out/vnc/types.js +6 -0
- package/out/vnc/types.js.map +1 -0
- package/out/webSocketPoller.d.ts +95 -0
- package/out/webSocketPoller.js +986 -0
- package/out/webSocketPoller.js.map +1 -0
- package/out/webhook.d.ts +37 -0
- package/out/webhook.js +265 -0
- package/out/webhook.js.map +1 -0
- package/out/webhookPoller.d.ts +40 -0
- package/out/webhookPoller.js +378 -0
- package/out/webhookPoller.js.map +1 -0
- package/package.json +54 -41
|
@@ -0,0 +1,1398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RdpBridge — TCP/TLS connection to an RDP server.
|
|
4
|
+
*
|
|
5
|
+
* Implements the minimal RDP protocol phases needed for screen + input:
|
|
6
|
+
*
|
|
7
|
+
* Phase 1 X.224 Connection Request / Confirm (TPKT/X.224)
|
|
8
|
+
* Phase 2 TLS upgrade (PROTOCOL_SSL negotiated in Phase 1)
|
|
9
|
+
* Phase 3 MCS Connect (T.125 / GCC Conference Create)
|
|
10
|
+
* Phase 4 MCS Erect Domain + Attach User
|
|
11
|
+
* Phase 5 MCS Channel Joins (Global + optional cliprdr)
|
|
12
|
+
* Phase 6 RDP Security Exchange + Client Info (Classic RDP Security disabled; TLS carries everything)
|
|
13
|
+
* Phase 7 Demand-Active / Confirm-Active capability exchange
|
|
14
|
+
* Phase 8 Running — bitmap updates, input events, clipboard
|
|
15
|
+
*
|
|
16
|
+
* Emits:
|
|
17
|
+
* 'fbu' (rects: RdpRect[]) — one or more bitmap rectangles
|
|
18
|
+
* 'cursor' ({ hotX, hotY, width, height, rgba: string }) — cursor shape update
|
|
19
|
+
* 'clipboard' (text: string) — remote clipboard changed
|
|
20
|
+
* 'error' (err: Error) — unrecoverable error
|
|
21
|
+
* 'close' () — TCP/TLS connection closed
|
|
22
|
+
*/
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.RdpBridge = void 0;
|
|
58
|
+
const net = __importStar(require("net"));
|
|
59
|
+
const crypto = __importStar(require("crypto"));
|
|
60
|
+
const events_1 = require("events");
|
|
61
|
+
const auth_1 = require("./auth");
|
|
62
|
+
const constants_1 = require("./constants");
|
|
63
|
+
// ── TPKT helpers ───────────────────────────────────────────────────────────
|
|
64
|
+
function wrapTpkt(payload) {
|
|
65
|
+
const out = Buffer.alloc(4 + payload.length);
|
|
66
|
+
out[0] = 0x03; // TPKT version
|
|
67
|
+
out[1] = 0x00;
|
|
68
|
+
out.writeUInt16BE(4 + payload.length, 2);
|
|
69
|
+
payload.copy(out, 4);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function wrapX224Data(payload) {
|
|
73
|
+
// X.224 Data TPDU: LI(1)=2, code(1)=0xF0, EOT(1)=0x80
|
|
74
|
+
const header = Buffer.from([0x02, constants_1.X224_TPDU_DATA, 0x80]);
|
|
75
|
+
return Buffer.concat([header, payload]);
|
|
76
|
+
}
|
|
77
|
+
function wrapMcsSend(userId, channelId, payload) {
|
|
78
|
+
// MCS SendDataRequest BER encoding (simplified):
|
|
79
|
+
// opcode(1) | initiator(2) | channelId(2) | dataPriority+segmentation(1) | length(variable) | data
|
|
80
|
+
const dataLen = payload.length;
|
|
81
|
+
let lenBuf;
|
|
82
|
+
if (dataLen < 128) {
|
|
83
|
+
lenBuf = Buffer.from([dataLen]);
|
|
84
|
+
}
|
|
85
|
+
else if (dataLen < 0x4000) {
|
|
86
|
+
lenBuf = Buffer.from([0x80 | (dataLen >> 8), dataLen & 0xff]);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
lenBuf = Buffer.from([0x82, (dataLen >> 16) & 0xff, (dataLen >> 8) & 0xff, dataLen & 0xff]);
|
|
90
|
+
}
|
|
91
|
+
const header = Buffer.alloc(6);
|
|
92
|
+
header[0] = constants_1.MCS_SEND_DATA_REQUEST;
|
|
93
|
+
header.writeUInt16BE(userId - 1001, 1); // initiator (offset from base)
|
|
94
|
+
header.writeUInt16BE(channelId, 3);
|
|
95
|
+
header[5] = 0x70; // high priority + end of segmentation
|
|
96
|
+
return Buffer.concat([header, lenBuf, payload]);
|
|
97
|
+
}
|
|
98
|
+
// ── GCC / MCS Connect helpers ─────────────────────────────────────────────
|
|
99
|
+
/** Encode a BER length field. */
|
|
100
|
+
function berLen(len) {
|
|
101
|
+
if (len < 128)
|
|
102
|
+
return Buffer.from([len]);
|
|
103
|
+
if (len < 256)
|
|
104
|
+
return Buffer.from([0x81, len]);
|
|
105
|
+
return Buffer.from([0x82, len >> 8, len & 0xff]);
|
|
106
|
+
}
|
|
107
|
+
/** Wrap `content` in a BER TLV. `tag` may be 1 or 2 bytes (e.g. 0x7f65). */
|
|
108
|
+
function berTlv(tag, content) {
|
|
109
|
+
const tagBuf = tag > 0xff
|
|
110
|
+
? Buffer.from([tag >> 8, tag & 0xff])
|
|
111
|
+
: Buffer.from([tag]);
|
|
112
|
+
return Buffer.concat([tagBuf, berLen(content.length), content]);
|
|
113
|
+
}
|
|
114
|
+
/** BER-encode a small non-negative INTEGER. */
|
|
115
|
+
function berInt(v) {
|
|
116
|
+
if (v === 0)
|
|
117
|
+
return Buffer.from([0x02, 0x01, 0x00]);
|
|
118
|
+
if (v <= 0x7f)
|
|
119
|
+
return Buffer.from([0x02, 0x01, v]);
|
|
120
|
+
if (v <= 0x7fff)
|
|
121
|
+
return Buffer.from([0x02, 0x02, v >> 8, v & 0xff]);
|
|
122
|
+
return Buffer.from([0x02, 0x03, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff]);
|
|
123
|
+
}
|
|
124
|
+
/** Build one MCS DomainParameters SEQUENCE. */
|
|
125
|
+
function berDomainParams(maxChannelIds, maxUserIds, maxTokenIds, numPriorities, minThroughput, maxHeight, maxMCSPDU, protocolVersion) {
|
|
126
|
+
const body = Buffer.concat([
|
|
127
|
+
berInt(maxChannelIds), berInt(maxUserIds), berInt(maxTokenIds),
|
|
128
|
+
berInt(numPriorities), berInt(minThroughput), berInt(maxHeight),
|
|
129
|
+
berInt(maxMCSPDU), berInt(protocolVersion),
|
|
130
|
+
]);
|
|
131
|
+
return berTlv(0x30, body); // SEQUENCE
|
|
132
|
+
}
|
|
133
|
+
function buildMcsConnectInitial(width, height, colorDepth) {
|
|
134
|
+
// ── GCC Conference Create Request (client core data only) ─────────────────
|
|
135
|
+
// Full spec: [MS-RDPBCGR] section 2.2.1.3
|
|
136
|
+
const rdpVersion = 0x00080004; // RDP 5.0+
|
|
137
|
+
// Client Core Data (CS_CORE): type=0xC001, length=216 bytes
|
|
138
|
+
const csCore = Buffer.alloc(216);
|
|
139
|
+
csCore.writeUInt16LE(0xC001, 0); // CS_CORE
|
|
140
|
+
csCore.writeUInt16LE(216, 2); // length
|
|
141
|
+
csCore.writeUInt32LE(rdpVersion, 4);
|
|
142
|
+
csCore.writeUInt16LE(width, 8);
|
|
143
|
+
csCore.writeUInt16LE(height, 10);
|
|
144
|
+
csCore.writeUInt16LE(0xCA01, 12); // colorDepth = 8bpp (negotiated later)
|
|
145
|
+
csCore.writeUInt16LE(0xAA03, 14); // SASSequence
|
|
146
|
+
csCore.writeUInt32LE(0x0409, 16); // keyboardLayout (English US)
|
|
147
|
+
csCore.writeUInt32LE(2600, 20); // clientBuild
|
|
148
|
+
Buffer.from('autodev', 'utf16le').copy(csCore, 24); // clientName (32 bytes)
|
|
149
|
+
csCore.writeUInt32LE(0x00000004, 56); // keyboardType = IBM enhanced
|
|
150
|
+
csCore.writeUInt32LE(0x00000000, 60); // keyboardSubType
|
|
151
|
+
csCore.writeUInt32LE(12, 64); // keyboardFunctionKey
|
|
152
|
+
csCore.writeUInt16LE(0xCA01, 130); // postBeta2ColorDepth
|
|
153
|
+
csCore.writeUInt16LE(1, 132); // clientProductId
|
|
154
|
+
csCore.writeUInt32LE(0, 134); // serialNumber
|
|
155
|
+
const hcd = colorDepth >= 24 ? 24 : colorDepth >= 16 ? 16 : colorDepth >= 15 ? 15 : 8;
|
|
156
|
+
csCore.writeUInt16LE(hcd, 138);
|
|
157
|
+
csCore.writeUInt16LE(0x0007, 140); // supportedColorDepths: 15|16|24bpp
|
|
158
|
+
csCore.writeUInt16LE(0x0001, 142); // earlyCapabilityFlags: ERRINFO_PDU
|
|
159
|
+
csCore.writeUInt8(0, 208); // connectionType
|
|
160
|
+
csCore.writeUInt8(0, 209); // pad1Octet
|
|
161
|
+
csCore.writeUInt32LE(0x00000001 /* PROTOCOL_SSL */, 210); // serverSelectedProtocol
|
|
162
|
+
// Client Security Data (CS_SECURITY): no encryption
|
|
163
|
+
const csSec = Buffer.alloc(12);
|
|
164
|
+
csSec.writeUInt16LE(0xC002, 0);
|
|
165
|
+
csSec.writeUInt16LE(12, 2);
|
|
166
|
+
csSec.writeUInt32LE(constants_1.ENCRYPTION_METHOD_NONE, 4);
|
|
167
|
+
csSec.writeUInt32LE(constants_1.ENCRYPTION_LEVEL_NONE, 8);
|
|
168
|
+
// Client Cluster Data (CS_CLUSTER)
|
|
169
|
+
const csCluster = Buffer.alloc(12);
|
|
170
|
+
csCluster.writeUInt16LE(0xC004, 0);
|
|
171
|
+
csCluster.writeUInt16LE(12, 2);
|
|
172
|
+
csCluster.writeUInt32LE(0x0000000D, 4); // REDIRECTION_SUPPORTED
|
|
173
|
+
csCluster.writeUInt32LE(0, 8);
|
|
174
|
+
const clientData = Buffer.concat([csCore, csSec, csCluster]);
|
|
175
|
+
// GCC ConferenceCreateRequest — T.124 PER-encoded wrapper
|
|
176
|
+
// [MS-RDPBCGR] 2.2.1.3 — the ConnectGCCPDU body must carry the proper
|
|
177
|
+
// T.124 ConferenceCreateRequest structure before the H.221/Duca client data.
|
|
178
|
+
// Without this structure xrdp cannot extract the screen dimensions and aborts
|
|
179
|
+
// with "xrdp_bitmap_create: size overflow 0x0x4" after the FontMap handshake.
|
|
180
|
+
function perLen(len) {
|
|
181
|
+
// PER unconstrained length: 1 byte for 0-127, 2 bytes for 128-16383
|
|
182
|
+
if (len <= 0x7F)
|
|
183
|
+
return Buffer.from([len]);
|
|
184
|
+
return Buffer.from([0x80 | (len >> 8), len & 0xFF]);
|
|
185
|
+
}
|
|
186
|
+
// ConnectGCCPDU body: choice(1) + conference-name(5) + optFlags(2) + "Duca"(4)
|
|
187
|
+
// + perLen(clientData) + clientData
|
|
188
|
+
const ccrBody = Buffer.concat([
|
|
189
|
+
Buffer.from([
|
|
190
|
+
0x00, // choice: conferenceCreateRequest
|
|
191
|
+
0x08, 0x00, 0x10, 0x00, 0x01, // conference name PER
|
|
192
|
+
0xC0, 0x00, // optional fields: userData present
|
|
193
|
+
0x44, 0x75, 0x63, 0x61, // H.221 non-standard key "Duca"
|
|
194
|
+
]),
|
|
195
|
+
perLen(clientData.length), // PER length of client data blocks
|
|
196
|
+
clientData,
|
|
197
|
+
]);
|
|
198
|
+
// T.124 ConnectData: H.221 OID key + PER length of CCR body + CCR body
|
|
199
|
+
const gccKey = Buffer.from([0x00, 0x05, 0x00, 0x14, 0x7c, 0x00, 0x01]);
|
|
200
|
+
const gccConnReq = Buffer.concat([gccKey, perLen(ccrBody.length), ccrBody]);
|
|
201
|
+
// MCS Connect-Initial BER encoding [MS-RDPBCGR] 2.2.1.3 / T.125
|
|
202
|
+
//
|
|
203
|
+
// ConnectInitial ::= [APPLICATION 101] IMPLICIT SEQUENCE {
|
|
204
|
+
// callingDomainSelector OCTET STRING,
|
|
205
|
+
// calledDomainSelector OCTET STRING,
|
|
206
|
+
// upwardFlag BOOLEAN,
|
|
207
|
+
// targetParameters DomainParameters,
|
|
208
|
+
// minimumParameters DomainParameters,
|
|
209
|
+
// maximumParameters DomainParameters,
|
|
210
|
+
// userData OCTET STRING ← contains GCC data
|
|
211
|
+
// }
|
|
212
|
+
const body = Buffer.concat([
|
|
213
|
+
berTlv(0x04, Buffer.from([0x01])), // callingDomainSelector
|
|
214
|
+
berTlv(0x04, Buffer.from([0x01])), // calledDomainSelector
|
|
215
|
+
Buffer.from([0x01, 0x01, 0xff]), // upwardFlag BOOLEAN TRUE
|
|
216
|
+
berDomainParams(34, 2, 0, 1, 0, 1, 65535, 2), // target
|
|
217
|
+
berDomainParams(1, 1, 1, 1, 0, 1, 1056, 2), // minimum
|
|
218
|
+
berDomainParams(65535, 64535, 65535, 1, 0, 1, 65535, 2), // maximum
|
|
219
|
+
berTlv(0x04, gccConnReq), // userData
|
|
220
|
+
]);
|
|
221
|
+
// Wrap in APPLICATION 101 CONSTRUCTED = 0x7f 0x65
|
|
222
|
+
return berTlv(0x7f65, body);
|
|
223
|
+
}
|
|
224
|
+
// ── License crypto helpers ────────────────────────────────────────────────
|
|
225
|
+
/** Modular exponentiation using BigInt (for pure-JS RSA). */
|
|
226
|
+
function modpow(base, exp, mod) {
|
|
227
|
+
let result = 1n;
|
|
228
|
+
base = base % mod;
|
|
229
|
+
while (exp > 0n) {
|
|
230
|
+
if (exp & 1n)
|
|
231
|
+
result = (result * base) % mod;
|
|
232
|
+
exp >>= 1n;
|
|
233
|
+
base = (base * base) % mod;
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* RSA PKCS#1 v1.5 encrypt `message` using an RDP little-endian modulus.
|
|
239
|
+
* Returns the ciphertext in little-endian (as RDP expects).
|
|
240
|
+
*/
|
|
241
|
+
function rsaEncryptLe(message, modulusLe, exponent) {
|
|
242
|
+
const modulusBe = Buffer.from(modulusLe).reverse();
|
|
243
|
+
const n = BigInt('0x' + modulusBe.toString('hex'));
|
|
244
|
+
const ks = modulusLe.length;
|
|
245
|
+
const padLen = ks - message.length - 3;
|
|
246
|
+
const padBytes = crypto.randomBytes(padLen).map(b => b === 0 ? 1 : b);
|
|
247
|
+
const padded = Buffer.concat([Buffer.from([0x00, 0x02]), padBytes, Buffer.from([0x00]), message]);
|
|
248
|
+
const m = BigInt('0x' + padded.toString('hex'));
|
|
249
|
+
const c = modpow(m, BigInt(exponent), n);
|
|
250
|
+
const cHex = c.toString(16).padStart(ks * 2, '0');
|
|
251
|
+
return Buffer.from(Buffer.from(cHex, 'hex')).reverse(); // LE
|
|
252
|
+
}
|
|
253
|
+
/** MS-RDPELE §5.1.3 salted hash: SHA1 + MD5 mix. */
|
|
254
|
+
function saltedHash(secret, label, r1, r2) {
|
|
255
|
+
const sha = crypto.createHash('sha1');
|
|
256
|
+
sha.update(label);
|
|
257
|
+
sha.update(secret);
|
|
258
|
+
sha.update(r1);
|
|
259
|
+
sha.update(r2);
|
|
260
|
+
const md5 = crypto.createHash('md5');
|
|
261
|
+
md5.update(secret);
|
|
262
|
+
md5.update(sha.digest());
|
|
263
|
+
return md5.digest();
|
|
264
|
+
}
|
|
265
|
+
/** Derive MAC key (16 B) and enc key (16 B) from pre-master secret. */
|
|
266
|
+
function deriveLicenseKeys(preMaster, clientRand, serverRand) {
|
|
267
|
+
let ms = Buffer.concat([
|
|
268
|
+
saltedHash(preMaster, Buffer.from('A'), clientRand, serverRand),
|
|
269
|
+
saltedHash(preMaster, Buffer.from('BB'), clientRand, serverRand),
|
|
270
|
+
saltedHash(preMaster, Buffer.from('CCC'), clientRand, serverRand),
|
|
271
|
+
]);
|
|
272
|
+
const skb = Buffer.concat([
|
|
273
|
+
saltedHash(ms, Buffer.from('A'), serverRand, clientRand),
|
|
274
|
+
saltedHash(ms, Buffer.from('BB'), serverRand, clientRand),
|
|
275
|
+
saltedHash(ms, Buffer.from('CCC'), serverRand, clientRand),
|
|
276
|
+
]);
|
|
277
|
+
return [skb.slice(0, 16), skb.slice(16, 32)]; // [mac_key, enc_key]
|
|
278
|
+
}
|
|
279
|
+
function macData(macKey, data) {
|
|
280
|
+
const p1 = Buffer.alloc(40).fill(0x36);
|
|
281
|
+
const p2 = Buffer.alloc(48).fill(0x5c);
|
|
282
|
+
const sha = crypto.createHash('sha1');
|
|
283
|
+
sha.update(macKey);
|
|
284
|
+
sha.update(p1);
|
|
285
|
+
const lenBuf = Buffer.alloc(4);
|
|
286
|
+
lenBuf.writeUInt32LE(data.length, 0);
|
|
287
|
+
sha.update(lenBuf);
|
|
288
|
+
sha.update(data);
|
|
289
|
+
const md5 = crypto.createHash('md5');
|
|
290
|
+
md5.update(macKey);
|
|
291
|
+
md5.update(p2);
|
|
292
|
+
md5.update(sha.digest());
|
|
293
|
+
return md5.digest().slice(0, 16);
|
|
294
|
+
}
|
|
295
|
+
function rc4(key, data) {
|
|
296
|
+
const cipher = crypto.createDecipheriv('rc4', key, null);
|
|
297
|
+
return Buffer.concat([cipher.update(data), cipher.final()]);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse SERVER_LICENSE_REQUEST to extract serverRandom, RSA modulus (LE) and exponent.
|
|
301
|
+
* `body` starts at the license preamble (4 bytes before ServerRandom).
|
|
302
|
+
*/
|
|
303
|
+
function parseLicenseRequest(body) {
|
|
304
|
+
// body[0..3] = preamble; body[4..35] = ServerRandom[32]
|
|
305
|
+
const serverRand = body.slice(4, 36);
|
|
306
|
+
let pos = 36;
|
|
307
|
+
// ProductInfo: dwVersion(4) + cbCompanyName(4) + name + cbProductId(4) + id
|
|
308
|
+
const cbCompany = body.readUInt32LE(pos + 4);
|
|
309
|
+
pos += 4 + 4 + cbCompany;
|
|
310
|
+
const cbProduct = body.readUInt32LE(pos);
|
|
311
|
+
pos += 4 + cbProduct;
|
|
312
|
+
// KeyExchangeList blob: wBlobType(2)+wBlobLen(2)+data
|
|
313
|
+
const kbLen = body.readUInt16LE(pos + 2);
|
|
314
|
+
pos += 4 + kbLen;
|
|
315
|
+
// ServerCertificate blob
|
|
316
|
+
const certLen = body.readUInt16LE(pos + 2);
|
|
317
|
+
const cert = body.slice(pos + 4, pos + 4 + certLen);
|
|
318
|
+
// PROPRIETARYSERVERCERTIFICATE: dwVersion(4)+dwSigAlgId(4)+dwKeyAlgId(4)+wBlobType(2)+wBlobLen(2)+RSA_PUBLIC_KEY
|
|
319
|
+
const pkBlobLen = cert.readUInt16LE(14);
|
|
320
|
+
const pk = cert.slice(16, 16 + pkBlobLen); // RSA_PUBLIC_KEY
|
|
321
|
+
// RSA1(4)+keylen(4)+bitlen(4)+datalen(4)+pubExp(4)+modulus(keylen)
|
|
322
|
+
const keylen = pk.readUInt32LE(4);
|
|
323
|
+
const bitlen = pk.readUInt32LE(8);
|
|
324
|
+
const exponent = pk.readUInt32LE(16);
|
|
325
|
+
const modulusLe = pk.slice(20, 20 + bitlen / 8);
|
|
326
|
+
return { serverRand, modulusLe, exponent };
|
|
327
|
+
}
|
|
328
|
+
function buildNewLicenseRequest(clientRand, encPms, username, machine) {
|
|
329
|
+
const un = Buffer.from(username + '\0', 'ascii');
|
|
330
|
+
const mn = Buffer.from(machine + '\0', 'ascii');
|
|
331
|
+
const body = Buffer.alloc(4 + 4 + 32 + 4 + encPms.length + 4 + un.length + 4 + mn.length);
|
|
332
|
+
let off = 0;
|
|
333
|
+
body.writeUInt32LE(1, off);
|
|
334
|
+
off += 4; // KEY_EXCHANGE_ALG_RSA
|
|
335
|
+
body.writeUInt32LE(0x04000000, off);
|
|
336
|
+
off += 4; // PlatformId
|
|
337
|
+
clientRand.copy(body, off);
|
|
338
|
+
off += 32;
|
|
339
|
+
body.writeUInt16LE(0x0006, off);
|
|
340
|
+
off += 2; // wBlobType = BB_KEY_EXCHG_ALG_BLOB
|
|
341
|
+
body.writeUInt16LE(encPms.length, off);
|
|
342
|
+
off += 2;
|
|
343
|
+
encPms.copy(body, off);
|
|
344
|
+
off += encPms.length;
|
|
345
|
+
body.writeUInt16LE(0x000f, off);
|
|
346
|
+
off += 2; // wBlobType = BB_CLIENT_USER_NAME_BLOB
|
|
347
|
+
body.writeUInt16LE(un.length, off);
|
|
348
|
+
off += 2;
|
|
349
|
+
un.copy(body, off);
|
|
350
|
+
off += un.length;
|
|
351
|
+
body.writeUInt16LE(0x0010, off);
|
|
352
|
+
off += 2; // wBlobType = BB_CLIENT_MACHINE_NAME_BLOB
|
|
353
|
+
body.writeUInt16LE(mn.length, off);
|
|
354
|
+
off += 2;
|
|
355
|
+
mn.copy(body, off);
|
|
356
|
+
const preamble = Buffer.alloc(4);
|
|
357
|
+
preamble.writeUInt8(0x13, 0);
|
|
358
|
+
preamble.writeUInt8(0x00, 1);
|
|
359
|
+
preamble.writeUInt16LE(4 + body.length, 2);
|
|
360
|
+
return Buffer.concat([preamble, body]);
|
|
361
|
+
}
|
|
362
|
+
function buildPlatformChallengeResponse(encKey, macKey, challengePlain) {
|
|
363
|
+
const hwid = Buffer.alloc(8);
|
|
364
|
+
hwid.writeUInt32LE(2, 0); // hardwareIDVersion=2, hardwareID1=0
|
|
365
|
+
const encChallenge = rc4(encKey, challengePlain);
|
|
366
|
+
const encHwid = rc4(encKey, hwid);
|
|
367
|
+
const mac = macData(macKey, Buffer.concat([challengePlain, hwid]));
|
|
368
|
+
const encChalLen = Buffer.alloc(4);
|
|
369
|
+
encChalLen.writeUInt16LE(0x000e, 0);
|
|
370
|
+
encChalLen.writeUInt16LE(encChallenge.length, 2);
|
|
371
|
+
const encHwidLen = Buffer.alloc(4);
|
|
372
|
+
encHwidLen.writeUInt16LE(0x000f, 0);
|
|
373
|
+
encHwidLen.writeUInt16LE(encHwid.length, 2);
|
|
374
|
+
const body = Buffer.concat([encChalLen, encChallenge, encHwidLen, encHwid, mac]);
|
|
375
|
+
const preamble = Buffer.alloc(4);
|
|
376
|
+
preamble.writeUInt8(0x15, 0);
|
|
377
|
+
preamble.writeUInt8(0x80, 1);
|
|
378
|
+
preamble.writeUInt16LE(4 + body.length, 2);
|
|
379
|
+
return Buffer.concat([preamble, body]);
|
|
380
|
+
}
|
|
381
|
+
// ── Client Info PDU ───────────────────────────────────────────────────────
|
|
382
|
+
function buildClientInfoPdu(username, password, domain) {
|
|
383
|
+
// [MS-RDPBCGR] 2.2.1.11 INFO_PACKET
|
|
384
|
+
const INFO_MOUSE = 0x00000001;
|
|
385
|
+
const INFO_DISABLECTRLALTDEL = 0x00000002;
|
|
386
|
+
const INFO_AUTOLOGON = 0x00000008;
|
|
387
|
+
const INFO_UNICODE = 0x00000010;
|
|
388
|
+
const INFO_MAXIMIZESHELL = 0x00000020;
|
|
389
|
+
const INFO_COMPRESSION = 0x00000080;
|
|
390
|
+
const INFO_ENABLEWINDOWSKEY = 0x00000100;
|
|
391
|
+
const encU = (s) => Buffer.from(s + '\0', 'utf16le');
|
|
392
|
+
const domainBuf = encU(domain);
|
|
393
|
+
const usernameBuf = encU(username);
|
|
394
|
+
const passwordBuf = encU(password);
|
|
395
|
+
const shellBuf = encU('');
|
|
396
|
+
const workdirBuf = encU('');
|
|
397
|
+
const flags = INFO_MOUSE | INFO_DISABLECTRLALTDEL | INFO_AUTOLOGON |
|
|
398
|
+
INFO_UNICODE | INFO_MAXIMIZESHELL | INFO_COMPRESSION |
|
|
399
|
+
INFO_ENABLEWINDOWSKEY;
|
|
400
|
+
const hdr = Buffer.alloc(18);
|
|
401
|
+
hdr.writeUInt32LE(0, 0); // codePage
|
|
402
|
+
hdr.writeUInt32LE(flags, 4);
|
|
403
|
+
hdr.writeUInt16LE(domainBuf.length - 2, 8); // cbDomain (excluding null)
|
|
404
|
+
hdr.writeUInt16LE(usernameBuf.length - 2, 10); // cbUserName
|
|
405
|
+
hdr.writeUInt16LE(passwordBuf.length - 2, 12); // cbPassword
|
|
406
|
+
hdr.writeUInt16LE(shellBuf.length - 2, 14); // cbAlternateShell
|
|
407
|
+
hdr.writeUInt16LE(workdirBuf.length - 2, 16); // cbWorkingDir
|
|
408
|
+
// TS_EXTENDED_INFO_PACKET (required by RDP 5.0+)
|
|
409
|
+
// clientAddressFamily(2) + cbClientAddress(2) + clientAddress(2) +
|
|
410
|
+
// cbClientDir(2) + clientDir(2) + clientTimeZone(172) +
|
|
411
|
+
// clientSessionId(4) + performanceFlags(4) + cbAutoReconnectCookie(2)
|
|
412
|
+
const ext = Buffer.alloc(2 + 2 + 2 + 2 + 2 + 172 + 4 + 4 + 2);
|
|
413
|
+
ext.writeUInt16LE(0x0002, 0); // clientAddressFamily = AF_INET
|
|
414
|
+
ext.writeUInt16LE(2, 2); // cbClientAddress = 2 (null-term only)
|
|
415
|
+
// clientAddress[2] = 0x0000 (already zero)
|
|
416
|
+
ext.writeUInt16LE(2, 6); // cbClientDir = 2 (null-term only)
|
|
417
|
+
// clientDir[2] = 0x0000 (already zero)
|
|
418
|
+
// clientTimeZone[172] = all zeros (UTC)
|
|
419
|
+
// clientSessionId, performanceFlags, cbAutoReconnectCookie = 0
|
|
420
|
+
const info = Buffer.concat([hdr, domainBuf, usernameBuf, passwordBuf, shellBuf, workdirBuf, ext]);
|
|
421
|
+
// Prepend SEC_INFO_PKT Security Header (flags only — no encryption)
|
|
422
|
+
const secHdr = Buffer.alloc(4);
|
|
423
|
+
secHdr.writeUInt16LE(constants_1.SEC_LOGON_INFO, 0);
|
|
424
|
+
secHdr.writeUInt16LE(0, 2); // flagsHi
|
|
425
|
+
return Buffer.concat([secHdr, info]);
|
|
426
|
+
}
|
|
427
|
+
// ── RDP Capabilities ──────────────────────────────────────────────────────
|
|
428
|
+
function buildConfirmActivePdu(userId, shareId, width, height) {
|
|
429
|
+
// ShareControlHeader
|
|
430
|
+
const numCaps = 13;
|
|
431
|
+
// Build minimal capability set
|
|
432
|
+
const caps = [];
|
|
433
|
+
// CAPSTYPE_GENERAL (0x0001)
|
|
434
|
+
const genCap = Buffer.alloc(24);
|
|
435
|
+
genCap.writeUInt16LE(0x0001, 0);
|
|
436
|
+
genCap.writeUInt16LE(24, 2);
|
|
437
|
+
genCap.writeUInt16LE(1, 4); // osMajorType = Windows
|
|
438
|
+
genCap.writeUInt16LE(3, 6); // osMinorType = Windows NT
|
|
439
|
+
genCap.writeUInt16LE(0x0200, 8); // protocolVersion
|
|
440
|
+
genCap.writeUInt16LE(0x0000, 12); // extraFlags: no fast-path (force slow-path TPKT updates)
|
|
441
|
+
genCap.writeUInt16LE(2, 20); // refreshRectSupport
|
|
442
|
+
genCap.writeUInt16LE(2, 22); // suppressOutputSupport
|
|
443
|
+
caps.push(genCap);
|
|
444
|
+
// CAPSTYPE_BITMAP (0x0002)
|
|
445
|
+
const bitmapCap = Buffer.alloc(28);
|
|
446
|
+
bitmapCap.writeUInt16LE(0x0002, 0);
|
|
447
|
+
bitmapCap.writeUInt16LE(28, 2);
|
|
448
|
+
bitmapCap.writeUInt16LE(24, 4); // preferredBitsPerPixel
|
|
449
|
+
bitmapCap.writeUInt16LE(1, 6); // receive1BitPerPixel
|
|
450
|
+
bitmapCap.writeUInt16LE(1, 8); // receive4BitsPerPixel
|
|
451
|
+
bitmapCap.writeUInt16LE(1, 10); // receive8BitsPerPixel
|
|
452
|
+
bitmapCap.writeUInt16LE(width, 12);
|
|
453
|
+
bitmapCap.writeUInt16LE(height, 14);
|
|
454
|
+
bitmapCap.writeUInt16LE(0, 16); // pad
|
|
455
|
+
bitmapCap.writeUInt16LE(1, 18); // desktopResizeFlag
|
|
456
|
+
bitmapCap.writeUInt16LE(1, 20); // bitmapCompressionFlag
|
|
457
|
+
bitmapCap.writeUInt8(0, 22); // highColorFlags
|
|
458
|
+
bitmapCap.writeUInt8(0, 23); // drawingFlags
|
|
459
|
+
bitmapCap.writeUInt16LE(1, 24); // multipleRectangleSupport
|
|
460
|
+
bitmapCap.writeUInt16LE(0, 26); // pad
|
|
461
|
+
caps.push(bitmapCap);
|
|
462
|
+
// CAPSTYPE_ORDER (0x0003) — minimal, no complex orders
|
|
463
|
+
const orderCap = Buffer.alloc(88);
|
|
464
|
+
orderCap.writeUInt16LE(0x0003, 0);
|
|
465
|
+
orderCap.writeUInt16LE(88, 2);
|
|
466
|
+
// terminalDescriptor: 16 bytes (zero)
|
|
467
|
+
orderCap.writeUInt32LE(0, 20); // pad4OctetsA
|
|
468
|
+
orderCap.writeUInt16LE(1, 24); // desktopSaveXGranularity
|
|
469
|
+
orderCap.writeUInt16LE(20, 26); // desktopSaveYGranularity
|
|
470
|
+
orderCap.writeUInt16LE(0, 28); // pad2OctetsA
|
|
471
|
+
orderCap.writeUInt16LE(1, 30); // maximumOrderLevel
|
|
472
|
+
orderCap.writeUInt16LE(0, 32); // numberFonts
|
|
473
|
+
orderCap.writeUInt16LE(0x0022, 34); // orderFlags: NEGOTIATEORDERSUPPORT | ZEROBOUNDSDELTASSUPPORT
|
|
474
|
+
// orderSupport: 32 bytes (all zero — no drawing orders)
|
|
475
|
+
orderCap.writeUInt16LE(0, 68); // textFlags
|
|
476
|
+
orderCap.writeUInt16LE(0, 70); // orderSupportExFlags
|
|
477
|
+
orderCap.writeUInt32LE(0, 72); // pad4OctetsB
|
|
478
|
+
orderCap.writeUInt32LE(230400, 76); // desktopSaveSize
|
|
479
|
+
orderCap.writeUInt16LE(0, 80); // pad2OctetsC
|
|
480
|
+
orderCap.writeUInt16LE(0, 82); // pad2OctetsD
|
|
481
|
+
orderCap.writeUInt16LE(0x006e, 84); // textANSICodePage
|
|
482
|
+
orderCap.writeUInt16LE(0, 86); // pad2OctetsE
|
|
483
|
+
caps.push(orderCap);
|
|
484
|
+
// CAPSTYPE_INPUT (0x000d)
|
|
485
|
+
const inputCap = Buffer.alloc(88);
|
|
486
|
+
inputCap.writeUInt16LE(0x000d, 0);
|
|
487
|
+
inputCap.writeUInt16LE(88, 2);
|
|
488
|
+
inputCap.writeUInt16LE(0x0001 | 0x0004 | 0x0020, 4); // INPUT_FLAG_SCANCODES | MOUSEX | UNICODE
|
|
489
|
+
inputCap.writeUInt16LE(0, 6); // pad
|
|
490
|
+
inputCap.writeUInt32LE(0x0409, 8); // keyboardLayout
|
|
491
|
+
inputCap.writeUInt32LE(4, 12); // keyboardType
|
|
492
|
+
inputCap.writeUInt32LE(0, 16); // keyboardSubType
|
|
493
|
+
inputCap.writeUInt32LE(12, 20); // keyboardFunctionKey
|
|
494
|
+
caps.push(inputCap);
|
|
495
|
+
// CAPSTYPE_POINTER (0x0008)
|
|
496
|
+
const ptrCap = Buffer.alloc(10);
|
|
497
|
+
ptrCap.writeUInt16LE(0x0008, 0);
|
|
498
|
+
ptrCap.writeUInt16LE(10, 2);
|
|
499
|
+
ptrCap.writeUInt16LE(0, 4); // colorPointerFlag
|
|
500
|
+
ptrCap.writeUInt16LE(20, 6); // colorPointerCacheSize
|
|
501
|
+
ptrCap.writeUInt16LE(21, 8); // pointerCacheSize
|
|
502
|
+
caps.push(ptrCap);
|
|
503
|
+
// CAPSTYPE_SHARE (0x0009)
|
|
504
|
+
const shareCap = Buffer.alloc(8);
|
|
505
|
+
shareCap.writeUInt16LE(0x0009, 0);
|
|
506
|
+
shareCap.writeUInt16LE(8, 2);
|
|
507
|
+
shareCap.writeUInt16LE(0, 4); // nodeId (client = 0)
|
|
508
|
+
shareCap.writeUInt16LE(0, 6); // pad
|
|
509
|
+
caps.push(shareCap);
|
|
510
|
+
// CAPSTYPE_COLORCACHE (0x000a)
|
|
511
|
+
const ccCap = Buffer.alloc(8);
|
|
512
|
+
ccCap.writeUInt16LE(0x000a, 0);
|
|
513
|
+
ccCap.writeUInt16LE(8, 2);
|
|
514
|
+
ccCap.writeUInt16LE(6, 4);
|
|
515
|
+
ccCap.writeUInt16LE(0, 6);
|
|
516
|
+
caps.push(ccCap);
|
|
517
|
+
// CAPSTYPE_CONTROL (0x0005)
|
|
518
|
+
const ctrlCap = Buffer.alloc(12);
|
|
519
|
+
ctrlCap.writeUInt16LE(0x0005, 0);
|
|
520
|
+
ctrlCap.writeUInt16LE(12, 2);
|
|
521
|
+
caps.push(ctrlCap);
|
|
522
|
+
// CAPSTYPE_ACTIVATION (0x0007)
|
|
523
|
+
const actCap = Buffer.alloc(12);
|
|
524
|
+
actCap.writeUInt16LE(0x0007, 0);
|
|
525
|
+
actCap.writeUInt16LE(12, 2);
|
|
526
|
+
caps.push(actCap);
|
|
527
|
+
// CAPSTYPE_FONT (0x000e)
|
|
528
|
+
const fontCap = Buffer.alloc(8);
|
|
529
|
+
fontCap.writeUInt16LE(0x000e, 0);
|
|
530
|
+
fontCap.writeUInt16LE(8, 2);
|
|
531
|
+
fontCap.writeUInt16LE(1, 4); // fontSupportFlags: FONTSUPPORT_FONTLIST
|
|
532
|
+
caps.push(fontCap);
|
|
533
|
+
// CAPSTYPE_BITMAPCACHE (0x0004)
|
|
534
|
+
const bmpCache = Buffer.alloc(40);
|
|
535
|
+
bmpCache.writeUInt16LE(0x0004, 0);
|
|
536
|
+
bmpCache.writeUInt16LE(40, 2);
|
|
537
|
+
caps.push(bmpCache);
|
|
538
|
+
// CAPSTYPE_BRUSHSUPPORT (0x000f)
|
|
539
|
+
const brushCap = Buffer.alloc(8);
|
|
540
|
+
brushCap.writeUInt16LE(0x000f, 0);
|
|
541
|
+
brushCap.writeUInt16LE(8, 2);
|
|
542
|
+
caps.push(brushCap);
|
|
543
|
+
// CAPSTYPE_GLYPHCACHE (0x0010)
|
|
544
|
+
const glyphCap = Buffer.alloc(52);
|
|
545
|
+
glyphCap.writeUInt16LE(0x0010, 0);
|
|
546
|
+
glyphCap.writeUInt16LE(52, 2);
|
|
547
|
+
glyphCap.writeUInt8(0, 50); // glyphSupportLevel = GLYPH_SUPPORT_NONE
|
|
548
|
+
caps.push(glyphCap);
|
|
549
|
+
const capsData = Buffer.concat(caps);
|
|
550
|
+
// ConfirmActive PDU body: shareId(4)+originatorId(2)+lengthSrcDesc(2)+lengthCombCaps(2)
|
|
551
|
+
// + sourceDescriptor(9) + numberCapabilities(2) + pad(2) = 23 bytes
|
|
552
|
+
const body = Buffer.alloc(23);
|
|
553
|
+
body.writeUInt32LE(shareId, 0);
|
|
554
|
+
body.writeUInt16LE(0x03EA, 4); // originatorId (client)
|
|
555
|
+
body.writeUInt16LE(9, 6); // lengthSourceDescriptor
|
|
556
|
+
body.writeUInt16LE(capsData.length + 4, 8); // lengthCombinedCapabilities (+4 for numCaps+pad)
|
|
557
|
+
Buffer.from('autodev\0\0', 'ascii').copy(body, 10); // sourceDescriptor (9 bytes, ends at 18)
|
|
558
|
+
body.writeUInt16LE(caps.length, 19); // numberCapabilities — immediately after sourceDescriptor
|
|
559
|
+
body.writeUInt16LE(0, 21); // pad2Octets
|
|
560
|
+
const pduBody = Buffer.concat([body, capsData]);
|
|
561
|
+
// Share Control Header
|
|
562
|
+
const shareCtrl = Buffer.alloc(6);
|
|
563
|
+
const pduLen = 6 + pduBody.length;
|
|
564
|
+
shareCtrl.writeUInt16LE(pduLen, 0);
|
|
565
|
+
shareCtrl.writeUInt16LE(constants_1.PDUTYPE_CONFIRMACTIVEPDU | 0x10, 2); // type | version
|
|
566
|
+
shareCtrl.writeUInt16LE(userId, 4); // PDUSource
|
|
567
|
+
return Buffer.concat([shareCtrl, pduBody]);
|
|
568
|
+
}
|
|
569
|
+
// ── Synchronize / Control / Font PDUs ────────────────────────────────────
|
|
570
|
+
function buildSynchronizePdu(userId, shareId) {
|
|
571
|
+
const hdr = buildShareDataHeader(userId, shareId, constants_1.PDUTYPE2_SYNCHRONIZE, 4);
|
|
572
|
+
const body = Buffer.alloc(4);
|
|
573
|
+
body.writeUInt16LE(1, 0); // messageType = SYNCMSGTYPE_SYNC
|
|
574
|
+
body.writeUInt16LE(1002, 2); // targetUser (MCS user ID)
|
|
575
|
+
return Buffer.concat([hdr, body]);
|
|
576
|
+
}
|
|
577
|
+
function buildControlPdu(userId, shareId, action) {
|
|
578
|
+
const hdr = buildShareDataHeader(userId, shareId, constants_1.PDUTYPE2_CONTROL, 8);
|
|
579
|
+
const body = Buffer.alloc(8);
|
|
580
|
+
body.writeUInt16LE(action, 0);
|
|
581
|
+
body.writeUInt16LE(0, 2);
|
|
582
|
+
body.writeUInt32LE(0, 4);
|
|
583
|
+
return Buffer.concat([hdr, body]);
|
|
584
|
+
}
|
|
585
|
+
function buildFontListPdu(userId, shareId) {
|
|
586
|
+
const hdr = buildShareDataHeader(userId, shareId, constants_1.PDUTYPE2_FONTLIST, 8);
|
|
587
|
+
const body = Buffer.alloc(8);
|
|
588
|
+
body.writeUInt16LE(0, 0); // numberFonts
|
|
589
|
+
body.writeUInt16LE(0, 2); // totalNumFonts
|
|
590
|
+
body.writeUInt16LE(0x0003, 4); // listFlags
|
|
591
|
+
body.writeUInt16LE(50, 6); // entrySize
|
|
592
|
+
return Buffer.concat([hdr, body]);
|
|
593
|
+
}
|
|
594
|
+
function buildShareDataHeader(userId, shareId, pduType2, dataLen) {
|
|
595
|
+
// Share Control Header (6) + Share Data Header (12) = 18 bytes
|
|
596
|
+
const totalBodyLen = dataLen;
|
|
597
|
+
const pduLen = 18 + totalBodyLen;
|
|
598
|
+
const hdr = Buffer.alloc(18);
|
|
599
|
+
// Share Control Header
|
|
600
|
+
hdr.writeUInt16LE(pduLen, 0);
|
|
601
|
+
hdr.writeUInt16LE(constants_1.PDUTYPE_DATAPDU | 0x10, 2); // type | RDP5 version
|
|
602
|
+
hdr.writeUInt16LE(userId, 4); // PDUSource
|
|
603
|
+
// Share Data Header (TS_SHAREDATAHEADER, 12 bytes at hdr[6..17])
|
|
604
|
+
hdr.writeUInt32LE(shareId, 6); // shareId
|
|
605
|
+
hdr[10] = 0; // pad1Octet
|
|
606
|
+
hdr[11] = 1; // streamId (STREAM_LOW)
|
|
607
|
+
hdr.writeUInt16LE(totalBodyLen, 12); // uncompressedLength
|
|
608
|
+
hdr[14] = pduType2; // pduType2
|
|
609
|
+
hdr[15] = 0; // compressType
|
|
610
|
+
hdr.writeUInt16LE(totalBodyLen, 16); // compressedLength
|
|
611
|
+
return hdr.slice(0, 18);
|
|
612
|
+
}
|
|
613
|
+
function buildRefreshRectPdu(userId, shareId, width, height) {
|
|
614
|
+
// TS_REFRESH_RECT_PDU: numberOfAreas(1) + pad(3) + InclusiveRect (left/top/right/bottom each uint16)
|
|
615
|
+
const body = Buffer.alloc(12);
|
|
616
|
+
body[0] = 1; // numberOfAreas = 1
|
|
617
|
+
body.writeUInt16LE(0, 4); // left
|
|
618
|
+
body.writeUInt16LE(0, 6); // top
|
|
619
|
+
body.writeUInt16LE(width - 1, 8); // right
|
|
620
|
+
body.writeUInt16LE(height - 1, 10); // bottom
|
|
621
|
+
const hdr = buildShareDataHeader(userId, shareId, constants_1.PDUTYPE2_REFRESH_RECT, body.length);
|
|
622
|
+
return Buffer.concat([hdr, body]);
|
|
623
|
+
}
|
|
624
|
+
function buildSuppressOutputPdu(userId, shareId, allow, width = 0, height = 0) {
|
|
625
|
+
// TS_SUPPRESS_OUTPUT_PDU: allowDisplayUpdates(1) + pad(3) [+ desktopRect(8) when allow=true]
|
|
626
|
+
const body = allow ? Buffer.alloc(12) : Buffer.alloc(4);
|
|
627
|
+
body[0] = allow ? 1 : 0;
|
|
628
|
+
if (allow) {
|
|
629
|
+
// pad3Octets[1..3] already zero
|
|
630
|
+
body.writeUInt16LE(0, 4); // left
|
|
631
|
+
body.writeUInt16LE(0, 6); // top
|
|
632
|
+
body.writeUInt16LE(width - 1, 8); // right
|
|
633
|
+
body.writeUInt16LE(height - 1, 10); // bottom
|
|
634
|
+
}
|
|
635
|
+
const hdr = buildShareDataHeader(userId, shareId, constants_1.PDUTYPE2_SUPPRESS_OUTPUT, body.length);
|
|
636
|
+
return Buffer.concat([hdr, body]);
|
|
637
|
+
}
|
|
638
|
+
// ── Bitmap decompression (RLE — MS-RDPEGDI 3.1.9) ─────────────────────────
|
|
639
|
+
function decompressBitmap(src, width, height, bytesPerPixel) {
|
|
640
|
+
const rowSize = width * bytesPerPixel;
|
|
641
|
+
const dst = Buffer.alloc(rowSize * height);
|
|
642
|
+
let si = 0;
|
|
643
|
+
let di = 0;
|
|
644
|
+
const readPixel = () => {
|
|
645
|
+
if (si + bytesPerPixel > src.length)
|
|
646
|
+
return Buffer.alloc(bytesPerPixel);
|
|
647
|
+
const p = src.slice(si, si + bytesPerPixel);
|
|
648
|
+
si += bytesPerPixel;
|
|
649
|
+
return p;
|
|
650
|
+
};
|
|
651
|
+
while (si < src.length && di < dst.length) {
|
|
652
|
+
if (si >= src.length)
|
|
653
|
+
break;
|
|
654
|
+
const code = src[si++];
|
|
655
|
+
const type = code >> 4;
|
|
656
|
+
let len = code & 0x0f;
|
|
657
|
+
if (len === 0) {
|
|
658
|
+
if (si >= src.length)
|
|
659
|
+
break;
|
|
660
|
+
len = src[si++] + 16;
|
|
661
|
+
}
|
|
662
|
+
if (type === 0x0) {
|
|
663
|
+
// FILL — copy from scanline above
|
|
664
|
+
for (let i = 0; i < len && di < dst.length; i++, di++) {
|
|
665
|
+
dst[di] = di >= rowSize ? dst[di - rowSize] : 0;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else if (type === 0x8) {
|
|
669
|
+
// MIX — XOR with previous scanline pixel
|
|
670
|
+
const pixel = readPixel();
|
|
671
|
+
for (let i = 0; i < len && di < dst.length; i++, di += bytesPerPixel) {
|
|
672
|
+
for (let b = 0; b < bytesPerPixel; b++) {
|
|
673
|
+
dst[di + b] = di >= rowSize ? dst[di - rowSize + b] ^ pixel[b] : pixel[b];
|
|
674
|
+
}
|
|
675
|
+
di -= bytesPerPixel;
|
|
676
|
+
di += bytesPerPixel;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else if (type === 0xc) {
|
|
680
|
+
// COLOR — repeat pixel
|
|
681
|
+
const pixel = readPixel();
|
|
682
|
+
for (let i = 0; i < len && di + bytesPerPixel <= dst.length; i++, di += bytesPerPixel) {
|
|
683
|
+
pixel.copy(dst, di);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (type === 0xf) {
|
|
687
|
+
// COPY — raw pixels
|
|
688
|
+
for (let i = 0; i < len && di + bytesPerPixel <= dst.length; i++, di += bytesPerPixel) {
|
|
689
|
+
readPixel().copy(dst, di);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
// Unknown / unhandled run type — skip
|
|
694
|
+
si += len * bytesPerPixel;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return dst;
|
|
698
|
+
}
|
|
699
|
+
// ── Convert various bit depths to RGBA ────────────────────────────────────
|
|
700
|
+
function toRgba(data, bytesPerPixel) {
|
|
701
|
+
if (bytesPerPixel === 4) {
|
|
702
|
+
// Assume BGRX (Windows native) → RGBA
|
|
703
|
+
const out = Buffer.alloc(data.length);
|
|
704
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
705
|
+
out[i] = data[i + 2]; // R
|
|
706
|
+
out[i + 1] = data[i + 1]; // G
|
|
707
|
+
out[i + 2] = data[i]; // B
|
|
708
|
+
out[i + 3] = 0xff; // A
|
|
709
|
+
}
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
if (bytesPerPixel === 3) {
|
|
713
|
+
// BGR → RGBA
|
|
714
|
+
const pixels = Math.floor(data.length / 3);
|
|
715
|
+
const out = Buffer.alloc(pixels * 4);
|
|
716
|
+
for (let i = 0; i < pixels; i++) {
|
|
717
|
+
out[i * 4] = data[i * 3 + 2]; // R
|
|
718
|
+
out[i * 4 + 1] = data[i * 3 + 1]; // G
|
|
719
|
+
out[i * 4 + 2] = data[i * 3]; // B
|
|
720
|
+
out[i * 4 + 3] = 0xff;
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
if (bytesPerPixel === 2) {
|
|
725
|
+
// RGB565 → RGBA
|
|
726
|
+
const pixels = Math.floor(data.length / 2);
|
|
727
|
+
const out = Buffer.alloc(pixels * 4);
|
|
728
|
+
for (let i = 0; i < pixels; i++) {
|
|
729
|
+
const v = data.readUInt16LE(i * 2);
|
|
730
|
+
out[i * 4] = ((v >> 11) & 0x1f) << 3;
|
|
731
|
+
out[i * 4 + 1] = ((v >> 5) & 0x3f) << 2;
|
|
732
|
+
out[i * 4 + 2] = (v & 0x1f) << 3;
|
|
733
|
+
out[i * 4 + 3] = 0xff;
|
|
734
|
+
}
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
737
|
+
// 1 bpp / other — return as-is with alpha=0xff
|
|
738
|
+
const out = Buffer.alloc(data.length * 2);
|
|
739
|
+
for (let i = 0; i < data.length; i++) {
|
|
740
|
+
out[i * 2] = data[i];
|
|
741
|
+
out[i * 2 + 1] = 0xff;
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
// ── RdpBridge class ───────────────────────────────────────────────────────
|
|
746
|
+
class RdpBridge extends events_1.EventEmitter {
|
|
747
|
+
_sock = null;
|
|
748
|
+
_closed = false;
|
|
749
|
+
_recvBuf = Buffer.alloc(0);
|
|
750
|
+
_handshakeBuf = Buffer.alloc(0); // raw-byte accumulator for handshake TPKT parsing
|
|
751
|
+
_width = constants_1.RDP_DEFAULT_WIDTH;
|
|
752
|
+
_height = constants_1.RDP_DEFAULT_HEIGHT;
|
|
753
|
+
_colorDepth = constants_1.RDP_DEFAULT_COLOR_DEPTH;
|
|
754
|
+
_userId = 0;
|
|
755
|
+
_shareId = 0;
|
|
756
|
+
_connected = false;
|
|
757
|
+
// Auto-reconnect support (xrdp closes TCP after initial handshake, expects client to reconnect)
|
|
758
|
+
_opts = null;
|
|
759
|
+
_reconnectCount = 0;
|
|
760
|
+
static _MAX_RECONNECTS = 10;
|
|
761
|
+
/** Optional external logger — set before calling connect(). */
|
|
762
|
+
log = (msg) => console.error(msg);
|
|
763
|
+
get width() { return this._width; }
|
|
764
|
+
get height() { return this._height; }
|
|
765
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
766
|
+
/**
|
|
767
|
+
* Connect to an RDP server. Resolves once the desktop is ready.
|
|
768
|
+
*/
|
|
769
|
+
async connect(opts) {
|
|
770
|
+
this._opts = opts; // save for auto-reconnect
|
|
771
|
+
const host = opts.host;
|
|
772
|
+
const port = opts.port ?? constants_1.RDP_DEFAULT_PORT;
|
|
773
|
+
const username = opts.username ?? '';
|
|
774
|
+
const password = opts.password ?? '';
|
|
775
|
+
const domain = opts.domain ?? '';
|
|
776
|
+
const width = opts.width ?? constants_1.RDP_DEFAULT_WIDTH;
|
|
777
|
+
const height = opts.height ?? constants_1.RDP_DEFAULT_HEIGHT;
|
|
778
|
+
const colorDepth = opts.colorDepth ?? constants_1.RDP_DEFAULT_COLOR_DEPTH;
|
|
779
|
+
this._width = width;
|
|
780
|
+
this._height = height;
|
|
781
|
+
this._colorDepth = colorDepth;
|
|
782
|
+
return new Promise((resolve, reject) => {
|
|
783
|
+
// connectResolved: true once the initial handshake has resolved the Promise.
|
|
784
|
+
// The rawSock 'close' listener uses this guard so it doesn't emit 'close'
|
|
785
|
+
// after the TLS socket takes over (the TLS 'close' handler manages reconnect).
|
|
786
|
+
let connectResolved = false;
|
|
787
|
+
const sock = net.createConnection({ host, port });
|
|
788
|
+
this._sock = sock;
|
|
789
|
+
sock.once('error', (err) => {
|
|
790
|
+
if (!this._closed) {
|
|
791
|
+
this._closed = true;
|
|
792
|
+
this.emit('error', err);
|
|
793
|
+
reject(err);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
sock.once('close', () => {
|
|
797
|
+
// Only fire if the Promise never resolved (pre-Phase-8 failure on raw socket)
|
|
798
|
+
if (!connectResolved && !this._closed) {
|
|
799
|
+
this._closed = true;
|
|
800
|
+
this.emit('close');
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
sock.once('connect', async () => {
|
|
804
|
+
try {
|
|
805
|
+
await this._handshake(sock, host, port, username, password, domain, width, height, colorDepth, (info) => { connectResolved = true; resolve(info); }, reject);
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
809
|
+
sock.destroy();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
/** Send a mouse event. buttonMask: bit0=left, bit1=right, bit2=middle */
|
|
815
|
+
sendMouse(x, y, buttonMask) {
|
|
816
|
+
if (!this._userId || !this._connected)
|
|
817
|
+
return;
|
|
818
|
+
let flags = constants_1.PTRFLAGS_MOVE;
|
|
819
|
+
if (buttonMask & 1)
|
|
820
|
+
flags |= constants_1.PTRFLAGS_BUTTON1 | constants_1.PTRFLAGS_DOWN;
|
|
821
|
+
if (buttonMask & 2)
|
|
822
|
+
flags |= constants_1.PTRFLAGS_BUTTON2 | constants_1.PTRFLAGS_DOWN;
|
|
823
|
+
if (buttonMask & 4)
|
|
824
|
+
flags |= constants_1.PTRFLAGS_BUTTON3 | constants_1.PTRFLAGS_DOWN;
|
|
825
|
+
this._sendInput(constants_1.INPUT_EVENT_MOUSE, Buffer.from([
|
|
826
|
+
flags & 0xff, (flags >> 8) & 0xff,
|
|
827
|
+
x & 0xff, (x >> 8) & 0xff,
|
|
828
|
+
y & 0xff, (y >> 8) & 0xff,
|
|
829
|
+
]));
|
|
830
|
+
}
|
|
831
|
+
/** Send a key event. jsKeyCode is the browser keyCode value. */
|
|
832
|
+
sendKey(jsKeyCode, down) {
|
|
833
|
+
if (!this._userId || !this._connected)
|
|
834
|
+
return;
|
|
835
|
+
const scanCode = constants_1.RDP_SCANCODE[jsKeyCode] ?? jsKeyCode;
|
|
836
|
+
const isExt = constants_1.RDP_EXTENDED_KEYS.has(jsKeyCode);
|
|
837
|
+
let flags = down ? 0 : constants_1.KBDFLAGS_RELEASE;
|
|
838
|
+
if (isExt)
|
|
839
|
+
flags |= constants_1.KBDFLAGS_EXTENDED;
|
|
840
|
+
this._sendInput(constants_1.INPUT_EVENT_SCANCODE, Buffer.from([
|
|
841
|
+
flags & 0xff, (flags >> 8) & 0xff,
|
|
842
|
+
scanCode & 0xff, (scanCode >> 8) & 0xff,
|
|
843
|
+
0, 0,
|
|
844
|
+
]));
|
|
845
|
+
}
|
|
846
|
+
/** Push local clipboard text to the remote. */
|
|
847
|
+
sendClipboardText(text) {
|
|
848
|
+
// Clipboard redirection requires the cliprdr channel (Phase 2).
|
|
849
|
+
// For now, log silently — no-op until channel join is implemented.
|
|
850
|
+
void text;
|
|
851
|
+
}
|
|
852
|
+
close() {
|
|
853
|
+
if (!this._closed) {
|
|
854
|
+
this._closed = true;
|
|
855
|
+
this._sock?.destroy();
|
|
856
|
+
this._sock = null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// ── Auto-reconnect ─────────────────────────────────────────────────────
|
|
860
|
+
/**
|
|
861
|
+
* Reconnect using saved opts — called after xrdp's clean TCP close that
|
|
862
|
+
* follows the initial session-creation handshake.
|
|
863
|
+
*/
|
|
864
|
+
_doReconnect() {
|
|
865
|
+
if (this._closed || !this._opts)
|
|
866
|
+
return;
|
|
867
|
+
const opts = this._opts;
|
|
868
|
+
const host = opts.host;
|
|
869
|
+
const port = opts.port ?? constants_1.RDP_DEFAULT_PORT;
|
|
870
|
+
const username = opts.username ?? '';
|
|
871
|
+
const password = opts.password ?? '';
|
|
872
|
+
const domain = opts.domain ?? '';
|
|
873
|
+
const width = opts.width ?? constants_1.RDP_DEFAULT_WIDTH;
|
|
874
|
+
const height = opts.height ?? constants_1.RDP_DEFAULT_HEIGHT;
|
|
875
|
+
const colorDepth = opts.colorDepth ?? constants_1.RDP_DEFAULT_COLOR_DEPTH;
|
|
876
|
+
this._width = width;
|
|
877
|
+
this._height = height;
|
|
878
|
+
this._colorDepth = colorDepth;
|
|
879
|
+
const sock = net.createConnection({ host, port });
|
|
880
|
+
this._sock = sock;
|
|
881
|
+
sock.once('error', (err) => {
|
|
882
|
+
this.log(`[RDP] reconnect socket error: ${err.message}`);
|
|
883
|
+
if (err.code === 'ECONNRESET' && this._connected)
|
|
884
|
+
return;
|
|
885
|
+
if (!this._closed) {
|
|
886
|
+
this._closed = true;
|
|
887
|
+
this.emit('error', err);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
sock.once('connect', async () => {
|
|
891
|
+
try {
|
|
892
|
+
await this._handshake(sock, host, port, username, password, domain, width, height, colorDepth, (_info) => { this.log('[RDP] reconnect complete — desktop ready'); }, (err) => {
|
|
893
|
+
this.log(`[RDP] reconnect handshake failed: ${err.message}`);
|
|
894
|
+
if (!this._closed) {
|
|
895
|
+
this._closed = true;
|
|
896
|
+
this.emit('error', err);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
catch (err) {
|
|
901
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
902
|
+
this.log(`[RDP] reconnect exception: ${e.message}`);
|
|
903
|
+
if (!this._closed) {
|
|
904
|
+
this._closed = true;
|
|
905
|
+
this.emit('error', e);
|
|
906
|
+
}
|
|
907
|
+
sock.destroy();
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
// ── Handshake ──────────────────────────────────────────────────────────
|
|
912
|
+
async _handshake(rawSock, host, _port, username, password, domain, width, height, colorDepth, resolve, reject) {
|
|
913
|
+
// ── Phase 1: X.224 negotiation ─────────────────────────────────────
|
|
914
|
+
const reqProtocol = (0, auth_1.selectRequestProtocol)();
|
|
915
|
+
rawSock.write((0, auth_1.buildX224ConnectRequest)(reqProtocol, username));
|
|
916
|
+
const cc = await this._readTpkt(rawSock);
|
|
917
|
+
const selectedProtocol = (0, auth_1.parseX224ConnectConfirm)(cc);
|
|
918
|
+
this.log(`[RDP] Phase1 done — selectedProtocol=${selectedProtocol}`);
|
|
919
|
+
// ── Phase 2: TLS upgrade ───────────────────────────────────────────
|
|
920
|
+
let sock = rawSock;
|
|
921
|
+
if (selectedProtocol !== 0 /* PROTOCOL_RDP */) {
|
|
922
|
+
sock = await (0, auth_1.upgradeTls)(rawSock, host);
|
|
923
|
+
this._sock = sock;
|
|
924
|
+
}
|
|
925
|
+
this.log(`[RDP] Phase2 done — TLS=${selectedProtocol !== 0}`);
|
|
926
|
+
// Attach error/close monitors — these must be in place before we send
|
|
927
|
+
// anything so we can surface failures immediately. We do NOT install the
|
|
928
|
+
// _recvBuf 'data' listener here; that is done inside _runLoop so that
|
|
929
|
+
// handshake packets read via _readTpkt are not also buffered in _recvBuf.
|
|
930
|
+
sock.on('error', (err) => {
|
|
931
|
+
this.log(`[RDP] socket error: ${err.message}`);
|
|
932
|
+
// ECONNRESET while connected = xrdp closing for reconnect; let 'close' handler decide
|
|
933
|
+
if (err.code === 'ECONNRESET' && this._connected)
|
|
934
|
+
return;
|
|
935
|
+
if (!this._closed) {
|
|
936
|
+
this._closed = true;
|
|
937
|
+
this.emit('error', err);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
sock.on('close', () => {
|
|
941
|
+
this.log(`[RDP] socket closed (connected=${this._connected})`);
|
|
942
|
+
if (this._closed)
|
|
943
|
+
return;
|
|
944
|
+
// xrdp closes TCP after initial session setup — auto-reconnect for actual desktop
|
|
945
|
+
if (this._connected && this._opts && this._reconnectCount < RdpBridge._MAX_RECONNECTS) {
|
|
946
|
+
this._reconnectCount++;
|
|
947
|
+
this.log(`[RDP] clean close — auto-reconnect ${this._reconnectCount}/${RdpBridge._MAX_RECONNECTS}`);
|
|
948
|
+
this._connected = false;
|
|
949
|
+
this._userId = 0;
|
|
950
|
+
this._shareId = 0;
|
|
951
|
+
this._recvBuf = Buffer.alloc(0);
|
|
952
|
+
this._handshakeBuf = Buffer.alloc(0);
|
|
953
|
+
this._sock = null;
|
|
954
|
+
// Progressive delay: give xrdp time to start the X session backend
|
|
955
|
+
const delayMs = Math.min(1000 * this._reconnectCount, 5000);
|
|
956
|
+
setTimeout(() => this._doReconnect(), delayMs);
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
this._closed = true;
|
|
960
|
+
this.emit('close');
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
// ── Phase 3: MCS Connect ───────────────────────────────────────────
|
|
964
|
+
const mcsInit = buildMcsConnectInitial(width, height, colorDepth);
|
|
965
|
+
this._sendX224Data(sock, mcsInit);
|
|
966
|
+
const mcsResp = await this._readTpktPayload(sock);
|
|
967
|
+
// Parse MCS ConnectResponse to extract server data (GCC) — we only need userId seed
|
|
968
|
+
// Most fields are not critical for basic connectivity; skip deep parsing for now.
|
|
969
|
+
void mcsResp;
|
|
970
|
+
// ── Phase 4: MCS Erect Domain + Attach User ────────────────────────
|
|
971
|
+
this._sendX224Data(sock, Buffer.from([constants_1.MCS_ERECT_DOMAIN_REQUEST, 0x00, 0x01, 0x00, 0x01]));
|
|
972
|
+
this._sendX224Data(sock, Buffer.from([constants_1.MCS_ATTACH_USER_REQUEST]));
|
|
973
|
+
const auConf = await this._readTpktPayload(sock);
|
|
974
|
+
if (auConf[0] !== constants_1.MCS_ATTACH_USER_CONFIRM) {
|
|
975
|
+
throw new Error(`Expected MCS_ATTACH_USER_CONFIRM, got 0x${auConf[0].toString(16)}`);
|
|
976
|
+
}
|
|
977
|
+
this._userId = 1001 + auConf.readUInt16BE(2);
|
|
978
|
+
this.log(`[RDP] Phase4 done — userId=${this._userId}`);
|
|
979
|
+
// ── Phase 5: Channel joins ─────────────────────────────────────────
|
|
980
|
+
for (const channelId of [this._userId, constants_1.MCS_CHANNEL_GLOBAL]) {
|
|
981
|
+
const joinReq = Buffer.alloc(5);
|
|
982
|
+
joinReq[0] = constants_1.MCS_CHANNEL_JOIN_REQUEST;
|
|
983
|
+
joinReq.writeUInt16BE(this._userId - 1001, 1);
|
|
984
|
+
joinReq.writeUInt16BE(channelId, 3);
|
|
985
|
+
this._sendX224Data(sock, joinReq);
|
|
986
|
+
const joinConf = await this._readTpktPayload(sock);
|
|
987
|
+
if (joinConf[0] !== constants_1.MCS_CHANNEL_JOIN_CONFIRM) {
|
|
988
|
+
throw new Error(`MCS channel join failed for channel ${channelId}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
// ── Phase 6: Client Info (login) ───────────────────────────────────
|
|
992
|
+
const clientInfo = buildClientInfoPdu(username, password, domain);
|
|
993
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, clientInfo);
|
|
994
|
+
this.log(`[RDP] Phase6 sent ClientInfo`);
|
|
995
|
+
// ── Phase 6b: License exchange ─────────────────────────────────────
|
|
996
|
+
await this._doLicenseExchange(sock, username);
|
|
997
|
+
this.log(`[RDP] Phase6b license exchange done`);
|
|
998
|
+
// ── Phase 7: Capability exchange ──────────────────────────────────
|
|
999
|
+
// Wait for Demand Active PDU from server
|
|
1000
|
+
const demandPdu = await this._waitForPduType(sock, constants_1.PDUTYPE_DEMANDACTIVEPDU, 15_000);
|
|
1001
|
+
this._shareId = demandPdu.readUInt32LE(6); // shareId in Share Control Header
|
|
1002
|
+
this.log(`[RDP] Phase7 DemandActive — shareId=0x${this._shareId.toString(16)}`);
|
|
1003
|
+
const confirmActive = buildConfirmActivePdu(this._userId, this._shareId, width, height);
|
|
1004
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, confirmActive);
|
|
1005
|
+
this.log(`[RDP] Phase7 ConfirmActive sent, sending sync/ctrl/fontlist`);
|
|
1006
|
+
// Send synchronize + control + font list (required sequence per MS-RDPBCGR §1.3.1.1)
|
|
1007
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, buildSynchronizePdu(this._userId, this._shareId));
|
|
1008
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, buildControlPdu(this._userId, this._shareId, 4 /*CTRLACTION_COOPERATE*/));
|
|
1009
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, buildControlPdu(this._userId, this._shareId, 1 /*CTRLACTION_REQUEST_CONTROL*/));
|
|
1010
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, buildFontListPdu(this._userId, this._shareId));
|
|
1011
|
+
// ── Phase 8: Running ───────────────────────────────────────────────
|
|
1012
|
+
this._connected = true;
|
|
1013
|
+
const info = {
|
|
1014
|
+
name: host,
|
|
1015
|
+
width,
|
|
1016
|
+
height,
|
|
1017
|
+
colorDepth,
|
|
1018
|
+
};
|
|
1019
|
+
resolve(info);
|
|
1020
|
+
// Start continuous parsing
|
|
1021
|
+
this._runLoop(sock);
|
|
1022
|
+
}
|
|
1023
|
+
// ── License exchange ──────────────────────────────────────────────────
|
|
1024
|
+
/**
|
|
1025
|
+
* Handle RDP license exchange after Client Info PDU.
|
|
1026
|
+
* Responds to SERVER_LICENSE_REQUEST and PLATFORM_CHALLENGE until the
|
|
1027
|
+
* server completes licensing (or sends a non-license PDU).
|
|
1028
|
+
*/
|
|
1029
|
+
async _doLicenseExchange(sock, username) {
|
|
1030
|
+
const SEC_LICENSE_PKT = 0x0080;
|
|
1031
|
+
let clientRand;
|
|
1032
|
+
let preMaster;
|
|
1033
|
+
let serverRand;
|
|
1034
|
+
let macKey;
|
|
1035
|
+
let encKey;
|
|
1036
|
+
for (let i = 0; i < 10; i++) {
|
|
1037
|
+
const tpkt = await this._readTpkt(sock);
|
|
1038
|
+
// Parse MCS SDI header to find the RDP/license payload
|
|
1039
|
+
const payload = tpkt.slice(7); // skip TPKT(4)+X224(3)
|
|
1040
|
+
if (payload.length < 7 || payload[0] !== constants_1.MCS_SEND_DATA_INDICATION) {
|
|
1041
|
+
// Not an SDI — put it back and stop
|
|
1042
|
+
this._handshakeBuf = Buffer.concat([tpkt, this._handshakeBuf]);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
let off = 6;
|
|
1046
|
+
const lb = payload[off++];
|
|
1047
|
+
if (lb & 0x80)
|
|
1048
|
+
off++; // T.125 PER 2-byte form: [0x80|hi, lo] — skip the low byte
|
|
1049
|
+
const rdp = payload.slice(off);
|
|
1050
|
+
if (rdp.length < 6)
|
|
1051
|
+
continue;
|
|
1052
|
+
const secFlags = rdp.readUInt16LE(0);
|
|
1053
|
+
if (!(secFlags & SEC_LICENSE_PKT)) {
|
|
1054
|
+
// Not a license packet (could be Demand Active) — put it back
|
|
1055
|
+
this._handshakeBuf = Buffer.concat([tpkt, this._handshakeBuf]);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const body = rdp.slice(4); // skip 4-byte security header
|
|
1059
|
+
const msgType = body[0];
|
|
1060
|
+
if (msgType === 0x01) { // SERVER_LICENSE_REQUEST
|
|
1061
|
+
const { serverRand: sr, modulusLe, exponent } = parseLicenseRequest(body);
|
|
1062
|
+
serverRand = sr;
|
|
1063
|
+
clientRand = crypto.randomBytes(32);
|
|
1064
|
+
preMaster = crypto.randomBytes(48);
|
|
1065
|
+
const encPms = rsaEncryptLe(preMaster, modulusLe, exponent);
|
|
1066
|
+
const req = buildNewLicenseRequest(clientRand, encPms, username, 'autodev');
|
|
1067
|
+
const secHdr = Buffer.from([0x80, 0x00, 0x00, 0x00]); // SEC_LICENSE_PKT
|
|
1068
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, Buffer.concat([secHdr, req]));
|
|
1069
|
+
}
|
|
1070
|
+
else if (msgType === 0x02 && clientRand && preMaster && serverRand) { // PLATFORM_CHALLENGE
|
|
1071
|
+
[macKey, encKey] = deriveLicenseKeys(preMaster, clientRand, serverRand);
|
|
1072
|
+
const chalLen = body.readUInt16LE(6);
|
|
1073
|
+
const encChal = body.slice(8, 8 + chalLen);
|
|
1074
|
+
const challenge = rc4(encKey, encChal);
|
|
1075
|
+
const resp = buildPlatformChallengeResponse(encKey, macKey, challenge);
|
|
1076
|
+
const secHdr = Buffer.from([0x80, 0x00, 0x00, 0x00]);
|
|
1077
|
+
this._sendMcsData(sock, constants_1.MCS_CHANNEL_GLOBAL, Buffer.concat([secHdr, resp]));
|
|
1078
|
+
}
|
|
1079
|
+
else if (msgType === 0x03 || msgType === 0xff) {
|
|
1080
|
+
// NEW_LICENSE or ERROR_ALERT — licensing done regardless of error code
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// ── Run loop ─────────────────────────────────────────────────────────
|
|
1086
|
+
_runLoop(sock) {
|
|
1087
|
+
this.log(`[RDP] runLoop started (handshakeBuf=${this._handshakeBuf.length} bytes)`);
|
|
1088
|
+
const pump = () => {
|
|
1089
|
+
while (true) {
|
|
1090
|
+
const pdu = this._tryParseTpkt();
|
|
1091
|
+
if (!pdu)
|
|
1092
|
+
break;
|
|
1093
|
+
try {
|
|
1094
|
+
this._dispatchPdu(pdu);
|
|
1095
|
+
}
|
|
1096
|
+
catch (e) {
|
|
1097
|
+
this.log(`[RDP] dispatchPdu error: ${e}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
// Drain any bytes left over from handshake phase into _recvBuf
|
|
1102
|
+
if (this._handshakeBuf.length > 0) {
|
|
1103
|
+
this._recvBuf = Buffer.concat([this._recvBuf, this._handshakeBuf]);
|
|
1104
|
+
this._handshakeBuf = Buffer.alloc(0);
|
|
1105
|
+
}
|
|
1106
|
+
// Install _recvBuf listener here (not during handshake) so that
|
|
1107
|
+
// _readTpkt's own listeners are the sole consumers during negotiation.
|
|
1108
|
+
sock.on('data', (chunk) => {
|
|
1109
|
+
this._recvBuf = Buffer.concat([this._recvBuf, chunk]);
|
|
1110
|
+
pump();
|
|
1111
|
+
});
|
|
1112
|
+
pump();
|
|
1113
|
+
}
|
|
1114
|
+
_dispatchPdu(data) {
|
|
1115
|
+
if (data.length < 4)
|
|
1116
|
+
return;
|
|
1117
|
+
// X.224 Data TPDU header is 3 bytes (LI=2, code=F0, EOT=80)
|
|
1118
|
+
const x224Start = 4; // after TPKT
|
|
1119
|
+
if (data[x224Start + 1] !== constants_1.X224_TPDU_DATA)
|
|
1120
|
+
return;
|
|
1121
|
+
const payload = data.slice(x224Start + 3);
|
|
1122
|
+
if (payload.length < 1)
|
|
1123
|
+
return;
|
|
1124
|
+
const mcsOpcode = payload[0];
|
|
1125
|
+
// MCS SendDataIndication
|
|
1126
|
+
if (mcsOpcode !== constants_1.MCS_SEND_DATA_INDICATION)
|
|
1127
|
+
return;
|
|
1128
|
+
if (payload.length < 8)
|
|
1129
|
+
return;
|
|
1130
|
+
// Decode MCS SDI PER/BER length field.
|
|
1131
|
+
// MCS fixed header: opcode(1)+initiator(2)+channelId(2)+priority(1) = 6 bytes.
|
|
1132
|
+
let offset = 6;
|
|
1133
|
+
const lenByte = payload[offset];
|
|
1134
|
+
offset++;
|
|
1135
|
+
if (lenByte & 0x80) {
|
|
1136
|
+
offset++; // T.125 PER 2-byte form: [0x80|hi, lo] — skip the low byte
|
|
1137
|
+
}
|
|
1138
|
+
const rdpPayload = payload.slice(offset);
|
|
1139
|
+
if (rdpPayload.length < 6)
|
|
1140
|
+
return;
|
|
1141
|
+
// With TLS (no RDP-level encryption), run-loop PDUs have no security header.
|
|
1142
|
+
// rdpPayload starts directly with the Share Control Header.
|
|
1143
|
+
const shareCtrl = rdpPayload;
|
|
1144
|
+
if (shareCtrl.length < 6)
|
|
1145
|
+
return;
|
|
1146
|
+
const pduType = shareCtrl.readUInt16LE(2) & 0x0f;
|
|
1147
|
+
this.log(`[RDP] runLoop PDU: pduType=0x${pduType.toString(16)} len=${shareCtrl.length}`);
|
|
1148
|
+
if (pduType === (constants_1.PDUTYPE_DATAPDU & 0x0f)) {
|
|
1149
|
+
const body = shareCtrl.slice(6);
|
|
1150
|
+
const pt2 = body.length >= 9 ? body[8] : -1;
|
|
1151
|
+
this.log(`[RDP] DataPDU pduType2=0x${pt2.toString(16)}`);
|
|
1152
|
+
this._handleDataPdu(body);
|
|
1153
|
+
}
|
|
1154
|
+
else if (pduType === (constants_1.PDUTYPE_DEMANDACTIVEPDU & 0x0f)) {
|
|
1155
|
+
// Re-issued demand active — re-confirm and re-sync
|
|
1156
|
+
this._shareId = shareCtrl.readUInt32LE(6);
|
|
1157
|
+
if (this._sock) {
|
|
1158
|
+
const s = this._sock;
|
|
1159
|
+
const confirm = buildConfirmActivePdu(this._userId, this._shareId, this._width, this._height);
|
|
1160
|
+
this._sendMcsData(s, constants_1.MCS_CHANNEL_GLOBAL, confirm);
|
|
1161
|
+
this._sendMcsData(s, constants_1.MCS_CHANNEL_GLOBAL, buildSynchronizePdu(this._userId, this._shareId));
|
|
1162
|
+
this._sendMcsData(s, constants_1.MCS_CHANNEL_GLOBAL, buildControlPdu(this._userId, this._shareId, 4));
|
|
1163
|
+
this._sendMcsData(s, constants_1.MCS_CHANNEL_GLOBAL, buildControlPdu(this._userId, this._shareId, 1));
|
|
1164
|
+
this._sendMcsData(s, constants_1.MCS_CHANNEL_GLOBAL, buildFontListPdu(this._userId, this._shareId));
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
_handleDataPdu(body) {
|
|
1169
|
+
if (body.length < 12)
|
|
1170
|
+
return;
|
|
1171
|
+
// TS_SHAREDATAHEADER: shareId(4)+pad(1)+streamId(1)+uncompressedLength(2)+pduType2(1)
|
|
1172
|
+
const pduType2 = body[8];
|
|
1173
|
+
if (pduType2 === constants_1.PDUTYPE2_UPDATE) {
|
|
1174
|
+
if (body.length >= 14) {
|
|
1175
|
+
const updateType = body.readUInt16LE(12);
|
|
1176
|
+
this.log(`[RDP] UpdatePDU updateType=0x${updateType.toString(16)}`);
|
|
1177
|
+
}
|
|
1178
|
+
this._handleUpdatePdu(body.slice(12));
|
|
1179
|
+
}
|
|
1180
|
+
else if (pduType2 === constants_1.PDUTYPE2_FONTMAP) {
|
|
1181
|
+
// Server finished capability sequence.
|
|
1182
|
+
// Send TS_SYNCHRONIZE_EVENT (keyboard state sync) to signal client readiness.
|
|
1183
|
+
// Do NOT send SuppressOutput/RefreshRect here — xrdp's Xvnc backend crashes on them
|
|
1184
|
+
// before it has fully started; just send INPUT_SYNC and let xrdp push updates.
|
|
1185
|
+
this._connected = true;
|
|
1186
|
+
if (this._sock) {
|
|
1187
|
+
// toggleFlags=0: no CapsLock/NumLock/ScrollLock
|
|
1188
|
+
this._sendInput(constants_1.INPUT_EVENT_SYNC, Buffer.from([0, 0, 0, 0, 0, 0]));
|
|
1189
|
+
this.log('[RDP] FontMap received — sent INPUT_SYNC');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
_handleUpdatePdu(body) {
|
|
1194
|
+
if (body.length < 2)
|
|
1195
|
+
return;
|
|
1196
|
+
const updateType = body.readUInt16LE(0);
|
|
1197
|
+
if (updateType === constants_1.UPDATE_BITMAP) {
|
|
1198
|
+
this._parseBitmapUpdate(body.slice(2));
|
|
1199
|
+
}
|
|
1200
|
+
else if (updateType === 0x0003 /* UPDATETYPE_SYNCHRONIZE */) {
|
|
1201
|
+
// xrdp backend is syncing — wait before requesting a full-screen refresh
|
|
1202
|
+
// to give the Xvnc backend time to start up
|
|
1203
|
+
if (this._sock && !this._closed) {
|
|
1204
|
+
const s = this._sock;
|
|
1205
|
+
setTimeout(() => {
|
|
1206
|
+
if (!this._closed && this._connected && s === this._sock) {
|
|
1207
|
+
this._sendMcsData(s, constants_1.MCS_CHANNEL_GLOBAL, buildRefreshRectPdu(this._userId, this._shareId, this._width, this._height));
|
|
1208
|
+
this.log('[RDP] UPDATETYPE_SYNCHRONIZE — sent RefreshRect (delayed)');
|
|
1209
|
+
}
|
|
1210
|
+
}, 2000);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// UPDATE_ORDERS and UPDATE_POINTER can be added in Phase 2
|
|
1214
|
+
}
|
|
1215
|
+
_parseBitmapUpdate(data) {
|
|
1216
|
+
if (data.length < 2)
|
|
1217
|
+
return;
|
|
1218
|
+
const numRects = data.readUInt16LE(0); // TS_BITMAP_DATA_ARRAY: numberRectangles
|
|
1219
|
+
let offset = 2;
|
|
1220
|
+
const rects = [];
|
|
1221
|
+
for (let i = 0; i < numRects; i++) {
|
|
1222
|
+
if (offset + 18 > data.length)
|
|
1223
|
+
break;
|
|
1224
|
+
const x = data.readUInt16LE(offset);
|
|
1225
|
+
const y = data.readUInt16LE(offset + 2);
|
|
1226
|
+
const w = data.readUInt16LE(offset + 4) - x + 1; // destRight - destLeft + 1
|
|
1227
|
+
const h = data.readUInt16LE(offset + 6) - y + 1; // destBottom - destTop + 1
|
|
1228
|
+
// offset+8 = width (bitmap pixels), offset+10 = height (bitmap pixels) — skipped
|
|
1229
|
+
const bpp = data.readUInt16LE(offset + 12); // bitsPerPixel
|
|
1230
|
+
const flags = data.readUInt16LE(offset + 14); // flags
|
|
1231
|
+
const bitmapLen = data.readUInt16LE(offset + 16); // bitmapLength
|
|
1232
|
+
offset += 18;
|
|
1233
|
+
if (offset + bitmapLen > data.length)
|
|
1234
|
+
break;
|
|
1235
|
+
const bitmapData = data.slice(offset, offset + bitmapLen);
|
|
1236
|
+
offset += bitmapLen;
|
|
1237
|
+
const bpp8 = Math.ceil(bpp / 8);
|
|
1238
|
+
let raw;
|
|
1239
|
+
if (flags & constants_1.BITMAP_COMPRESSION && !(flags & constants_1.NO_BITMAP_COMPRESSION_HDR)) {
|
|
1240
|
+
// Compressed with standard header (12 bytes)
|
|
1241
|
+
raw = decompressBitmap(bitmapData.slice(12), w, h, bpp8);
|
|
1242
|
+
}
|
|
1243
|
+
else if (flags & constants_1.BITMAP_COMPRESSION) {
|
|
1244
|
+
raw = decompressBitmap(bitmapData, w, h, bpp8);
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
raw = bitmapData;
|
|
1248
|
+
}
|
|
1249
|
+
// RDP sends bottom-up bitmaps — flip vertically
|
|
1250
|
+
const rowSize = w * bpp8;
|
|
1251
|
+
const flipped = Buffer.alloc(raw.length);
|
|
1252
|
+
for (let row = 0; row < h; row++) {
|
|
1253
|
+
raw.copy(flipped, row * rowSize, (h - 1 - row) * rowSize, (h - row) * rowSize);
|
|
1254
|
+
}
|
|
1255
|
+
rects.push({ x, y, w, h, data: toRgba(flipped, bpp8) });
|
|
1256
|
+
}
|
|
1257
|
+
if (rects.length > 0) {
|
|
1258
|
+
this.emit('fbu', rects);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// ── Input helper ──────────────────────────────────────────────────────
|
|
1262
|
+
_sendInput(eventType, eventData) {
|
|
1263
|
+
if (!this._sock || !this._userId)
|
|
1264
|
+
return;
|
|
1265
|
+
// TS_INPUT_PDU_DATA: numEvents(2) + pad(2) + [TS_INPUT_EVENT: eventTime(4) + messageType(2) + slowPathData(6)]
|
|
1266
|
+
const event = Buffer.alloc(16);
|
|
1267
|
+
event.writeUInt16LE(1, 0); // numEvents
|
|
1268
|
+
event.writeUInt16LE(0, 2); // pad
|
|
1269
|
+
event.writeUInt32LE(0, 4); // eventTime (0 = server picks)
|
|
1270
|
+
event.writeUInt16LE(eventType, 8); // messageType
|
|
1271
|
+
if (eventData.length >= 6) {
|
|
1272
|
+
eventData.copy(event, 10, 0, 6); // slowPathData at offset 10
|
|
1273
|
+
}
|
|
1274
|
+
const shareHdr = buildShareDataHeader(this._userId, this._shareId, constants_1.PDUTYPE2_INPUT, event.length);
|
|
1275
|
+
this._sendMcsData(this._sock, constants_1.MCS_CHANNEL_GLOBAL, Buffer.concat([shareHdr, event]));
|
|
1276
|
+
}
|
|
1277
|
+
// ── Low-level send helpers ─────────────────────────────────────────────
|
|
1278
|
+
_sendX224Data(sock, payload) {
|
|
1279
|
+
sock.write(wrapTpkt(wrapX224Data(payload)));
|
|
1280
|
+
}
|
|
1281
|
+
_sendMcsData(sock, channelId, payload) {
|
|
1282
|
+
const mcs = wrapMcsSend(this._userId, channelId, payload);
|
|
1283
|
+
this._sendX224Data(sock, mcs);
|
|
1284
|
+
}
|
|
1285
|
+
// ── Receive helpers ───────────────────────────────────────────────────
|
|
1286
|
+
/** Read a complete TPKT from the given socket (used during handshake only). */
|
|
1287
|
+
_readTpkt(sock) {
|
|
1288
|
+
return new Promise((resolve, reject) => {
|
|
1289
|
+
const tryExtract = () => {
|
|
1290
|
+
if (this._handshakeBuf.length < 4)
|
|
1291
|
+
return false;
|
|
1292
|
+
const len = this._handshakeBuf.readUInt16BE(2);
|
|
1293
|
+
if (this._handshakeBuf.length < len)
|
|
1294
|
+
return false;
|
|
1295
|
+
const pkt = this._handshakeBuf.slice(0, len);
|
|
1296
|
+
this._handshakeBuf = this._handshakeBuf.slice(len); // preserve remainder
|
|
1297
|
+
resolve(pkt);
|
|
1298
|
+
return true;
|
|
1299
|
+
};
|
|
1300
|
+
if (tryExtract())
|
|
1301
|
+
return; // already buffered enough
|
|
1302
|
+
const onData = (chunk) => {
|
|
1303
|
+
this._handshakeBuf = Buffer.concat([this._handshakeBuf, chunk]);
|
|
1304
|
+
if (tryExtract())
|
|
1305
|
+
sock.off('data', onData);
|
|
1306
|
+
};
|
|
1307
|
+
sock.on('data', onData);
|
|
1308
|
+
setTimeout(() => { sock.off('data', onData); reject(new Error('TPKT read timeout')); }, 15_000);
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
/** Read a TPKT and return only the payload (after TPKT+X.224 headers). */
|
|
1312
|
+
async _readTpktPayload(sock) {
|
|
1313
|
+
const full = await this._readTpkt(sock);
|
|
1314
|
+
// TPKT(4) + X.224 Data header(3) = 7
|
|
1315
|
+
return full.slice(7);
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Wait for an RDP PDU with a specific pduType in the Share Control Header.
|
|
1319
|
+
* Times out after `timeoutMs` ms.
|
|
1320
|
+
*/
|
|
1321
|
+
_waitForPduType(sock, pduType, timeoutMs) {
|
|
1322
|
+
return new Promise((resolve, reject) => {
|
|
1323
|
+
// Seed with any bytes buffered during license exchange
|
|
1324
|
+
let buf = this._handshakeBuf;
|
|
1325
|
+
this._handshakeBuf = Buffer.alloc(0);
|
|
1326
|
+
const timer = setTimeout(() => {
|
|
1327
|
+
sock.off('data', onData);
|
|
1328
|
+
reject(new Error(`Timeout waiting for PDU type 0x${pduType.toString(16)}`));
|
|
1329
|
+
}, timeoutMs);
|
|
1330
|
+
const onData = (chunk) => {
|
|
1331
|
+
buf = Buffer.concat([buf, chunk]);
|
|
1332
|
+
// Try to extract complete TPKT packets
|
|
1333
|
+
while (buf.length >= 4) {
|
|
1334
|
+
const len = buf.readUInt16BE(2);
|
|
1335
|
+
if (buf.length < len)
|
|
1336
|
+
break;
|
|
1337
|
+
const tpkt = buf.slice(0, len);
|
|
1338
|
+
buf = buf.slice(len);
|
|
1339
|
+
// Decode MCS → RDP
|
|
1340
|
+
if (tpkt.length < 11)
|
|
1341
|
+
continue;
|
|
1342
|
+
const mcsPayload = tpkt.slice(7); // skip TPKT(4) + X224(3)
|
|
1343
|
+
if (mcsPayload[0] !== constants_1.MCS_SEND_DATA_INDICATION)
|
|
1344
|
+
continue;
|
|
1345
|
+
// MCS fixed header: opcode(1)+initiator(2)+channelId(2)+priority(1) = 6 bytes
|
|
1346
|
+
let offset = 6;
|
|
1347
|
+
const lb = mcsPayload[offset++];
|
|
1348
|
+
if (lb & 0x80)
|
|
1349
|
+
offset++; // T.125 PER 2-byte form: skip low byte
|
|
1350
|
+
const rdp = mcsPayload.slice(offset);
|
|
1351
|
+
if (rdp.length < 6)
|
|
1352
|
+
continue;
|
|
1353
|
+
// With TLS, Demand Active has no security header — rdp IS the Share Control Header.
|
|
1354
|
+
const t = rdp.readUInt16LE(2) & 0x0f;
|
|
1355
|
+
if (t === (pduType & 0x0f)) {
|
|
1356
|
+
clearTimeout(timer);
|
|
1357
|
+
sock.off('data', onData);
|
|
1358
|
+
// Save any remaining bytes so _runLoop can consume them
|
|
1359
|
+
this._handshakeBuf = Buffer.concat([buf, this._handshakeBuf]);
|
|
1360
|
+
buf = Buffer.alloc(0);
|
|
1361
|
+
resolve(rdp);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
sock.on('data', onData);
|
|
1367
|
+
// Process any pre-seeded data immediately
|
|
1368
|
+
if (buf.length > 0)
|
|
1369
|
+
onData(Buffer.alloc(0));
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
/** Try to parse one complete TPKT from _recvBuf without blocking.
|
|
1373
|
+
* Fast-path PDUs (first byte != 0x03) are skipped with a warning since we
|
|
1374
|
+
* advertise no fast-path support; they should not appear, but guard anyway. */
|
|
1375
|
+
_tryParseTpkt() {
|
|
1376
|
+
if (this._recvBuf.length < 4)
|
|
1377
|
+
return null;
|
|
1378
|
+
if (this._recvBuf[0] !== 0x03) {
|
|
1379
|
+
// Not a TPKT — could be a stray fast-path byte. Scan forward to next 0x03.
|
|
1380
|
+
this.log(`[RDP] warn: non-TPKT byte 0x${this._recvBuf[0].toString(16)} — skipping`);
|
|
1381
|
+
const next = this._recvBuf.indexOf(0x03, 1);
|
|
1382
|
+
this._recvBuf = next >= 0 ? this._recvBuf.slice(next) : Buffer.alloc(0);
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
const len = this._recvBuf.readUInt16BE(2);
|
|
1386
|
+
if (len < 4) {
|
|
1387
|
+
this._recvBuf = this._recvBuf.slice(4);
|
|
1388
|
+
return null;
|
|
1389
|
+
} // guard
|
|
1390
|
+
if (this._recvBuf.length < len)
|
|
1391
|
+
return null;
|
|
1392
|
+
const pkt = this._recvBuf.slice(0, len);
|
|
1393
|
+
this._recvBuf = this._recvBuf.slice(len);
|
|
1394
|
+
return pkt;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
exports.RdpBridge = RdpBridge;
|
|
1398
|
+
//# sourceMappingURL=bridge.js.map
|