ai-remote 0.1.0
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/README.md +50 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +7 -0
- package/dist/protocols/index.js +35 -0
- package/dist/protocols/index.js.map +7 -0
- package/dist/protocols/rdp/buffer.js +170 -0
- package/dist/protocols/rdp/buffer.js.map +7 -0
- package/dist/protocols/rdp/caps.js +260 -0
- package/dist/protocols/rdp/caps.js.map +7 -0
- package/dist/protocols/rdp/cert.js +135 -0
- package/dist/protocols/rdp/cert.js.map +7 -0
- package/dist/protocols/rdp/client.js +1068 -0
- package/dist/protocols/rdp/client.js.map +7 -0
- package/dist/protocols/rdp/cliprdr.js +240 -0
- package/dist/protocols/rdp/cliprdr.js.map +7 -0
- package/dist/protocols/rdp/credssp.js +254 -0
- package/dist/protocols/rdp/credssp.js.map +7 -0
- package/dist/protocols/rdp/crypto.js +369 -0
- package/dist/protocols/rdp/crypto.js.map +7 -0
- package/dist/protocols/rdp/display.js +589 -0
- package/dist/protocols/rdp/display.js.map +7 -0
- package/dist/protocols/rdp/gcc.js +132 -0
- package/dist/protocols/rdp/gcc.js.map +7 -0
- package/dist/protocols/rdp/keymap.js +132 -0
- package/dist/protocols/rdp/keymap.js.map +7 -0
- package/dist/protocols/rdp/mcs.js +223 -0
- package/dist/protocols/rdp/mcs.js.map +7 -0
- package/dist/protocols/rdp/ntlm.js +305 -0
- package/dist/protocols/rdp/ntlm.js.map +7 -0
- package/dist/protocols/rdp/pdu.js +444 -0
- package/dist/protocols/rdp/pdu.js.map +7 -0
- package/dist/protocols/rdp/rail.js +383 -0
- package/dist/protocols/rdp/rail.js.map +7 -0
- package/dist/protocols/rdp/rle.js +276 -0
- package/dist/protocols/rdp/rle.js.map +7 -0
- package/dist/protocols/rdp/sec.js +156 -0
- package/dist/protocols/rdp/sec.js.map +7 -0
- package/dist/protocols/rdp/session.js +307 -0
- package/dist/protocols/rdp/session.js.map +7 -0
- package/dist/protocols/rdp/tls.js +354 -0
- package/dist/protocols/rdp/tls.js.map +7 -0
- package/dist/protocols/rdp/vchannel.js +59 -0
- package/dist/protocols/rdp/vchannel.js.map +7 -0
- package/dist/protocols/rdp/x224.js +110 -0
- package/dist/protocols/rdp/x224.js.map +7 -0
- package/dist/protocols/ssh/kex.js +177 -0
- package/dist/protocols/ssh/kex.js.map +7 -0
- package/dist/protocols/ssh/messages.js +55 -0
- package/dist/protocols/ssh/messages.js.map +7 -0
- package/dist/protocols/ssh/session.js +608 -0
- package/dist/protocols/ssh/session.js.map +7 -0
- package/dist/protocols/ssh/terminal.js +191 -0
- package/dist/protocols/ssh/terminal.js.map +7 -0
- package/dist/protocols/ssh/transport.js +611 -0
- package/dist/protocols/ssh/transport.js.map +7 -0
- package/dist/protocols/ssh/wire.js +189 -0
- package/dist/protocols/ssh/wire.js.map +7 -0
- package/dist/protocols/types.js +1 -0
- package/dist/protocols/types.js.map +7 -0
- package/dist/protocols/vnc/input.js +16 -0
- package/dist/protocols/vnc/input.js.map +7 -0
- package/dist/protocols/vnc/session.js +259 -0
- package/dist/protocols/vnc/session.js.map +7 -0
- package/dist/shared/connection.js +114 -0
- package/dist/shared/connection.js.map +7 -0
- package/dist/shared/device.js +1 -0
- package/dist/shared/device.js.map +7 -0
- package/dist/shared/hosts.js +133 -0
- package/dist/shared/hosts.js.map +7 -0
- package/dist/shared/icons.js +42 -0
- package/dist/shared/icons.js.map +7 -0
- package/dist/shared/protocol.js +40 -0
- package/dist/shared/protocol.js.map +7 -0
- package/dist/shared/signals.js +42 -0
- package/dist/shared/signals.js.map +7 -0
- package/package.json +82 -0
- package/src/index.ts +14 -0
- package/src/protocols/index.ts +72 -0
- package/src/protocols/rdp/buffer.ts +204 -0
- package/src/protocols/rdp/caps.ts +304 -0
- package/src/protocols/rdp/cert.ts +190 -0
- package/src/protocols/rdp/client.ts +1363 -0
- package/src/protocols/rdp/cliprdr.ts +344 -0
- package/src/protocols/rdp/credssp.ts +319 -0
- package/src/protocols/rdp/crypto.ts +416 -0
- package/src/protocols/rdp/display.ts +750 -0
- package/src/protocols/rdp/gcc.ts +162 -0
- package/src/protocols/rdp/keymap.ts +68 -0
- package/src/protocols/rdp/mcs.ts +283 -0
- package/src/protocols/rdp/ntlm.ts +380 -0
- package/src/protocols/rdp/pdu.ts +497 -0
- package/src/protocols/rdp/rail.ts +480 -0
- package/src/protocols/rdp/rle.ts +343 -0
- package/src/protocols/rdp/sec.ts +184 -0
- package/src/protocols/rdp/session.ts +393 -0
- package/src/protocols/rdp/tls.ts +452 -0
- package/src/protocols/rdp/vchannel.ts +89 -0
- package/src/protocols/rdp/x224.ts +139 -0
- package/src/protocols/ssh/kex.ts +227 -0
- package/src/protocols/ssh/messages.ts +54 -0
- package/src/protocols/ssh/session.ts +878 -0
- package/src/protocols/ssh/terminal.ts +242 -0
- package/src/protocols/ssh/transport.ts +732 -0
- package/src/protocols/ssh/wire.ts +220 -0
- package/src/protocols/types.ts +146 -0
- package/src/protocols/vnc/input.ts +13 -0
- package/src/protocols/vnc/novnc.d.ts +75 -0
- package/src/protocols/vnc/session.ts +367 -0
- package/src/shared/connection.ts +206 -0
- package/src/shared/device.ts +28 -0
- package/src/shared/hosts.ts +240 -0
- package/src/shared/icons.ts +49 -0
- package/src/shared/protocol.ts +55 -0
- package/src/shared/signals.ts +67 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// GCC user data: what the client says about itself in the MCS connect request,
|
|
2
|
+
// and what the host answers with (MS-RDPBCGR 2.2.1.3 / 2.2.1.4).
|
|
3
|
+
|
|
4
|
+
import { Reader, Writer, concatBytes } from './buffer';
|
|
5
|
+
import { parseServerCertificate } from './cert';
|
|
6
|
+
|
|
7
|
+
const CS_CORE = 0xc001;
|
|
8
|
+
const CS_SECURITY = 0xc002;
|
|
9
|
+
const CS_NET = 0xc003;
|
|
10
|
+
const CS_CLUSTER = 0xc004;
|
|
11
|
+
|
|
12
|
+
const SC_CORE = 0x0c01;
|
|
13
|
+
const SC_SECURITY = 0x0c02;
|
|
14
|
+
const SC_NET = 0x0c03;
|
|
15
|
+
|
|
16
|
+
/** RDP 5.0 and later. Anything newer would oblige us to speak newer PDUs. */
|
|
17
|
+
export const RDP_VERSION_5_PLUS = 0x00080004;
|
|
18
|
+
|
|
19
|
+
const COLOR_16BPP = 0xca03;
|
|
20
|
+
const RNS_UD_SAS_DEL = 0xaa03;
|
|
21
|
+
const SUPPORT_ERRINFO_PDU = 0x0001;
|
|
22
|
+
|
|
23
|
+
export const ENCRYPTION_LEVEL = {
|
|
24
|
+
NONE: 0,
|
|
25
|
+
LOW: 1,
|
|
26
|
+
CLIENT_COMPATIBLE: 2,
|
|
27
|
+
HIGH: 3,
|
|
28
|
+
FIPS: 4,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function userDataBlock(type: number, body: Uint8Array) {
|
|
32
|
+
const writer = new Writer(body.length + 4);
|
|
33
|
+
writer.u16le(type).u16le(body.length + 4).raw(body);
|
|
34
|
+
return writer.take();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* TS_UD_CS_CORE. The desktop size asked for here is a request: the host answers
|
|
39
|
+
* with the size it actually chose in its Demand Active PDU.
|
|
40
|
+
*/
|
|
41
|
+
function clientCoreData({ width, height, clientName, keyboardLayout, selectedProtocol }) {
|
|
42
|
+
const writer = new Writer(256);
|
|
43
|
+
|
|
44
|
+
writer.u32le(RDP_VERSION_5_PLUS);
|
|
45
|
+
writer.u16le(width);
|
|
46
|
+
writer.u16le(height);
|
|
47
|
+
writer.u16le(COLOR_16BPP);
|
|
48
|
+
writer.u16le(RNS_UD_SAS_DEL);
|
|
49
|
+
writer.u32le(keyboardLayout);
|
|
50
|
+
writer.u32le(2600); // clientBuild
|
|
51
|
+
writer.utf16le(clientName, 32); // clientName, 15 chars + NUL
|
|
52
|
+
writer.u32le(4); // keyboardType: IBM enhanced
|
|
53
|
+
writer.u32le(0); // keyboardSubType
|
|
54
|
+
writer.u32le(12); // keyboardFunctionKey
|
|
55
|
+
writer.zeros(64); // imeFileName
|
|
56
|
+
writer.u16le(COLOR_16BPP); // postBeta2ColorDepth
|
|
57
|
+
writer.u16le(1); // clientProductId
|
|
58
|
+
writer.u32le(0); // serialNumber
|
|
59
|
+
writer.u16le(16); // highColorDepth
|
|
60
|
+
writer.u16le(0x0007); // supportedColorDepths: 15/16/24
|
|
61
|
+
writer.u16le(SUPPORT_ERRINFO_PDU); // earlyCapabilityFlags
|
|
62
|
+
writer.zeros(64); // clientDigProductId
|
|
63
|
+
writer.u8(0); // connectionType: unknown
|
|
64
|
+
writer.u8(0); // pad
|
|
65
|
+
writer.u32le(selectedProtocol); // serverSelectedProtocol
|
|
66
|
+
|
|
67
|
+
return userDataBlock(CS_CORE, writer.take());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** TS_UD_CS_SEC. FIPS is deliberately absent -- this client does not do it. */
|
|
71
|
+
function clientSecurityData(encryptionMethods: number) {
|
|
72
|
+
const writer = new Writer(8);
|
|
73
|
+
writer.u32le(encryptionMethods);
|
|
74
|
+
writer.u32le(0); // extEncryptionMethods, French locale only
|
|
75
|
+
return userDataBlock(CS_SECURITY, writer.take());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** TS_UD_CS_CLUSTER, needed so the host reports redirection instead of hanging. */
|
|
79
|
+
function clientClusterData() {
|
|
80
|
+
const writer = new Writer(8);
|
|
81
|
+
writer.u32le(0x0000000d); // REDIRECTION_SUPPORTED | redirection version 4
|
|
82
|
+
writer.u32le(0); // RedirectedSessionID
|
|
83
|
+
return userDataBlock(CS_CLUSTER, writer.take());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clientNetworkData(channels) {
|
|
87
|
+
if (channels.length === 0) return new Uint8Array(0);
|
|
88
|
+
|
|
89
|
+
const writer = new Writer(4 + channels.length * 12);
|
|
90
|
+
writer.u32le(channels.length);
|
|
91
|
+
for (const channel of channels) {
|
|
92
|
+
const name = new Uint8Array(8);
|
|
93
|
+
for (let i = 0; i < Math.min(7, channel.name.length); i++) name[i] = channel.name.charCodeAt(i);
|
|
94
|
+
writer.raw(name);
|
|
95
|
+
writer.u32le(channel.options >>> 0);
|
|
96
|
+
}
|
|
97
|
+
return userDataBlock(CS_NET, writer.take());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function clientData(options) {
|
|
101
|
+
return concatBytes(
|
|
102
|
+
clientCoreData(options),
|
|
103
|
+
clientClusterData(),
|
|
104
|
+
clientSecurityData(options.encryptionMethods),
|
|
105
|
+
clientNetworkData(options.channels || [])
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Walk the host's answer. Only the security and network blocks carry anything
|
|
111
|
+
* the client must act on; the core block is kept for logging.
|
|
112
|
+
*/
|
|
113
|
+
export function parseServerData(blocks) {
|
|
114
|
+
const reader = new Reader(blocks);
|
|
115
|
+
const result = {
|
|
116
|
+
version: null,
|
|
117
|
+
encryptionMethod: 0,
|
|
118
|
+
encryptionLevel: ENCRYPTION_LEVEL.NONE,
|
|
119
|
+
serverRandom: null,
|
|
120
|
+
publicKey: null,
|
|
121
|
+
ioChannelId: 1003,
|
|
122
|
+
channelIds: [],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
while (reader.remaining >= 4) {
|
|
126
|
+
const type = reader.u16le();
|
|
127
|
+
const length = reader.u16le();
|
|
128
|
+
if (length < 4 || length - 4 > reader.remaining) break;
|
|
129
|
+
const body = new Reader(reader.raw(length - 4));
|
|
130
|
+
|
|
131
|
+
if (type === SC_CORE) {
|
|
132
|
+
result.version = body.remaining >= 4 ? body.u32le() : null;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (type === SC_NET) {
|
|
137
|
+
result.ioChannelId = body.u16le();
|
|
138
|
+
const channelCount = body.u16le();
|
|
139
|
+
for (let i = 0; i < channelCount && body.remaining >= 2; i++) {
|
|
140
|
+
result.channelIds.push(body.u16le());
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (type === SC_SECURITY) {
|
|
146
|
+
result.encryptionMethod = body.u32le();
|
|
147
|
+
result.encryptionLevel = body.u32le();
|
|
148
|
+
|
|
149
|
+
// With TLS in play the host reports no RDP-level encryption and stops.
|
|
150
|
+
if (result.encryptionMethod === 0 && result.encryptionLevel === 0) continue;
|
|
151
|
+
if (body.remaining < 8) continue;
|
|
152
|
+
|
|
153
|
+
const serverRandomLength = body.u32le();
|
|
154
|
+
const serverCertLength = body.u32le();
|
|
155
|
+
result.serverRandom = body.raw(serverRandomLength).slice();
|
|
156
|
+
result.publicKey = parseServerCertificate(body.raw(serverCertLength));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// KeyboardEvent.code -> PC/AT set 1 scancode, which is what RDP carries.
|
|
2
|
+
// Keys marked extended need KBDFLAGS_EXTENDED so the host tells, for example,
|
|
3
|
+
// the numeric keypad Enter from the main one.
|
|
4
|
+
|
|
5
|
+
const SCANCODES = {
|
|
6
|
+
Escape: 0x01,
|
|
7
|
+
Digit1: 0x02, Digit2: 0x03, Digit3: 0x04, Digit4: 0x05, Digit5: 0x06,
|
|
8
|
+
Digit6: 0x07, Digit7: 0x08, Digit8: 0x09, Digit9: 0x0a, Digit0: 0x0b,
|
|
9
|
+
Minus: 0x0c, Equal: 0x0d, Backspace: 0x0e, Tab: 0x0f,
|
|
10
|
+
KeyQ: 0x10, KeyW: 0x11, KeyE: 0x12, KeyR: 0x13, KeyT: 0x14,
|
|
11
|
+
KeyY: 0x15, KeyU: 0x16, KeyI: 0x17, KeyO: 0x18, KeyP: 0x19,
|
|
12
|
+
BracketLeft: 0x1a, BracketRight: 0x1b, Enter: 0x1c, ControlLeft: 0x1d,
|
|
13
|
+
KeyA: 0x1e, KeyS: 0x1f, KeyD: 0x20, KeyF: 0x21, KeyG: 0x22,
|
|
14
|
+
KeyH: 0x23, KeyJ: 0x24, KeyK: 0x25, KeyL: 0x26,
|
|
15
|
+
Semicolon: 0x27, Quote: 0x28, Backquote: 0x29, ShiftLeft: 0x2a, Backslash: 0x2b,
|
|
16
|
+
KeyZ: 0x2c, KeyX: 0x2d, KeyC: 0x2e, KeyV: 0x2f, KeyB: 0x30,
|
|
17
|
+
KeyN: 0x31, KeyM: 0x32, Comma: 0x33, Period: 0x34, Slash: 0x35,
|
|
18
|
+
ShiftRight: 0x36, NumpadMultiply: 0x37, AltLeft: 0x38, Space: 0x39, CapsLock: 0x3a,
|
|
19
|
+
F1: 0x3b, F2: 0x3c, F3: 0x3d, F4: 0x3e, F5: 0x3f,
|
|
20
|
+
F6: 0x40, F7: 0x41, F8: 0x42, F9: 0x43, F10: 0x44,
|
|
21
|
+
NumLock: 0x45, ScrollLock: 0x46,
|
|
22
|
+
Numpad7: 0x47, Numpad8: 0x48, Numpad9: 0x49, NumpadSubtract: 0x4a,
|
|
23
|
+
Numpad4: 0x4b, Numpad5: 0x4c, Numpad6: 0x4d, NumpadAdd: 0x4e,
|
|
24
|
+
Numpad1: 0x4f, Numpad2: 0x50, Numpad3: 0x51, Numpad0: 0x52, NumpadDecimal: 0x53,
|
|
25
|
+
IntlBackslash: 0x56, F11: 0x57, F12: 0x58,
|
|
26
|
+
IntlRo: 0x73, IntlYen: 0x7d,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const EXTENDED_SCANCODES = {
|
|
30
|
+
NumpadEnter: 0x1c,
|
|
31
|
+
ControlRight: 0x1d,
|
|
32
|
+
NumpadDivide: 0x35,
|
|
33
|
+
PrintScreen: 0x37,
|
|
34
|
+
AltRight: 0x38,
|
|
35
|
+
Home: 0x47,
|
|
36
|
+
ArrowUp: 0x48,
|
|
37
|
+
PageUp: 0x49,
|
|
38
|
+
ArrowLeft: 0x4b,
|
|
39
|
+
ArrowRight: 0x4d,
|
|
40
|
+
End: 0x4f,
|
|
41
|
+
ArrowDown: 0x50,
|
|
42
|
+
PageDown: 0x51,
|
|
43
|
+
Insert: 0x52,
|
|
44
|
+
Delete: 0x53,
|
|
45
|
+
MetaLeft: 0x5b,
|
|
46
|
+
MetaRight: 0x5c,
|
|
47
|
+
ContextMenu: 0x5d,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Returns { scancode, extended } or null when the key has no RDP equivalent. */
|
|
51
|
+
export function lookupScancode(code: string) {
|
|
52
|
+
if (Object.prototype.hasOwnProperty.call(SCANCODES, code)) {
|
|
53
|
+
return { scancode: SCANCODES[code], extended: false };
|
|
54
|
+
}
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(EXTENDED_SCANCODES, code)) {
|
|
56
|
+
return { scancode: EXTENDED_SCANCODES[code], extended: true };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const CTRL_ALT_DEL = [
|
|
62
|
+
{ code: 'ControlLeft', pressed: true },
|
|
63
|
+
{ code: 'AltLeft', pressed: true },
|
|
64
|
+
{ code: 'Delete', pressed: true },
|
|
65
|
+
{ code: 'Delete', pressed: false },
|
|
66
|
+
{ code: 'AltLeft', pressed: false },
|
|
67
|
+
{ code: 'ControlLeft', pressed: false },
|
|
68
|
+
];
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// MCS (T.125) connect sequence and domain PDUs, with the BER and PER encoding
|
|
2
|
+
// rules they need. MS-RDPBCGR 2.2.1.3 - 2.2.1.8.
|
|
3
|
+
|
|
4
|
+
import { Reader, Writer, concatBytes } from './buffer';
|
|
5
|
+
import { dataFrame, dataPayload } from './x224';
|
|
6
|
+
|
|
7
|
+
export const MCS_BASE_CHANNEL_ID = 1001;
|
|
8
|
+
export const MCS_GLOBAL_CHANNEL_ID = 1003;
|
|
9
|
+
|
|
10
|
+
const DOMAIN_MCSPDU = {
|
|
11
|
+
ERECT_DOMAIN_REQUEST: 1,
|
|
12
|
+
DISCONNECT_PROVIDER_ULTIMATUM: 8,
|
|
13
|
+
ATTACH_USER_REQUEST: 10,
|
|
14
|
+
ATTACH_USER_CONFIRM: 11,
|
|
15
|
+
CHANNEL_JOIN_REQUEST: 14,
|
|
16
|
+
CHANNEL_JOIN_CONFIRM: 15,
|
|
17
|
+
SEND_DATA_REQUEST: 25,
|
|
18
|
+
SEND_DATA_INDICATION: 26,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const DISCONNECT_REASONS = {
|
|
22
|
+
0: 'The host closed the RDP session (domain disconnected).',
|
|
23
|
+
1: 'The host closed the RDP session (provider initiated).',
|
|
24
|
+
2: 'The host closed the RDP session (token purged).',
|
|
25
|
+
3: 'The host closed the RDP session (user requested).',
|
|
26
|
+
4: 'The host closed the RDP session (channel purged).',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// --- BER --------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function berLength(length: number) {
|
|
32
|
+
if (length > 0xff) return new Uint8Array([0x82, (length >> 8) & 0xff, length & 0xff]);
|
|
33
|
+
if (length > 0x7f) return new Uint8Array([0x81, length]);
|
|
34
|
+
return new Uint8Array([length]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function berTlv(tag: number, content: Uint8Array) {
|
|
38
|
+
return concatBytes(new Uint8Array(tag), berLength(content.length), content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function berInteger(value: number) {
|
|
42
|
+
let body;
|
|
43
|
+
if (value < 0x80) body = new Uint8Array([value]);
|
|
44
|
+
else if (value < 0x8000) body = new Uint8Array([value >> 8, value & 0xff]);
|
|
45
|
+
else if (value < 0x800000) body = new Uint8Array([value >> 16, (value >> 8) & 0xff, value & 0xff]);
|
|
46
|
+
else body = new Uint8Array([value >>> 24, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff]);
|
|
47
|
+
return berTlv([0x02], body);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const berOctetString = (bytes: Uint8Array) => berTlv([0x04], bytes);
|
|
51
|
+
const berBoolean = (value: number) => berTlv([0x01], new Uint8Array([value ? 0xff : 0x00]));
|
|
52
|
+
|
|
53
|
+
/** DomainParameters ::= SEQUENCE of eight INTEGERs (T.125). */
|
|
54
|
+
function berDomainParameters(values: readonly number[]) {
|
|
55
|
+
return berTlv([0x30], concatBytes(...values.map(berInteger)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- PER --------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function perLength(writer: Writer, length: number) {
|
|
61
|
+
if (length > 0x7f) writer.u16be(0x8000 | length);
|
|
62
|
+
else writer.u8(length);
|
|
63
|
+
return writer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function perReadLength(reader: Reader) {
|
|
67
|
+
const first = reader.u8();
|
|
68
|
+
return first & 0x80 ? ((first & 0x7f) << 8) | reader.u8() : first;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- GCC Conference Create Request (T.124) ----------------------------------
|
|
72
|
+
|
|
73
|
+
const T124_OBJECT_IDENTIFIER = new Uint8Array([0x05, 0x00, 0x14, 0x7c, 0x00, 0x01]);
|
|
74
|
+
const H221_CLIENT_KEY = new Uint8Array([0x44, 0x75, 0x63, 0x61]); // "Duca"
|
|
75
|
+
const H221_SERVER_KEY = new Uint8Array([0x4d, 0x63, 0x44, 0x6e]); // "McDn"
|
|
76
|
+
|
|
77
|
+
function gccConferenceCreateRequest(clientData: Uint8Array) {
|
|
78
|
+
const writer = new Writer(clientData.length + 32);
|
|
79
|
+
|
|
80
|
+
writer.u8(0); // ConnectData::t124Identifier, choice 0
|
|
81
|
+
writer.raw(T124_OBJECT_IDENTIFIER); // ITU-T T.124 object identifier
|
|
82
|
+
perLength(writer, clientData.length + 14);
|
|
83
|
+
|
|
84
|
+
writer.u8(0); // ConnectGCCPDU: conferenceCreateRequest
|
|
85
|
+
writer.u8(0x08); // ConferenceCreateRequest::conferenceName present
|
|
86
|
+
writer.u8(0x00); // numeric string length (min 1)
|
|
87
|
+
writer.u8(0x10); // conference name "1"
|
|
88
|
+
writer.u8(0x00); // padding
|
|
89
|
+
writer.u8(0x01); // one set of UserData
|
|
90
|
+
writer.u8(0xc0); // UserData::value present, valueless choice
|
|
91
|
+
writer.u8(0x00); // h221NonStandard length (fixed 4)
|
|
92
|
+
writer.raw(H221_CLIENT_KEY);
|
|
93
|
+
perLength(writer, clientData.length);
|
|
94
|
+
writer.raw(clientData);
|
|
95
|
+
|
|
96
|
+
return writer.take();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Pull the server data blocks out of a GCC Conference Create Response.
|
|
101
|
+
*
|
|
102
|
+
* The response is PER-encoded with several variable-length fields in front of
|
|
103
|
+
* the payload; locating the server's H.221 key is both shorter and sturdier
|
|
104
|
+
* than replaying every one of those fields.
|
|
105
|
+
*/
|
|
106
|
+
function gccServerDataBlocks(userData: Uint8Array) {
|
|
107
|
+
for (let i = 0; i + 4 <= userData.length; i++) {
|
|
108
|
+
if (
|
|
109
|
+
userData[i] === H221_SERVER_KEY[0] &&
|
|
110
|
+
userData[i + 1] === H221_SERVER_KEY[1] &&
|
|
111
|
+
userData[i + 2] === H221_SERVER_KEY[2] &&
|
|
112
|
+
userData[i + 3] === H221_SERVER_KEY[3]
|
|
113
|
+
) {
|
|
114
|
+
const reader = new Reader(userData, i + 4);
|
|
115
|
+
const length = perReadLength(reader);
|
|
116
|
+
const blocks = reader.rest();
|
|
117
|
+
return blocks.subarray(0, Math.min(length, blocks.length));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw new Error('RDP: the host response contained no GCC server data');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Connect Initial / Response ---------------------------------------------
|
|
124
|
+
|
|
125
|
+
const DOMAIN_TARGET = [34, 2, 0, 1, 0, 1, 0xffff, 2];
|
|
126
|
+
const DOMAIN_MINIMUM = [1, 1, 1, 1, 0, 1, 0x420, 2];
|
|
127
|
+
const DOMAIN_MAXIMUM = [0xffff, 0xfc17, 0xffff, 1, 0, 1, 0xffff, 2];
|
|
128
|
+
|
|
129
|
+
export function connectInitial(clientData: Uint8Array) {
|
|
130
|
+
const userData = gccConferenceCreateRequest(clientData);
|
|
131
|
+
const content = concatBytes(
|
|
132
|
+
berOctetString(new Uint8Array([0x01])), // callingDomainSelector
|
|
133
|
+
berOctetString(new Uint8Array([0x01])), // calledDomainSelector
|
|
134
|
+
berBoolean(true), // upwardFlag
|
|
135
|
+
berDomainParameters(DOMAIN_TARGET),
|
|
136
|
+
berDomainParameters(DOMAIN_MINIMUM),
|
|
137
|
+
berDomainParameters(DOMAIN_MAXIMUM),
|
|
138
|
+
berOctetString(userData)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// [APPLICATION 101] IMPLICIT SEQUENCE
|
|
142
|
+
return dataFrame(berTlv([0x7f, 0x65], content));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Walk one BER TLV at the reader's position. */
|
|
146
|
+
function readBerTlv(reader: Reader) {
|
|
147
|
+
let tag = reader.u8();
|
|
148
|
+
if ((tag & 0x1f) === 0x1f) tag = (tag << 8) | reader.u8();
|
|
149
|
+
|
|
150
|
+
let length = reader.u8();
|
|
151
|
+
if (length & 0x80) {
|
|
152
|
+
const byteCount = length & 0x7f;
|
|
153
|
+
length = 0;
|
|
154
|
+
for (let i = 0; i < byteCount; i++) length = (length << 8) | reader.u8();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { tag, value: reader.raw(length) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function parseConnectResponse(frame: Uint8Array) {
|
|
161
|
+
const reader = new Reader(dataPayload(frame));
|
|
162
|
+
const outer = readBerTlv(reader);
|
|
163
|
+
if (outer.tag !== 0x7f66) {
|
|
164
|
+
throw new Error(`RDP: expected an MCS connect response, got BER tag 0x${outer.tag.toString(16)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const inner = new Reader(outer.value);
|
|
168
|
+
const result = readBerTlv(inner); // result ENUMERATED
|
|
169
|
+
readBerTlv(inner); // calledConnectId
|
|
170
|
+
readBerTlv(inner); // domainParameters
|
|
171
|
+
const userData = readBerTlv(inner); // userData OCTET STRING
|
|
172
|
+
|
|
173
|
+
if (result.value[0] !== 0) {
|
|
174
|
+
throw new Error(`RDP: the host rejected the MCS connection (result ${result.value[0]})`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return gccServerDataBlocks(userData.value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Domain PDUs ------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
const domainHeader = (pdu: Uint8Array) => pdu << 2;
|
|
183
|
+
|
|
184
|
+
export function erectDomainRequest() {
|
|
185
|
+
const writer = new Writer(8);
|
|
186
|
+
writer.u8(domainHeader(DOMAIN_MCSPDU.ERECT_DOMAIN_REQUEST));
|
|
187
|
+
writer.u8(1).u8(0); // subHeight: PER integer 0
|
|
188
|
+
writer.u8(1).u8(0); // subInterval: PER integer 0
|
|
189
|
+
return dataFrame(writer.take());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function attachUserRequest() {
|
|
193
|
+
return dataFrame(new Uint8Array([domainHeader(DOMAIN_MCSPDU.ATTACH_USER_REQUEST)]));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function parseAttachUserConfirm(frame: Uint8Array) {
|
|
197
|
+
const reader = new Reader(dataPayload(frame));
|
|
198
|
+
const header = reader.u8();
|
|
199
|
+
if (header >> 2 !== DOMAIN_MCSPDU.ATTACH_USER_CONFIRM) {
|
|
200
|
+
throw new Error(`RDP: expected an MCS attach-user confirm, got PDU ${header >> 2}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = reader.u8();
|
|
204
|
+
if (result !== 0) throw new Error(`RDP: the host refused to attach a user (result ${result})`);
|
|
205
|
+
|
|
206
|
+
// The initiator is only present when the header's low bit says so.
|
|
207
|
+
const userId = header & 0x02 ? reader.u16be() + MCS_BASE_CHANNEL_ID : MCS_BASE_CHANNEL_ID;
|
|
208
|
+
return { userId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function channelJoinRequest(userId: number, channelId: number) {
|
|
212
|
+
const writer = new Writer(8);
|
|
213
|
+
writer.u8(domainHeader(DOMAIN_MCSPDU.CHANNEL_JOIN_REQUEST));
|
|
214
|
+
writer.u16be(userId - MCS_BASE_CHANNEL_ID);
|
|
215
|
+
writer.u16be(channelId);
|
|
216
|
+
return dataFrame(writer.take());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function parseChannelJoinConfirm(frame: Uint8Array) {
|
|
220
|
+
const reader = new Reader(dataPayload(frame));
|
|
221
|
+
const header = reader.u8();
|
|
222
|
+
if (header >> 2 !== DOMAIN_MCSPDU.CHANNEL_JOIN_CONFIRM) {
|
|
223
|
+
throw new Error(`RDP: expected an MCS channel-join confirm, got PDU ${header >> 2}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = reader.u8();
|
|
227
|
+
reader.u16be(); // initiator
|
|
228
|
+
const requested = reader.u16be();
|
|
229
|
+
if (result !== 0) {
|
|
230
|
+
throw new Error(`RDP: the host refused to join channel ${requested} (result ${result})`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The confirmed channel id is normally echoed after the requested one.
|
|
234
|
+
const channelId = reader.remaining >= 2 ? reader.u16be() : requested;
|
|
235
|
+
return { channelId: channelId || requested };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function sendDataRequest(userId: number, channelId: number, payload: Uint8Array) {
|
|
239
|
+
const writer = new Writer(payload.length + 16);
|
|
240
|
+
writer.u8(domainHeader(DOMAIN_MCSPDU.SEND_DATA_REQUEST));
|
|
241
|
+
writer.u16be(userId - MCS_BASE_CHANNEL_ID);
|
|
242
|
+
writer.u16be(channelId);
|
|
243
|
+
writer.u8(0x70); // dataPriority high, segmentation begin + end
|
|
244
|
+
perLength(writer, payload.length);
|
|
245
|
+
writer.raw(payload);
|
|
246
|
+
return dataFrame(writer.take());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Classify an inbound X.224 data frame. Everything the server sends after the
|
|
251
|
+
* connect sequence arrives as a send-data indication on some channel.
|
|
252
|
+
*/
|
|
253
|
+
export function parseDomainPdu(frame: Uint8Array) {
|
|
254
|
+
const reader = new Reader(dataPayload(frame));
|
|
255
|
+
const header = reader.u8();
|
|
256
|
+
const type = header >> 2;
|
|
257
|
+
|
|
258
|
+
if (type === DOMAIN_MCSPDU.SEND_DATA_INDICATION) {
|
|
259
|
+
reader.u16be(); // initiator
|
|
260
|
+
const channelId = reader.u16be();
|
|
261
|
+
reader.u8(); // flags
|
|
262
|
+
const length = perReadLength(reader);
|
|
263
|
+
const payload = reader.rest();
|
|
264
|
+
return {
|
|
265
|
+
type: 'send-data',
|
|
266
|
+
channelId,
|
|
267
|
+
payload: payload.subarray(0, Math.min(length, payload.length)),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (type === DOMAIN_MCSPDU.DISCONNECT_PROVIDER_ULTIMATUM) {
|
|
272
|
+
// The reason straddles a byte boundary: one bit from the header, one from
|
|
273
|
+
// the byte that follows (MS-RDPBCGR 2.2.2.3).
|
|
274
|
+
const reason = ((header & 0x01) << 1) | (reader.remaining > 0 ? reader.u8() >> 7 : 0);
|
|
275
|
+
return {
|
|
276
|
+
type: 'disconnect',
|
|
277
|
+
reason,
|
|
278
|
+
message: DISCONNECT_REASONS[reason] || `The host closed the RDP session (reason ${reason}).`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { type: 'other', mcsPduType: type };
|
|
283
|
+
}
|