ai-remote 0.3.1 → 0.4.1

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 (165) hide show
  1. package/dist/cli.mjs +232 -81
  2. package/{src/index.ts → dist/index.d.ts} +0 -1
  3. package/dist/index.js +0 -1
  4. package/dist/protocols/index.d.ts +31 -0
  5. package/dist/protocols/index.js +0 -1
  6. package/dist/protocols/rdp/buffer.d.ts +38 -0
  7. package/dist/protocols/rdp/buffer.js +0 -1
  8. package/dist/protocols/rdp/caps.d.ts +49 -0
  9. package/dist/protocols/rdp/caps.js +0 -1
  10. package/dist/protocols/rdp/cert.d.ts +18 -0
  11. package/dist/protocols/rdp/cert.js +0 -1
  12. package/dist/protocols/rdp/client.d.ts +56 -0
  13. package/dist/protocols/rdp/client.js +0 -1
  14. package/dist/protocols/rdp/cliprdr.d.ts +27 -0
  15. package/dist/protocols/rdp/cliprdr.js +0 -1
  16. package/dist/protocols/rdp/credssp.d.ts +34 -0
  17. package/dist/protocols/rdp/credssp.js +0 -1
  18. package/dist/protocols/rdp/crypto.d.ts +63 -0
  19. package/dist/protocols/rdp/crypto.js +0 -1
  20. package/dist/protocols/rdp/display.d.ts +38 -0
  21. package/dist/protocols/rdp/display.js +0 -1
  22. package/dist/protocols/rdp/gcc.d.ts +23 -0
  23. package/dist/protocols/rdp/gcc.js +0 -1
  24. package/dist/protocols/rdp/keymap.d.ts +9 -0
  25. package/dist/protocols/rdp/keymap.js +0 -1
  26. package/dist/protocols/rdp/mcs.d.ts +40 -0
  27. package/dist/protocols/rdp/mcs.js +0 -1
  28. package/dist/protocols/rdp/ntlm.d.ts +61 -0
  29. package/dist/protocols/rdp/ntlm.js +0 -1
  30. package/dist/protocols/rdp/pdu.d.ts +155 -0
  31. package/dist/protocols/rdp/pdu.js +0 -1
  32. package/dist/protocols/rdp/rail.d.ts +119 -0
  33. package/dist/protocols/rdp/rail.js +0 -1
  34. package/dist/protocols/rdp/rle.d.ts +13 -0
  35. package/dist/protocols/rdp/rle.js +0 -1
  36. package/dist/protocols/rdp/sec.d.ts +59 -0
  37. package/dist/protocols/rdp/sec.js +0 -1
  38. package/dist/protocols/rdp/session.d.ts +62 -0
  39. package/dist/protocols/rdp/session.js +0 -1
  40. package/dist/protocols/rdp/tls.d.ts +18 -0
  41. package/dist/protocols/rdp/tls.js +0 -1
  42. package/dist/protocols/rdp/vchannel.d.ts +28 -0
  43. package/dist/protocols/rdp/vchannel.js +0 -1
  44. package/dist/protocols/rdp/x224.d.ts +40 -0
  45. package/dist/protocols/rdp/x224.js +0 -1
  46. package/dist/protocols/ssh/kex.d.ts +97 -0
  47. package/dist/protocols/ssh/kex.js +0 -1
  48. package/dist/protocols/ssh/messages.d.ts +50 -0
  49. package/dist/protocols/ssh/messages.js +0 -1
  50. package/dist/protocols/ssh/session.d.ts +66 -0
  51. package/dist/protocols/ssh/session.js +0 -1
  52. package/dist/protocols/ssh/terminal.d.ts +46 -0
  53. package/dist/protocols/ssh/terminal.js +0 -1
  54. package/dist/protocols/ssh/transport.d.ts +62 -0
  55. package/dist/protocols/ssh/transport.js +0 -1
  56. package/dist/protocols/ssh/wire.d.ts +52 -0
  57. package/dist/protocols/ssh/wire.js +0 -1
  58. package/dist/protocols/types.d.ts +127 -0
  59. package/dist/protocols/types.js +0 -1
  60. package/dist/protocols/vnc/input.d.ts +5 -0
  61. package/dist/protocols/vnc/input.js +0 -1
  62. package/dist/protocols/vnc/session.d.ts +12 -0
  63. package/dist/protocols/vnc/session.js +0 -1
  64. package/dist/shared/connection.d.ts +81 -0
  65. package/dist/shared/connection.js +0 -1
  66. package/dist/shared/device.d.ts +26 -0
  67. package/dist/shared/device.js +0 -1
  68. package/dist/shared/hosts.d.ts +63 -0
  69. package/dist/shared/hosts.js +0 -1
  70. package/dist/shared/icons.d.ts +46 -0
  71. package/dist/shared/icons.js +0 -1
  72. package/dist/shared/protocol.d.ts +28 -0
  73. package/dist/shared/protocol.js +0 -1
  74. package/dist/shared/signals.d.ts +37 -0
  75. package/dist/shared/signals.js +0 -1
  76. package/package.json +16 -14
  77. package/dist/index.js.map +0 -7
  78. package/dist/protocols/index.js.map +0 -7
  79. package/dist/protocols/rdp/buffer.js.map +0 -7
  80. package/dist/protocols/rdp/caps.js.map +0 -7
  81. package/dist/protocols/rdp/cert.js.map +0 -7
  82. package/dist/protocols/rdp/client.js.map +0 -7
  83. package/dist/protocols/rdp/cliprdr.js.map +0 -7
  84. package/dist/protocols/rdp/credssp.js.map +0 -7
  85. package/dist/protocols/rdp/crypto.js.map +0 -7
  86. package/dist/protocols/rdp/display.js.map +0 -7
  87. package/dist/protocols/rdp/gcc.js.map +0 -7
  88. package/dist/protocols/rdp/keymap.js.map +0 -7
  89. package/dist/protocols/rdp/mcs.js.map +0 -7
  90. package/dist/protocols/rdp/ntlm.js.map +0 -7
  91. package/dist/protocols/rdp/pdu.js.map +0 -7
  92. package/dist/protocols/rdp/rail.js.map +0 -7
  93. package/dist/protocols/rdp/rle.js.map +0 -7
  94. package/dist/protocols/rdp/sec.js.map +0 -7
  95. package/dist/protocols/rdp/session.js.map +0 -7
  96. package/dist/protocols/rdp/tls.js.map +0 -7
  97. package/dist/protocols/rdp/vchannel.js.map +0 -7
  98. package/dist/protocols/rdp/x224.js.map +0 -7
  99. package/dist/protocols/ssh/kex.js.map +0 -7
  100. package/dist/protocols/ssh/messages.js.map +0 -7
  101. package/dist/protocols/ssh/session.js.map +0 -7
  102. package/dist/protocols/ssh/terminal.js.map +0 -7
  103. package/dist/protocols/ssh/transport.js.map +0 -7
  104. package/dist/protocols/ssh/wire.js.map +0 -7
  105. package/dist/protocols/types.js.map +0 -7
  106. package/dist/protocols/vnc/input.js.map +0 -7
  107. package/dist/protocols/vnc/session.js.map +0 -7
  108. package/dist/shared/connection.js.map +0 -7
  109. package/dist/shared/device.js.map +0 -7
  110. package/dist/shared/hosts.js.map +0 -7
  111. package/dist/shared/icons.js.map +0 -7
  112. package/dist/shared/protocol.js.map +0 -7
  113. package/dist/shared/signals.js.map +0 -7
  114. package/src/cli/cli.ts +0 -452
  115. package/src/cli/daemon.ts +0 -295
  116. package/src/cli/framebuffer.ts +0 -115
  117. package/src/cli/generated/viewer-bundle.ts +0 -5
  118. package/src/cli/ipc.ts +0 -78
  119. package/src/cli/paths.ts +0 -28
  120. package/src/cli/png.ts +0 -106
  121. package/src/cli/session.ts +0 -263
  122. package/src/cli/shell.ts +0 -184
  123. package/src/cli/transport.ts +0 -86
  124. package/src/cli/viewer-client/assets.d.ts +0 -4
  125. package/src/cli/viewer-client/main.ts +0 -300
  126. package/src/cli/viewer-page.ts +0 -102
  127. package/src/cli/viewer.ts +0 -272
  128. package/src/cli/wsserver.ts +0 -144
  129. package/src/protocols/index.ts +0 -72
  130. package/src/protocols/rdp/buffer.ts +0 -204
  131. package/src/protocols/rdp/caps.ts +0 -304
  132. package/src/protocols/rdp/cert.ts +0 -190
  133. package/src/protocols/rdp/client.ts +0 -1371
  134. package/src/protocols/rdp/cliprdr.ts +0 -344
  135. package/src/protocols/rdp/credssp.ts +0 -319
  136. package/src/protocols/rdp/crypto.ts +0 -416
  137. package/src/protocols/rdp/display.ts +0 -750
  138. package/src/protocols/rdp/gcc.ts +0 -162
  139. package/src/protocols/rdp/keymap.ts +0 -68
  140. package/src/protocols/rdp/mcs.ts +0 -283
  141. package/src/protocols/rdp/ntlm.ts +0 -380
  142. package/src/protocols/rdp/pdu.ts +0 -497
  143. package/src/protocols/rdp/rail.ts +0 -480
  144. package/src/protocols/rdp/rle.ts +0 -343
  145. package/src/protocols/rdp/sec.ts +0 -184
  146. package/src/protocols/rdp/session.ts +0 -393
  147. package/src/protocols/rdp/tls.ts +0 -452
  148. package/src/protocols/rdp/vchannel.ts +0 -89
  149. package/src/protocols/rdp/x224.ts +0 -139
  150. package/src/protocols/ssh/kex.ts +0 -227
  151. package/src/protocols/ssh/messages.ts +0 -54
  152. package/src/protocols/ssh/session.ts +0 -879
  153. package/src/protocols/ssh/terminal.ts +0 -242
  154. package/src/protocols/ssh/transport.ts +0 -745
  155. package/src/protocols/ssh/wire.ts +0 -220
  156. package/src/protocols/types.ts +0 -146
  157. package/src/protocols/vnc/input.ts +0 -13
  158. package/src/protocols/vnc/novnc.d.ts +0 -75
  159. package/src/protocols/vnc/session.ts +0 -367
  160. package/src/shared/connection.ts +0 -206
  161. package/src/shared/device.ts +0 -28
  162. package/src/shared/hosts.ts +0 -240
  163. package/src/shared/icons.ts +0 -49
  164. package/src/shared/protocol.ts +0 -55
  165. package/src/shared/signals.ts +0 -67
package/src/cli/viewer.ts DELETED
@@ -1,272 +0,0 @@
1
- /**
2
- * The window a human watches the session through.
3
- *
4
- * The session already exists and already holds the pixels; this is an
5
- * attachment to it. Viewers may come and go, and there may be none at all --
6
- * a viewer that could end a session would make supervision a dependency
7
- * rather than a feature.
8
- *
9
- * A viewer that has just attached needs the whole desktop, not a delta, which
10
- * is exactly what the framebuffer is for: it sends one keyframe on connect and
11
- * painted rectangles after that.
12
- */
13
-
14
- import { createServer } from 'node:http';
15
- import type net from 'node:net';
16
- import { randomBytes } from 'node:crypto';
17
- import { spawn } from 'node:child_process';
18
- import { deflateSync } from 'node:zlib';
19
- import type { RdpSession } from './session';
20
- import type { Shell } from './shell';
21
- import { accept, type WebSocketPeer } from './wsserver';
22
- import { PAGE } from './viewer-page';
23
-
24
- export interface Viewer {
25
- url: string;
26
- /** Resolves when a browser has actually attached. */
27
- opened: Promise<void>;
28
- /** Pop the window again, for a viewer that was closed by hand. */
29
- launch(): void;
30
- close(): void;
31
- }
32
-
33
- /**
34
- * Frame layout, little-endian:
35
- * u8 kind 1 = keyframe, 2 = rectangle
36
- * u16 left, u16 top, u16 width, u16 height
37
- * deflated RGBA
38
- */
39
- function encodeRect(kind: number, left: number, top: number, width: number, height: number, rgba: Uint8Array): Buffer {
40
- const header = Buffer.alloc(9);
41
- header.writeUInt8(kind, 0);
42
- header.writeUInt16LE(left, 1);
43
- header.writeUInt16LE(top, 3);
44
- header.writeUInt16LE(width, 5);
45
- header.writeUInt16LE(height, 7);
46
- return Buffer.concat([header, deflateSync(rgba, { level: 1 })]);
47
- }
48
-
49
- export async function startViewer(
50
- session: RdpSession,
51
- port: number,
52
- { launch = true, openShell }: { launch?: boolean; openShell?: () => Promise<Shell> } = {}
53
- ): Promise<Viewer> {
54
- const token = randomBytes(16).toString('hex');
55
- const peers = new Set<WebSocketPeer>();
56
-
57
- /**
58
- * Which viewers may send input.
59
- *
60
- * The gate lives here rather than in the page, because a gate in the page is
61
- * not a gate: anything that can open a socket can skip it. A viewer attaches
62
- * as an observer and has to ask for control, which is also the right default
63
- * when the session was opened by an agent -- the human watches first.
64
- */
65
- const drivers = new WeakSet<WebSocketPeer>();
66
-
67
- /** Undoes each viewer's subscription to shell output when it goes away. */
68
- const detachShell = new Map<WebSocketPeer, () => void>();
69
-
70
- /**
71
- * Attach a viewer to the terminal, opening it if this is the first ask.
72
- *
73
- * The scrollback goes out first, so a viewer that opens the terminal ten
74
- * minutes into a session sees what has already been run rather than an empty
75
- * pane that looks broken.
76
- */
77
- async function attachShell(peer: WebSocketPeer): Promise<void> {
78
- if (!openShell) {
79
- peer.send(JSON.stringify({
80
- type: 'shell-error',
81
- message: 'This session has no terminal available.',
82
- }));
83
- return;
84
- }
85
- if (detachShell.has(peer)) return;
86
-
87
- peer.send(JSON.stringify({ type: 'shell-status', state: 'opening' }));
88
- let shell: Shell;
89
- try {
90
- shell = await openShell();
91
- } catch (error) {
92
- peer.send(JSON.stringify({
93
- type: 'shell-error',
94
- message: error instanceof Error ? error.message : String(error),
95
- }));
96
- return;
97
- }
98
-
99
- const history = shell.scrollback;
100
- if (history.length) {
101
- peer.send(JSON.stringify({ type: 'shell-data', base64: Buffer.from(history).toString('base64') }));
102
- }
103
- peer.send(JSON.stringify({ type: 'shell-status', state: 'open' }));
104
-
105
- detachShell.set(peer, shell.onData((bytes) => {
106
- peer.send(JSON.stringify({ type: 'shell-data', base64: Buffer.from(bytes).toString('base64') }));
107
- }));
108
-
109
- shellFor.set(peer, shell);
110
- }
111
-
112
- const shellFor = new Map<WebSocketPeer, Shell>();
113
-
114
- let resolveOpened: () => void;
115
- const opened = new Promise<void>((resolve) => { resolveOpened = resolve; });
116
-
117
- const server = createServer((request, response) => {
118
- const url = new URL(request.url ?? '/', 'http://127.0.0.1');
119
- if (url.pathname === '/' && url.searchParams.get('t') === token) {
120
- response.writeHead(200, {
121
- 'Content-Type': 'text/html; charset=utf-8',
122
- 'Cache-Control': 'no-store',
123
- });
124
- response.end(PAGE);
125
- return;
126
- }
127
- response.writeHead(404, { 'Content-Type': 'text/plain' });
128
- response.end('Not found');
129
- });
130
-
131
- server.on('upgrade', (request, socket) => {
132
- const url = new URL(request.url ?? '/', 'http://127.0.0.1');
133
- if (url.searchParams.get('t') !== token) { socket.destroy(); return; }
134
-
135
- // Node types an upgrade socket as Duplex; every runtime it has is a
136
- // net.Socket, which is what lets the peer turn Nagle off.
137
- const peer = accept(request, socket as net.Socket);
138
- if (!peer) return;
139
- peers.add(peer);
140
- resolveOpened();
141
-
142
- const { width, height } = session.framebuffer;
143
- peer.send(JSON.stringify({ type: 'hello', width, height, host: session.options.host }));
144
- if (width && height) {
145
- peer.send(encodeRect(1, 0, 0, width, height, session.framebuffer.pixels));
146
- }
147
-
148
- peer.addEventListener('message', (event: Event) => {
149
- const raw = (event as MessageEvent).data as string;
150
- if (process.env.REMOTECTL_DEBUG) console.log('[viewer] <-', raw);
151
- try {
152
- const message = JSON.parse(raw);
153
-
154
- if (message.type === 'control') {
155
- if (message.on) drivers.add(peer); else drivers.delete(peer);
156
- peer.send(JSON.stringify({ type: 'control', on: Boolean(message.on) }));
157
- return;
158
- }
159
-
160
- if (message.type === 'shell-attach') { void attachShell(peer); return; }
161
-
162
- if (!drivers.has(peer)) return;
163
-
164
- if (message.type === 'shell-input') {
165
- shellFor.get(peer)?.write(String(message.text ?? ''));
166
- return;
167
- }
168
- if (message.type === 'shell-resize') {
169
- shellFor.get(peer)?.resize(Number(message.columns) || 120, Number(message.rows) || 30);
170
- return;
171
- }
172
-
173
- applyInput(session, message);
174
- } catch (error) {
175
- // A malformed frame from a viewer must never take the session down.
176
- console.warn('[viewer] dropped an unreadable message', error);
177
- }
178
- });
179
- peer.addEventListener('close', () => {
180
- peers.delete(peer);
181
- detachShell.get(peer)?.();
182
- detachShell.delete(peer);
183
- });
184
- });
185
-
186
- /**
187
- * Loopback only. The viewer carries a live desktop and takes input for it.
188
- *
189
- * A busy port is not a reason to fail: a second session, or a stale process
190
- * holding the default, would otherwise take the whole connection down. The
191
- * chosen port is reported back, so nothing has to guess which one it got.
192
- */
193
- const bound = await new Promise<number>((resolve, reject) => {
194
- const listen = (candidate: number, retry: boolean) => {
195
- server.once('error', (error: NodeJS.ErrnoException) => {
196
- if (retry && error.code === 'EADDRINUSE') { listen(0, false); return; }
197
- reject(error);
198
- });
199
- server.listen(candidate, '127.0.0.1', () => {
200
- const address = server.address();
201
- resolve(typeof address === 'object' && address ? address.port : candidate);
202
- });
203
- };
204
- listen(port, true);
205
- });
206
-
207
- session.onResize((width, height) => {
208
- for (const peer of peers) {
209
- peer.send(JSON.stringify({ type: 'resize', width, height }));
210
- peer.send(encodeRect(1, 0, 0, width, height, session.framebuffer.pixels));
211
- }
212
- });
213
-
214
- session.onPaint((rects) => {
215
- if (!peers.size) return;
216
- for (const rect of rects) {
217
- const payload = encodeRect(2, rect.left, rect.top, rect.width, rect.height, rect.rgba);
218
- for (const peer of peers) peer.send(payload);
219
- }
220
- });
221
-
222
- const url = `http://127.0.0.1:${bound}/?t=${token}`;
223
- if (launch) open(url);
224
-
225
- return {
226
- url,
227
- opened,
228
- launch() { open(url); },
229
- close() {
230
- for (const peer of peers) peer.close();
231
- server.close();
232
- },
233
- };
234
- }
235
-
236
- function applyInput(session: RdpSession, message: any): void {
237
- switch (message.type) {
238
- case 'move':
239
- session.movePointer(message.x, message.y);
240
- break;
241
- case 'button':
242
- session.pointerButton(message.x, message.y, message.button ?? 0, Boolean(message.pressed));
243
- break;
244
- case 'scroll':
245
- session.scroll(message.x, message.y, message.dx ?? 0, message.dy ?? 0);
246
- break;
247
- case 'key':
248
- session.sendKeyCode(message.code, Boolean(message.pressed));
249
- break;
250
- case 'text':
251
- session.typeText(String(message.text ?? ''));
252
- break;
253
- case 'cad':
254
- session.sendCtrlAltDel();
255
- break;
256
- default:
257
- break;
258
- }
259
- }
260
-
261
- /** Pop the window. A failure here is not a failure of the session. */
262
- function open(url: string): void {
263
- const command = process.platform === 'darwin' ? 'open'
264
- : process.platform === 'win32' ? 'cmd'
265
- : 'xdg-open';
266
- const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
267
- try {
268
- spawn(command, args, { stdio: 'ignore', detached: true }).unref();
269
- } catch {
270
- // Headless, or no browser. The URL was printed; that is enough.
271
- }
272
- }
@@ -1,144 +0,0 @@
1
- /**
2
- * Just enough WebSocket to talk to a browser tab.
3
- *
4
- * Node has no WebSocket server, and pulling one in would cost `npx remotectl`
5
- * its single-download start. What the viewer needs is small: an upgrade
6
- * handshake, binary frames out, text frames in.
7
- */
8
-
9
- import { createHash } from 'node:crypto';
10
- import type { IncomingMessage } from 'node:http';
11
- import type net from 'node:net';
12
-
13
- const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
14
-
15
- export class WebSocketPeer extends EventTarget {
16
- #socket: net.Socket;
17
- #buffer = Buffer.alloc(0);
18
- /** Stop writing. Set as soon as closing begins, from either end. */
19
- #closed = false;
20
- /**
21
- * Whether the close event has gone out.
22
- *
23
- * Kept apart from #closed on purpose: one flag doing both jobs meant a peer
24
- * that closed itself -- which is what a browser tab does when it goes away --
25
- * swallowed its own close event, and the session went on sending frames to a
26
- * viewer that had left.
27
- */
28
- #reported = false;
29
-
30
- constructor(socket: net.Socket) {
31
- super();
32
- this.#socket = socket;
33
- socket.on('data', (chunk: Buffer) => this.#onData(chunk));
34
- socket.on('close', () => this.#onClose());
35
- socket.on('error', () => this.#onClose());
36
- }
37
-
38
- get closed(): boolean { return this.#closed; }
39
-
40
- send(data: string | Uint8Array): void {
41
- if (this.#closed) return;
42
- const isText = typeof data === 'string';
43
- const payload = isText ? Buffer.from(data, 'utf8') : Buffer.from(data);
44
- this.#socket.write(frame(isText ? 0x1 : 0x2, payload));
45
- }
46
-
47
- close(): void {
48
- if (!this.#closed) {
49
- this.#closed = true;
50
- try { this.#socket.end(frame(0x8, Buffer.alloc(0))); } catch { /* already gone */ }
51
- }
52
- this.#onClose();
53
- }
54
-
55
- #onClose(): void {
56
- this.#closed = true;
57
- if (this.#reported) return;
58
- this.#reported = true;
59
- this.dispatchEvent(new Event('close'));
60
- }
61
-
62
- #onData(chunk: Buffer): void {
63
- if (process.env.REMOTECTL_DEBUG) console.log('[ws] data', chunk.length, 'bytes');
64
- this.#buffer = Buffer.concat([this.#buffer, chunk]);
65
-
66
- for (;;) {
67
- const parsed = readFrame(this.#buffer);
68
- if (!parsed) return;
69
- this.#buffer = parsed.rest;
70
-
71
- if (parsed.opcode === 0x8) { this.close(); return; }
72
- if (parsed.opcode === 0x9) { this.#socket.write(frame(0xa, parsed.payload)); continue; }
73
- if (parsed.opcode === 0x1) {
74
- this.dispatchEvent(new MessageEvent('message', { data: parsed.payload.toString('utf8') }));
75
- }
76
- }
77
- }
78
- }
79
-
80
- /** Server frames are never masked, and the viewer sends nothing large. */
81
- function frame(opcode: number, payload: Buffer): Buffer {
82
- const length = payload.length;
83
- let header: Buffer;
84
-
85
- if (length < 126) {
86
- header = Buffer.from([0x80 | opcode, length]);
87
- } else if (length < 65536) {
88
- header = Buffer.alloc(4);
89
- header[0] = 0x80 | opcode;
90
- header[1] = 126;
91
- header.writeUInt16BE(length, 2);
92
- } else {
93
- header = Buffer.alloc(10);
94
- header[0] = 0x80 | opcode;
95
- header[1] = 127;
96
- header.writeBigUInt64BE(BigInt(length), 2);
97
- }
98
-
99
- return Buffer.concat([header, payload]);
100
- }
101
-
102
- function readFrame(buffer: Buffer) {
103
- if (buffer.length < 2) return null;
104
-
105
- const opcode = buffer[0] & 0x0f;
106
- const masked = (buffer[1] & 0x80) !== 0;
107
- let length = buffer[1] & 0x7f;
108
- let offset = 2;
109
-
110
- if (length === 126) {
111
- if (buffer.length < 4) return null;
112
- length = buffer.readUInt16BE(2);
113
- offset = 4;
114
- } else if (length === 127) {
115
- if (buffer.length < 10) return null;
116
- length = Number(buffer.readBigUInt64BE(2));
117
- offset = 10;
118
- }
119
-
120
- const maskKey = masked ? buffer.subarray(offset, offset + 4) : null;
121
- if (masked) offset += 4;
122
- if (buffer.length < offset + length) return null;
123
-
124
- const payload = Buffer.from(buffer.subarray(offset, offset + length));
125
- if (maskKey) for (let i = 0; i < payload.length; i++) payload[i] ^= maskKey[i % 4];
126
-
127
- return { opcode, payload, rest: buffer.subarray(offset + length) };
128
- }
129
-
130
- /** Complete the HTTP upgrade a browser sends, and hand back the peer. */
131
- export function accept(request: IncomingMessage, socket: net.Socket): WebSocketPeer | null {
132
- const key = request.headers['sec-websocket-key'];
133
- if (typeof key !== 'string') { socket.destroy(); return null; }
134
-
135
- const acceptKey = createHash('sha1').update(key + GUID).digest('base64');
136
- socket.write(
137
- 'HTTP/1.1 101 Switching Protocols\r\n'
138
- + 'Upgrade: websocket\r\n'
139
- + 'Connection: Upgrade\r\n'
140
- + `Sec-WebSocket-Accept: ${acceptKey}\r\n\r\n`
141
- );
142
- socket.setNoDelay(true);
143
- return new WebSocketPeer(socket);
144
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * The protocol registry the workspace talks to.
3
- *
4
- * The UI owns the toolbar, the dialogs, the stats rail and the on-screen
5
- * keyboard, none of which care whether a session is VNC or RDP. Everything that
6
- * does differ -- the endpoint, how a client is constructed, the wording of a
7
- * failure, the labels in the connect dialog -- is looked up here instead of
8
- * branching at each site.
9
- */
10
-
11
- import { rdpDriver } from './rdp/session';
12
- import { vncDriver } from './vnc/session';
13
- import type { DisconnectContext, ProtocolDriver } from './types';
14
- import type { RemoteProtocol } from '../shared/protocol';
15
-
16
- export type { ProtocolDriver, RemoteSession, SessionOptions, ScreenGeometry } from './types';
17
-
18
- export const DRIVERS: Record<RemoteProtocol, ProtocolDriver> = {
19
- vnc: vncDriver,
20
- rdp: rdpDriver,
21
- };
22
-
23
- /** Unknown names fall back to VNC, which is what the dialog opens on. */
24
- export function driverFor(protocol: string | undefined): ProtocolDriver {
25
- return DRIVERS[protocol as RemoteProtocol] ?? DRIVERS.vnc;
26
- }
27
-
28
- /** What the workspace knows about a session that has just ended. */
29
- export interface DisconnectDiagnostic {
30
- protocol: RemoteProtocol;
31
- /** The transport close, when one arrived. A 1006 means it did not. */
32
- close: { code: number; reason: string } | null;
33
- /** True once the session had reached the desktop. */
34
- established: boolean;
35
- }
36
-
37
- /**
38
- * Explain a disconnect in terms someone can act on. The protocol gets first
39
- * say, then the close codes that mean the same thing either way.
40
- */
41
- export function explainDisconnect(
42
- diagnostic: DisconnectDiagnostic,
43
- clean: boolean,
44
- protocolMessage: string
45
- ): string {
46
- // A client that reports its own failure states it in the protocol's terms,
47
- // which is always more specific than anything the transport can say.
48
- if (protocolMessage) return protocolMessage;
49
-
50
- const code = diagnostic.close?.code;
51
- const reason = diagnostic.close?.reason.trim() ?? '';
52
- const suffix = code ? ` (WebSocket ${code}${reason ? `: ${reason}` : ''})` : '';
53
- const context: DisconnectContext = { ...(code === undefined ? {} : { code }), reason, suffix };
54
-
55
- const driver = driverFor(diagnostic.protocol);
56
- const specific = driver.describeDisconnect(context);
57
- if (specific) return specific;
58
-
59
- const name = driver.protocol.toUpperCase();
60
- if (/tcp (reached )?eof/i.test(reason)) {
61
- return `The ${name} host accepted the connection and then closed it${suffix}. `
62
- + 'Check its remote-access permissions and the account name.';
63
- }
64
- if (code === 1006) {
65
- return 'The connection was interrupted before a close reason arrived (WebSocket 1006). '
66
- + 'This can happen during a deployment or a brief network change. Automatic reconnect '
67
- + 'did not restore the session; reconnect once, then check Worker live logs if it repeats.';
68
- }
69
- if (reason) return `The remote session ended${suffix}.`;
70
- if (clean) return `The remote server closed the session cleanly${suffix || ' (no reason supplied)'}.`;
71
- return `The ${name} negotiation failed${suffix || ' before the server supplied a close reason'}.`;
72
- }
@@ -1,204 +0,0 @@
1
- // Binary reader/writer used by every RDP layer. RDP mixes endianness freely --
2
- // TPKT and MCS are big endian, everything above them is little endian -- so the
3
- // byte order is part of each call rather than a mode on the object.
4
-
5
- /**
6
- * Plain `Uint8Array` throughout. Pinning the buffer type parameter would be
7
- * more precise, but every layer above this one passes byte slices around and
8
- * the variance between `ArrayBuffer` and `ArrayBufferLike` then has to be
9
- * restated at each boundary for no benefit the protocol code can use.
10
- */
11
- function allocate(length: number): Uint8Array {
12
- return new Uint8Array(length);
13
- }
14
-
15
- export class Writer {
16
- private bytes: Uint8Array;
17
- private view: DataView;
18
- private offset = 0;
19
-
20
- constructor(capacity = 512) {
21
- this.bytes = allocate(capacity);
22
- this.view = new DataView(this.bytes.buffer);
23
- }
24
-
25
- #ensure(extra: number): void {
26
- if (this.offset + extra <= this.bytes.length) return;
27
- let capacity = this.bytes.length * 2;
28
- while (capacity < this.offset + extra) capacity *= 2;
29
- const grown = allocate(capacity);
30
- grown.set(this.bytes.subarray(0, this.offset));
31
- this.bytes = grown;
32
- this.view = new DataView(grown.buffer);
33
- }
34
-
35
- u8(value: number): this {
36
- this.#ensure(1);
37
- this.view.setUint8(this.offset, value);
38
- this.offset += 1;
39
- return this;
40
- }
41
-
42
- u16le(value: number): this {
43
- this.#ensure(2);
44
- this.view.setUint16(this.offset, value, true);
45
- this.offset += 2;
46
- return this;
47
- }
48
-
49
- u16be(value: number): this {
50
- this.#ensure(2);
51
- this.view.setUint16(this.offset, value, false);
52
- this.offset += 2;
53
- return this;
54
- }
55
-
56
- u32le(value: number): this {
57
- this.#ensure(4);
58
- this.view.setUint32(this.offset, value >>> 0, true);
59
- this.offset += 4;
60
- return this;
61
- }
62
-
63
- u32be(value: number): this {
64
- this.#ensure(4);
65
- this.view.setUint32(this.offset, value >>> 0, false);
66
- this.offset += 4;
67
- return this;
68
- }
69
-
70
- raw(bytes: Uint8Array): this {
71
- this.#ensure(bytes.length);
72
- this.bytes.set(bytes, this.offset);
73
- this.offset += bytes.length;
74
- return this;
75
- }
76
-
77
- zeros(count: number): this {
78
- this.#ensure(count);
79
- this.offset += count;
80
- return this;
81
- }
82
-
83
- /** UTF-16LE, optionally padded or truncated to a fixed byte width. */
84
- utf16le(text: string, fixedBytes?: number): this {
85
- const start = this.offset;
86
- for (const unit of text) {
87
- const code = unit.codePointAt(0) ?? 0;
88
- // Fixed-width fields must keep room for the terminating NUL.
89
- if (fixedBytes !== undefined && this.offset - start >= fixedBytes - 2) break;
90
- if (code > 0xffff) {
91
- // Surrogate pair: both halves are already what UTF-16 needs.
92
- for (let i = 0; i < unit.length; i++) this.u16le(unit.charCodeAt(i));
93
- } else {
94
- this.u16le(code);
95
- }
96
- }
97
- if (fixedBytes !== undefined) {
98
- this.zeros(fixedBytes - (this.offset - start));
99
- }
100
- return this;
101
- }
102
-
103
- /** Overwrite an earlier 16-bit slot, for length fields known only later. */
104
- patchU16le(position: number, value: number): this {
105
- this.view.setUint16(position, value, true);
106
- return this;
107
- }
108
-
109
- patchU16be(position: number, value: number): this {
110
- this.view.setUint16(position, value, false);
111
- return this;
112
- }
113
-
114
- get length(): number {
115
- return this.offset;
116
- }
117
-
118
- take(): Uint8Array {
119
- return this.bytes.slice(0, this.offset);
120
- }
121
- }
122
-
123
- export class Reader {
124
- private readonly bytes: Uint8Array;
125
- private readonly view: DataView;
126
- offset: number;
127
-
128
- constructor(bytes: Uint8Array, offset = 0) {
129
- this.bytes = bytes;
130
- this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
131
- this.offset = offset;
132
- }
133
-
134
- get remaining(): number {
135
- return this.bytes.length - this.offset;
136
- }
137
-
138
- #need(count: number): void {
139
- if (this.remaining < count) {
140
- throw new Error(`RDP: truncated frame (needed ${count} bytes, ${this.remaining} left)`);
141
- }
142
- }
143
-
144
- u8(): number {
145
- this.#need(1);
146
- return this.view.getUint8(this.offset++);
147
- }
148
-
149
- u16le(): number {
150
- this.#need(2);
151
- const value = this.view.getUint16(this.offset, true);
152
- this.offset += 2;
153
- return value;
154
- }
155
-
156
- u16be(): number {
157
- this.#need(2);
158
- const value = this.view.getUint16(this.offset, false);
159
- this.offset += 2;
160
- return value;
161
- }
162
-
163
- u32le(): number {
164
- this.#need(4);
165
- const value = this.view.getUint32(this.offset, true);
166
- this.offset += 4;
167
- return value;
168
- }
169
-
170
- u32be(): number {
171
- this.#need(4);
172
- const value = this.view.getUint32(this.offset, false);
173
- this.offset += 4;
174
- return value;
175
- }
176
-
177
- raw(count: number): Uint8Array {
178
- this.#need(count);
179
- const slice = this.bytes.subarray(this.offset, this.offset + count);
180
- this.offset += count;
181
- return slice;
182
- }
183
-
184
- skip(count: number): this {
185
- this.#need(count);
186
- this.offset += count;
187
- return this;
188
- }
189
-
190
- rest(): Uint8Array {
191
- return this.bytes.subarray(this.offset);
192
- }
193
- }
194
-
195
- export function concatBytes(...parts: readonly Uint8Array[]): Uint8Array {
196
- const total = parts.reduce((sum, part) => sum + part.length, 0);
197
- const out = allocate(total);
198
- let offset = 0;
199
- for (const part of parts) {
200
- out.set(part, offset);
201
- offset += part.length;
202
- }
203
- return out;
204
- }