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.
Files changed (114) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +7 -0
  3. package/dist/index.js.map +7 -0
  4. package/dist/protocols/index.js +35 -0
  5. package/dist/protocols/index.js.map +7 -0
  6. package/dist/protocols/rdp/buffer.js +170 -0
  7. package/dist/protocols/rdp/buffer.js.map +7 -0
  8. package/dist/protocols/rdp/caps.js +260 -0
  9. package/dist/protocols/rdp/caps.js.map +7 -0
  10. package/dist/protocols/rdp/cert.js +135 -0
  11. package/dist/protocols/rdp/cert.js.map +7 -0
  12. package/dist/protocols/rdp/client.js +1068 -0
  13. package/dist/protocols/rdp/client.js.map +7 -0
  14. package/dist/protocols/rdp/cliprdr.js +240 -0
  15. package/dist/protocols/rdp/cliprdr.js.map +7 -0
  16. package/dist/protocols/rdp/credssp.js +254 -0
  17. package/dist/protocols/rdp/credssp.js.map +7 -0
  18. package/dist/protocols/rdp/crypto.js +369 -0
  19. package/dist/protocols/rdp/crypto.js.map +7 -0
  20. package/dist/protocols/rdp/display.js +589 -0
  21. package/dist/protocols/rdp/display.js.map +7 -0
  22. package/dist/protocols/rdp/gcc.js +132 -0
  23. package/dist/protocols/rdp/gcc.js.map +7 -0
  24. package/dist/protocols/rdp/keymap.js +132 -0
  25. package/dist/protocols/rdp/keymap.js.map +7 -0
  26. package/dist/protocols/rdp/mcs.js +223 -0
  27. package/dist/protocols/rdp/mcs.js.map +7 -0
  28. package/dist/protocols/rdp/ntlm.js +305 -0
  29. package/dist/protocols/rdp/ntlm.js.map +7 -0
  30. package/dist/protocols/rdp/pdu.js +444 -0
  31. package/dist/protocols/rdp/pdu.js.map +7 -0
  32. package/dist/protocols/rdp/rail.js +383 -0
  33. package/dist/protocols/rdp/rail.js.map +7 -0
  34. package/dist/protocols/rdp/rle.js +276 -0
  35. package/dist/protocols/rdp/rle.js.map +7 -0
  36. package/dist/protocols/rdp/sec.js +156 -0
  37. package/dist/protocols/rdp/sec.js.map +7 -0
  38. package/dist/protocols/rdp/session.js +307 -0
  39. package/dist/protocols/rdp/session.js.map +7 -0
  40. package/dist/protocols/rdp/tls.js +354 -0
  41. package/dist/protocols/rdp/tls.js.map +7 -0
  42. package/dist/protocols/rdp/vchannel.js +59 -0
  43. package/dist/protocols/rdp/vchannel.js.map +7 -0
  44. package/dist/protocols/rdp/x224.js +110 -0
  45. package/dist/protocols/rdp/x224.js.map +7 -0
  46. package/dist/protocols/ssh/kex.js +177 -0
  47. package/dist/protocols/ssh/kex.js.map +7 -0
  48. package/dist/protocols/ssh/messages.js +55 -0
  49. package/dist/protocols/ssh/messages.js.map +7 -0
  50. package/dist/protocols/ssh/session.js +608 -0
  51. package/dist/protocols/ssh/session.js.map +7 -0
  52. package/dist/protocols/ssh/terminal.js +191 -0
  53. package/dist/protocols/ssh/terminal.js.map +7 -0
  54. package/dist/protocols/ssh/transport.js +611 -0
  55. package/dist/protocols/ssh/transport.js.map +7 -0
  56. package/dist/protocols/ssh/wire.js +189 -0
  57. package/dist/protocols/ssh/wire.js.map +7 -0
  58. package/dist/protocols/types.js +1 -0
  59. package/dist/protocols/types.js.map +7 -0
  60. package/dist/protocols/vnc/input.js +16 -0
  61. package/dist/protocols/vnc/input.js.map +7 -0
  62. package/dist/protocols/vnc/session.js +259 -0
  63. package/dist/protocols/vnc/session.js.map +7 -0
  64. package/dist/shared/connection.js +114 -0
  65. package/dist/shared/connection.js.map +7 -0
  66. package/dist/shared/device.js +1 -0
  67. package/dist/shared/device.js.map +7 -0
  68. package/dist/shared/hosts.js +133 -0
  69. package/dist/shared/hosts.js.map +7 -0
  70. package/dist/shared/icons.js +42 -0
  71. package/dist/shared/icons.js.map +7 -0
  72. package/dist/shared/protocol.js +40 -0
  73. package/dist/shared/protocol.js.map +7 -0
  74. package/dist/shared/signals.js +42 -0
  75. package/dist/shared/signals.js.map +7 -0
  76. package/package.json +82 -0
  77. package/src/index.ts +14 -0
  78. package/src/protocols/index.ts +72 -0
  79. package/src/protocols/rdp/buffer.ts +204 -0
  80. package/src/protocols/rdp/caps.ts +304 -0
  81. package/src/protocols/rdp/cert.ts +190 -0
  82. package/src/protocols/rdp/client.ts +1363 -0
  83. package/src/protocols/rdp/cliprdr.ts +344 -0
  84. package/src/protocols/rdp/credssp.ts +319 -0
  85. package/src/protocols/rdp/crypto.ts +416 -0
  86. package/src/protocols/rdp/display.ts +750 -0
  87. package/src/protocols/rdp/gcc.ts +162 -0
  88. package/src/protocols/rdp/keymap.ts +68 -0
  89. package/src/protocols/rdp/mcs.ts +283 -0
  90. package/src/protocols/rdp/ntlm.ts +380 -0
  91. package/src/protocols/rdp/pdu.ts +497 -0
  92. package/src/protocols/rdp/rail.ts +480 -0
  93. package/src/protocols/rdp/rle.ts +343 -0
  94. package/src/protocols/rdp/sec.ts +184 -0
  95. package/src/protocols/rdp/session.ts +393 -0
  96. package/src/protocols/rdp/tls.ts +452 -0
  97. package/src/protocols/rdp/vchannel.ts +89 -0
  98. package/src/protocols/rdp/x224.ts +139 -0
  99. package/src/protocols/ssh/kex.ts +227 -0
  100. package/src/protocols/ssh/messages.ts +54 -0
  101. package/src/protocols/ssh/session.ts +878 -0
  102. package/src/protocols/ssh/terminal.ts +242 -0
  103. package/src/protocols/ssh/transport.ts +732 -0
  104. package/src/protocols/ssh/wire.ts +220 -0
  105. package/src/protocols/types.ts +146 -0
  106. package/src/protocols/vnc/input.ts +13 -0
  107. package/src/protocols/vnc/novnc.d.ts +75 -0
  108. package/src/protocols/vnc/session.ts +367 -0
  109. package/src/shared/connection.ts +206 -0
  110. package/src/shared/device.ts +28 -0
  111. package/src/shared/hosts.ts +240 -0
  112. package/src/shared/icons.ts +49 -0
  113. package/src/shared/protocol.ts +55 -0
  114. package/src/shared/signals.ts +67 -0
@@ -0,0 +1,480 @@
1
+ // Remote Applications Integrated Locally (RAIL), MS-RDPERP.
2
+ //
3
+ // This module owns the RAIL static virtual-channel messages. Window pixels
4
+ // still use the ordinary RDP graphics path; RAIL supplies the session mode,
5
+ // application launch request, and window lifecycle metadata around them.
6
+
7
+ import { Reader, Writer } from './buffer';
8
+
9
+ export const RAIL_ORDER = {
10
+ EXEC: 0x0001,
11
+ ACTIVATE: 0x0002,
12
+ SYSPARAM: 0x0003,
13
+ SYSCOMMAND: 0x0004,
14
+ HANDSHAKE: 0x0005,
15
+ WINDOW_MOVE: 0x0008,
16
+ CLIENT_STATUS: 0x000b,
17
+ EXEC_RESULT: 0x0080,
18
+ HANDSHAKE_EX: 0x0013,
19
+ };
20
+
21
+ export const RAIL_EXEC_RESULT = {
22
+ 0x0000: 'The remote application started.',
23
+ 0x0001: 'The RemoteApp host could not load its application monitoring hook.',
24
+ 0x0002: 'The RemoteApp host could not decode the application launch request.',
25
+ 0x0003: 'The requested application is not in the host RemoteApp allowlist.',
26
+ 0x0005: 'The requested remote application was not found.',
27
+ 0x0006: 'The RemoteApp host could not start the requested application.',
28
+ 0x0007: 'The RemoteApp session is locked and cannot start the application.',
29
+ };
30
+
31
+ /** WM_SYSCOMMAND values a RAIL client may ask the host to apply to a window. */
32
+ export const SYS_COMMAND = {
33
+ MINIMIZE: 0xf020,
34
+ MAXIMIZE: 0xf030,
35
+ CLOSE: 0xf060,
36
+ RESTORE: 0xf120,
37
+ };
38
+
39
+ /** SW_* show states reported by a window order's ShowState field. */
40
+ export const SHOW_STATE = {
41
+ HIDDEN: 0,
42
+ MINIMIZED: 2,
43
+ MAXIMIZED: 3,
44
+ NORMAL: 5,
45
+ };
46
+
47
+ export const WINDOW_ORDER = {
48
+ TYPE_WINDOW: 0x01000000,
49
+ STATE_NEW: 0x10000000,
50
+ STATE_DELETED: 0x20000000,
51
+ ICON: 0x40000000,
52
+ CACHED_ICON: 0x80000000,
53
+ };
54
+
55
+ /**
56
+ * Window Information Order fields (MS-RDPERP 2.2.1.3.1.2.1). A field is present
57
+ * only when its flag is set, and present fields arrive in the fixed order the
58
+ * parser below reads them in, so an unrecognised combination cannot be skipped
59
+ * field by field -- the order's own size is what re-syncs the stream.
60
+ */
61
+ const WINDOW_FIELD = {
62
+ OWNER: 0x00000002,
63
+ TITLE: 0x00000004,
64
+ STYLE: 0x00000008,
65
+ SHOW: 0x00000010,
66
+ WND_RECTS: 0x00000100,
67
+ WND_SIZE: 0x00000400,
68
+ WND_OFFSET: 0x00000800,
69
+ CLIENT_AREA_OFFSET: 0x00004000,
70
+ CLIENT_DELTA: 0x00008000,
71
+ CLIENT_AREA_SIZE: 0x00010000,
72
+ RP_CONTENT: 0x00020000,
73
+ ROOT_PARENT: 0x00040000,
74
+ VISIBILITY: 0x00080000,
75
+ VIS_OFFSET: 0x00200000,
76
+ };
77
+
78
+ /** Window styles that mark a window as something other than the app's own. */
79
+ const STYLE_CHILD = 0x40000000;
80
+ const EX_STYLE_TOOLWINDOW = 0x00000080;
81
+ const EX_STYLE_NOACTIVATE = 0x08000000;
82
+
83
+ const DRAWING_ORDER_CLASS_MASK = 0x03;
84
+ const DRAWING_ORDER_ALT_SECONDARY = 0x02;
85
+ const ALTSEC_WINDOW = 0x0b;
86
+
87
+ const CLIENT_BUILD = 2600;
88
+ const CLIENT_STATUS_SUPPRESS_ICON_ORDERS = 0x00000400;
89
+
90
+ const SYSTEM_PARAMETER = {
91
+ MOUSE_BUTTON_SWAP: 0x00000021,
92
+ DRAG_FULL_WINDOWS: 0x00000025,
93
+ WORK_AREA: 0x0000002f,
94
+ HIGH_CONTRAST: 0x00000043,
95
+ KEYBOARD_PREF: 0x00000045,
96
+ KEYBOARD_CUES: 0x0000100b,
97
+ };
98
+
99
+ function utf16Bytes(text: string) {
100
+ const writer = new Writer(text.length * 2);
101
+ writer.utf16le(text);
102
+ return writer.take();
103
+ }
104
+
105
+ function pdu(orderType, body: Uint8Array) {
106
+ const writer = new Writer(body.length + 4);
107
+ writer.u16le(orderType).u16le(body.length + 4).raw(body);
108
+ return writer.take();
109
+ }
110
+
111
+ /** Window coordinates are signed: an off-screen or bordered window is normal. */
112
+ function signed32(value) {
113
+ return value | 0;
114
+ }
115
+
116
+ export function railHandshake(buildNumber = CLIENT_BUILD) {
117
+ return pdu(RAIL_ORDER.HANDSHAKE, new Writer(4).u32le(buildNumber).take());
118
+ }
119
+
120
+ export function railClientStatus(flags = CLIENT_STATUS_SUPPRESS_ICON_ORDERS) {
121
+ return pdu(RAIL_ORDER.CLIENT_STATUS, new Writer(4).u32le(flags).take());
122
+ }
123
+
124
+ function railBooleanSystemParameter(parameter, enabled: boolean) {
125
+ return pdu(RAIL_ORDER.SYSPARAM, new Writer(5).u32le(parameter).u8(enabled ? 1 : 0).take());
126
+ }
127
+
128
+ function railHighContrast() {
129
+ const body = new Writer(16);
130
+ body.u32le(SYSTEM_PARAMETER.HIGH_CONTRAST);
131
+ body.u32le(0x0000007e); // HCF_AVAILABLE through HCF_HOTKEYAVAILABLE
132
+ body.u32le(2); // empty color scheme including its terminator
133
+ body.u16le(0).u16le(0); // UNICODE_STRING length and terminator
134
+ return pdu(RAIL_ORDER.SYSPARAM, body.take());
135
+ }
136
+
137
+ /**
138
+ * SPI_SETWORKAREA. This is how a RAIL client tells the host how much room it
139
+ * has, and the host maximizes remote windows into it. Sending it again during
140
+ * the session is what keeps a maximized window matched to the browser window.
141
+ */
142
+ export function railWorkArea(width: number, height: number) {
143
+ const body = new Writer(12);
144
+ body.u32le(SYSTEM_PARAMETER.WORK_AREA);
145
+ body.u16le(0).u16le(0).u16le(width).u16le(height);
146
+ return pdu(RAIL_ORDER.SYSPARAM, body.take());
147
+ }
148
+
149
+ export function railSystemParameters(width: number, height: number) {
150
+ return [
151
+ railHighContrast(),
152
+ railBooleanSystemParameter(SYSTEM_PARAMETER.MOUSE_BUTTON_SWAP, false),
153
+ railBooleanSystemParameter(SYSTEM_PARAMETER.KEYBOARD_PREF, false),
154
+ railBooleanSystemParameter(SYSTEM_PARAMETER.DRAG_FULL_WINDOWS, true),
155
+ railBooleanSystemParameter(SYSTEM_PARAMETER.KEYBOARD_CUES, false),
156
+ railWorkArea(width, height),
157
+ ];
158
+ }
159
+
160
+ /** TS_RAIL_ORDER_ACTIVATE: give (or take) input focus for one remote window. */
161
+ export function railActivate(windowId: number, enabled = true) {
162
+ const body = new Writer(5);
163
+ body.u32le(windowId).u8(enabled ? 1 : 0);
164
+ return pdu(RAIL_ORDER.ACTIVATE, body.take());
165
+ }
166
+
167
+ /** TS_RAIL_ORDER_SYSCOMMAND: minimize, maximize, restore or close a window. */
168
+ export function railSysCommand(windowId: number, command: string) {
169
+ const body = new Writer(6);
170
+ body.u32le(windowId).u16le(command);
171
+ return pdu(RAIL_ORDER.SYSCOMMAND, body.take());
172
+ }
173
+
174
+ /**
175
+ * TS_RAIL_ORDER_WINDOWMOVE. A client that draws remote windows itself sends
176
+ * this when the user finishes moving or resizing one; the host then applies
177
+ * the rectangle to the real window. It is the only way to give a remote window
178
+ * an exact size, which is what fitting one to the browser viewport needs.
179
+ */
180
+ export function railWindowMove(windowId: number, { left, top, right, bottom }) {
181
+ const body = new Writer(12);
182
+ body.u32le(windowId);
183
+ body.u16le(left & 0xffff).u16le(top & 0xffff);
184
+ body.u16le(right & 0xffff).u16le(bottom & 0xffff);
185
+ return pdu(RAIL_ORDER.WINDOW_MOVE, body.take());
186
+ }
187
+
188
+ /** Build TS_RAIL_ORDER_EXEC. Strings are UTF-16LE and are not terminated. */
189
+ export function railExecute({ program, workingDir = '', arguments: args = '' }) {
190
+ const exe = utf16Bytes(program || '');
191
+ const cwd = utf16Bytes(workingDir || '');
192
+ const argv = utf16Bytes(args || '');
193
+
194
+ if (exe.length === 0 || exe.length > 520) {
195
+ throw new Error('RemoteApp program must be between 1 and 260 Unicode characters.');
196
+ }
197
+ if (cwd.length > 520) throw new Error('RemoteApp working directory is too long.');
198
+ if (argv.length > 16000) throw new Error('RemoteApp arguments are too long.');
199
+
200
+ const body = new Writer(8 + exe.length + cwd.length + argv.length);
201
+ body.u16le(0); // executable, with no environment expansion requested
202
+ body.u16le(exe.length).u16le(cwd.length).u16le(argv.length);
203
+ body.raw(exe).raw(cwd).raw(argv);
204
+ return pdu(RAIL_ORDER.EXEC, body.take());
205
+ }
206
+
207
+ function decodeUtf16(bytes: Uint8Array) {
208
+ let result = '';
209
+ for (let i = 0; i + 1 < bytes.length; i += 2) {
210
+ const codeUnit = bytes[i] | (bytes[i + 1] << 8);
211
+ if (codeUnit === 0) break;
212
+ result += String.fromCharCode(codeUnit);
213
+ }
214
+ return result;
215
+ }
216
+
217
+ function readRectangles(reader: Reader, count: number) {
218
+ const rectangles = [];
219
+ for (let index = 0; index < count && reader.remaining >= 8; index++) {
220
+ const left = reader.u16le();
221
+ const top = reader.u16le();
222
+ const right = reader.u16le();
223
+ const bottom = reader.u16le();
224
+ rectangles.push({ left, top, right, bottom });
225
+ }
226
+ return rectangles;
227
+ }
228
+
229
+ /**
230
+ * Read the present fields of one Window Information Order. Fields the client
231
+ * has no use for are still read, because every later field's position depends
232
+ * on them.
233
+ */
234
+ function parseWindowFields(reader: Reader, fieldFlags) {
235
+ const fields = {};
236
+
237
+ if (fieldFlags & WINDOW_FIELD.OWNER) fields.ownerWindowId = reader.u32le();
238
+ if (fieldFlags & WINDOW_FIELD.STYLE) {
239
+ fields.style = reader.u32le();
240
+ fields.extendedStyle = reader.u32le();
241
+ }
242
+ if (fieldFlags & WINDOW_FIELD.SHOW) fields.showState = reader.u8();
243
+ if (fieldFlags & WINDOW_FIELD.TITLE) {
244
+ const titleBytes = reader.u16le();
245
+ fields.title = decodeUtf16(reader.raw(Math.min(titleBytes, reader.remaining)));
246
+ }
247
+ if (fieldFlags & WINDOW_FIELD.CLIENT_AREA_OFFSET) {
248
+ fields.clientOffsetX = signed32(reader.u32le());
249
+ fields.clientOffsetY = signed32(reader.u32le());
250
+ }
251
+ if (fieldFlags & WINDOW_FIELD.CLIENT_AREA_SIZE) {
252
+ fields.clientWidth = signed32(reader.u32le());
253
+ fields.clientHeight = signed32(reader.u32le());
254
+ }
255
+ if (fieldFlags & WINDOW_FIELD.RP_CONTENT) reader.u8();
256
+ if (fieldFlags & WINDOW_FIELD.ROOT_PARENT) reader.u32le();
257
+ if (fieldFlags & WINDOW_FIELD.WND_OFFSET) {
258
+ fields.x = signed32(reader.u32le());
259
+ fields.y = signed32(reader.u32le());
260
+ }
261
+ if (fieldFlags & WINDOW_FIELD.CLIENT_DELTA) {
262
+ reader.u32le();
263
+ reader.u32le();
264
+ }
265
+ if (fieldFlags & WINDOW_FIELD.WND_SIZE) {
266
+ fields.width = signed32(reader.u32le());
267
+ fields.height = signed32(reader.u32le());
268
+ }
269
+ if (fieldFlags & WINDOW_FIELD.WND_RECTS) {
270
+ fields.windowRects = readRectangles(reader, reader.u16le());
271
+ }
272
+ if (fieldFlags & WINDOW_FIELD.VIS_OFFSET) {
273
+ fields.visibleOffsetX = signed32(reader.u32le());
274
+ fields.visibleOffsetY = signed32(reader.u32le());
275
+ }
276
+ if (fieldFlags & WINDOW_FIELD.VISIBILITY) {
277
+ fields.visibilityRects = readRectangles(reader, reader.u16le());
278
+ }
279
+
280
+ return fields;
281
+ }
282
+
283
+ /**
284
+ * True for a window the session exists to show: not a child, not a tool
285
+ * window, and not one of the never-activated popups a Chromium or Electron
286
+ * application creates for menus, tooltips and drag images.
287
+ */
288
+ export function isPrimaryWindow(candidate) {
289
+ if (!candidate) return false;
290
+ if (candidate.ownerWindowId) return false;
291
+ if ((candidate.style || 0) & STYLE_CHILD) return false;
292
+ if ((candidate.extendedStyle || 0) & (EX_STYLE_TOOLWINDOW | EX_STYLE_NOACTIVATE)) return false;
293
+ return true;
294
+ }
295
+
296
+ /**
297
+ * Parse the window lifecycle subset of TS_UPDATE_ORDERS_PDU_DATA.
298
+ *
299
+ * RemoteApp window orders are Alternate Secondary orders and, unlike ordinary
300
+ * drawing orders, carry their total size in every header. That lets the parser
301
+ * read the fields it understands and then seek to the next order, rather than
302
+ * having to understand every icon, notification and desktop order to stay in
303
+ * sync with the stream.
304
+ */
305
+ export function parseRemoteAppWindowOrders(reader: Reader) {
306
+ if (reader.remaining < 6) throw new Error('RDP RemoteApp orders update is truncated.');
307
+
308
+ reader.u16le(); // pad2OctetsA
309
+ const numberOrders = reader.u16le();
310
+ reader.u16le(); // pad2OctetsB
311
+ const events = [];
312
+
313
+ for (let index = 0; index < numberOrders && reader.remaining > 0; index++) {
314
+ const orderStart = reader.offset;
315
+ const controlFlags = reader.u8();
316
+ const orderClass = controlFlags & DRAWING_ORDER_CLASS_MASK;
317
+ const orderType = controlFlags >> 2;
318
+
319
+ // No primary drawing orders are advertised. If a broken host mixes one in,
320
+ // stop rather than guessing its variable-length encoding and losing sync.
321
+ if (orderClass !== DRAWING_ORDER_ALT_SECONDARY || orderType !== ALTSEC_WINDOW) break;
322
+ if (reader.remaining < 6) throw new Error('RDP RemoteApp window-order header is truncated.');
323
+
324
+ const orderSize = reader.u16le();
325
+ const fieldFlags = reader.u32le();
326
+ const orderEnd = orderStart + orderSize;
327
+ if (orderSize < 7 || orderEnd > reader.bytes.length) {
328
+ throw new Error('RDP RemoteApp window order has an invalid size.');
329
+ }
330
+
331
+ const describesWindow = Boolean(fieldFlags & WINDOW_ORDER.TYPE_WINDOW);
332
+ const isIconOrder = Boolean(fieldFlags & (WINDOW_ORDER.ICON | WINDOW_ORDER.CACHED_ICON));
333
+
334
+ if (describesWindow && !isIconOrder && orderEnd - reader.offset >= 4) {
335
+ const windowId = reader.u32le();
336
+ // A malformed order must not take the session down: the next update
337
+ // repeats the window state anyway, and the seek below keeps the stream
338
+ // aligned either way.
339
+ let fields = {};
340
+ try {
341
+ fields = parseWindowFields(reader, fieldFlags);
342
+ } catch (error) {
343
+ console.warn('[RDP] Dropped an unreadable RemoteApp window order', {
344
+ windowId,
345
+ fieldFlags: `0x${(fieldFlags >>> 0).toString(16)}`,
346
+ error: error instanceof Error ? error.message : String(error),
347
+ });
348
+ }
349
+
350
+ let type = 'update';
351
+ if (fieldFlags & WINDOW_ORDER.STATE_DELETED) type = 'deleted';
352
+ else if (fieldFlags & WINDOW_ORDER.STATE_NEW) type = 'new';
353
+ events.push({ type, windowId, fieldFlags, ...fields });
354
+ }
355
+
356
+ // The order's own size is authoritative, so an order carrying fields this
357
+ // parser skipped still leaves the reader on the next order's first byte.
358
+ reader.offset = orderEnd;
359
+ if (reader.offset <= orderStart) break;
360
+ }
361
+
362
+ return events;
363
+ }
364
+
365
+ /** Parse one or more complete RAIL PDUs delivered in a channel message. */
366
+ export function parseRailPdus(bytes: Uint8Array) {
367
+ const reader = new Reader(bytes);
368
+ const messages = [];
369
+
370
+ while (reader.remaining >= 4) {
371
+ const orderType = reader.u16le();
372
+ const orderLength = reader.u16le();
373
+ if (orderLength < 4 || orderLength - 4 > reader.remaining) {
374
+ throw new Error('RDP RemoteApp channel delivered a malformed PDU.');
375
+ }
376
+
377
+ const body = new Reader(reader.raw(orderLength - 4));
378
+ if (orderType === RAIL_ORDER.HANDSHAKE && body.remaining >= 4) {
379
+ messages.push({ type: 'handshake', buildNumber: body.u32le() });
380
+ continue;
381
+ }
382
+ if (orderType === RAIL_ORDER.HANDSHAKE_EX && body.remaining >= 8) {
383
+ messages.push({ type: 'handshake', buildNumber: body.u32le(), flags: body.u32le(), extended: true });
384
+ continue;
385
+ }
386
+ if (orderType === RAIL_ORDER.EXEC_RESULT && body.remaining >= 12) {
387
+ const flags = body.u16le();
388
+ const result = body.u16le();
389
+ const rawResult = body.u32le();
390
+ body.u16le(); // padding
391
+ const nameLength = body.u16le();
392
+ const program = decodeUtf16(body.raw(Math.min(nameLength, body.remaining)));
393
+ messages.push({ type: 'execute-result', flags, result, rawResult, program });
394
+ continue;
395
+ }
396
+
397
+ messages.push({ type: 'unknown', orderType, body: body.rest().slice() });
398
+ }
399
+
400
+ return messages;
401
+ }
402
+
403
+ /**
404
+ * Minimal RAIL endpoint for the browser client. It deliberately suppresses
405
+ * icon orders because the browser currently presents the RemoteApp session in
406
+ * one canvas rather than creating a local OS window per remote window.
407
+ */
408
+ export class RailChannel {
409
+ private readonly application: RemoteAppLaunch;
410
+ private width: number;
411
+ private height: number;
412
+ private readonly send: (bytes: Uint8Array) => void;
413
+ private readonly onReady: () => void;
414
+ private readonly onResult: (result: RailResult) => void;
415
+ private started: boolean;
416
+
417
+ constructor({ application, width, height, send, onReady, onResult }) {
418
+ this.application = application;
419
+ this.width = width;
420
+ this.height = height;
421
+ this.send = send;
422
+ this.onReady = onReady;
423
+ this.onResult = onResult;
424
+ this.started = false;
425
+ }
426
+
427
+ receive(bytes: Uint8Array) {
428
+ for (const message of parseRailPdus(bytes)) {
429
+ if (message.type === 'handshake') {
430
+ if (this.started) continue;
431
+ this.started = true;
432
+ this.send(railHandshake(CLIENT_BUILD));
433
+ this.send(railClientStatus());
434
+ for (const systemParameter of railSystemParameters(this.width, this.height)) {
435
+ this.send(systemParameter);
436
+ }
437
+ this.send(railExecute(this.application));
438
+ this.onReady?.(message);
439
+ continue;
440
+ }
441
+
442
+ if (message.type === 'execute-result') {
443
+ this.onResult?.({
444
+ ...message,
445
+ ok: message.result === 0,
446
+ message: RAIL_EXEC_RESULT[message.result] ||
447
+ `The RemoteApp host returned launch result 0x${message.result.toString(16).padStart(4, '0')}.`,
448
+ });
449
+ }
450
+ }
451
+ }
452
+
453
+ /** Republish the client's work area, which is what maximize resizes into. */
454
+ setWorkArea(width: number, height: number) {
455
+ if (!this.started || width <= 0 || height <= 0) return false;
456
+ if (width === this.width && height === this.height) return false;
457
+ this.width = width;
458
+ this.height = height;
459
+ this.send(railWorkArea(width, height));
460
+ return true;
461
+ }
462
+
463
+ activate(windowId: number) {
464
+ if (!this.started) return false;
465
+ this.send(railActivate(windowId, true));
466
+ return true;
467
+ }
468
+
469
+ sysCommand(windowId: number, command: string) {
470
+ if (!this.started) return false;
471
+ this.send(railSysCommand(windowId, command));
472
+ return true;
473
+ }
474
+
475
+ moveWindow(windowId: number, rectangle) {
476
+ if (!this.started) return false;
477
+ this.send(railWindowMove(windowId, rectangle));
478
+ return true;
479
+ }
480
+ }