ai-remote 0.4.2 → 0.4.4
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/dist/index.js +1 -6
- package/dist/{protocols/index.d.ts → protocols.d.ts} +2 -0
- package/dist/protocols.js +24 -0
- package/package.json +15 -10
- package/dist/protocols/index.js +0 -34
- package/dist/protocols/rdp/buffer.d.ts +0 -38
- package/dist/protocols/rdp/buffer.js +0 -169
- package/dist/protocols/rdp/caps.d.ts +0 -49
- package/dist/protocols/rdp/caps.js +0 -259
- package/dist/protocols/rdp/cert.d.ts +0 -18
- package/dist/protocols/rdp/cert.js +0 -134
- package/dist/protocols/rdp/client.d.ts +0 -56
- package/dist/protocols/rdp/client.js +0 -1069
- package/dist/protocols/rdp/cliprdr.d.ts +0 -27
- package/dist/protocols/rdp/cliprdr.js +0 -239
- package/dist/protocols/rdp/credssp.d.ts +0 -34
- package/dist/protocols/rdp/credssp.js +0 -253
- package/dist/protocols/rdp/crypto.d.ts +0 -63
- package/dist/protocols/rdp/crypto.js +0 -368
- package/dist/protocols/rdp/display.d.ts +0 -38
- package/dist/protocols/rdp/display.js +0 -588
- package/dist/protocols/rdp/gcc.d.ts +0 -23
- package/dist/protocols/rdp/gcc.js +0 -131
- package/dist/protocols/rdp/keymap.d.ts +0 -9
- package/dist/protocols/rdp/keymap.js +0 -131
- package/dist/protocols/rdp/mcs.d.ts +0 -40
- package/dist/protocols/rdp/mcs.js +0 -222
- package/dist/protocols/rdp/ntlm.d.ts +0 -61
- package/dist/protocols/rdp/ntlm.js +0 -304
- package/dist/protocols/rdp/pdu.d.ts +0 -155
- package/dist/protocols/rdp/pdu.js +0 -443
- package/dist/protocols/rdp/rail.d.ts +0 -119
- package/dist/protocols/rdp/rail.js +0 -382
- package/dist/protocols/rdp/rle.d.ts +0 -13
- package/dist/protocols/rdp/rle.js +0 -275
- package/dist/protocols/rdp/sec.d.ts +0 -59
- package/dist/protocols/rdp/sec.js +0 -155
- package/dist/protocols/rdp/session.d.ts +0 -62
- package/dist/protocols/rdp/session.js +0 -306
- package/dist/protocols/rdp/tls.d.ts +0 -18
- package/dist/protocols/rdp/tls.js +0 -353
- package/dist/protocols/rdp/vchannel.d.ts +0 -28
- package/dist/protocols/rdp/vchannel.js +0 -58
- package/dist/protocols/rdp/x224.d.ts +0 -40
- package/dist/protocols/rdp/x224.js +0 -109
- package/dist/protocols/ssh/kex.d.ts +0 -97
- package/dist/protocols/ssh/kex.js +0 -176
- package/dist/protocols/ssh/messages.d.ts +0 -50
- package/dist/protocols/ssh/messages.js +0 -54
- package/dist/protocols/ssh/session.d.ts +0 -66
- package/dist/protocols/ssh/session.js +0 -608
- package/dist/protocols/ssh/terminal.d.ts +0 -46
- package/dist/protocols/ssh/terminal.js +0 -190
- package/dist/protocols/ssh/transport.d.ts +0 -62
- package/dist/protocols/ssh/transport.js +0 -612
- package/dist/protocols/ssh/wire.d.ts +0 -52
- package/dist/protocols/ssh/wire.js +0 -188
- package/dist/protocols/types.d.ts +0 -127
- package/dist/protocols/types.js +0 -0
- package/dist/protocols/vnc/input.d.ts +0 -5
- package/dist/protocols/vnc/input.js +0 -15
- package/dist/protocols/vnc/session.d.ts +0 -12
- package/dist/protocols/vnc/session.js +0 -258
- package/dist/shared/connection.d.ts +0 -81
- package/dist/shared/connection.js +0 -113
- package/dist/shared/device.d.ts +0 -26
- package/dist/shared/device.js +0 -0
- package/dist/shared/hosts.d.ts +0 -63
- package/dist/shared/hosts.js +0 -132
- package/dist/shared/icons.d.ts +0 -46
- package/dist/shared/icons.js +0 -41
- package/dist/shared/protocol.d.ts +0 -28
- package/dist/shared/protocol.js +0 -39
- package/dist/shared/signals.d.ts +0 -37
- package/dist/shared/signals.js +0 -41
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
const textEncoder = new TextEncoder();
|
|
2
|
-
const textDecoder = new TextDecoder();
|
|
3
|
-
function encodeUtf8(text) {
|
|
4
|
-
return textEncoder.encode(text);
|
|
5
|
-
}
|
|
6
|
-
function decodeUtf8(bytes) {
|
|
7
|
-
return textDecoder.decode(bytes);
|
|
8
|
-
}
|
|
9
|
-
function concatBytes(...parts) {
|
|
10
|
-
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
11
|
-
const out = new Uint8Array(total);
|
|
12
|
-
let offset = 0;
|
|
13
|
-
for (const part of parts) {
|
|
14
|
-
out.set(part, offset);
|
|
15
|
-
offset += part.length;
|
|
16
|
-
}
|
|
17
|
-
return out;
|
|
18
|
-
}
|
|
19
|
-
function bytesEqual(a, b) {
|
|
20
|
-
if (a.length !== b.length) return false;
|
|
21
|
-
let difference = 0;
|
|
22
|
-
for (let index = 0; index < a.length; index++) difference |= (a[index] ?? 0) ^ (b[index] ?? 0);
|
|
23
|
-
return difference === 0;
|
|
24
|
-
}
|
|
25
|
-
function toBase64(bytes) {
|
|
26
|
-
let binary = "";
|
|
27
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
28
|
-
return btoa(binary);
|
|
29
|
-
}
|
|
30
|
-
function fromBase64(text) {
|
|
31
|
-
const binary = atob(text);
|
|
32
|
-
const bytes = new Uint8Array(binary.length);
|
|
33
|
-
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
34
|
-
return bytes;
|
|
35
|
-
}
|
|
36
|
-
function toBase64Url(bytes) {
|
|
37
|
-
return toBase64(bytes).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
38
|
-
}
|
|
39
|
-
class SshWriter {
|
|
40
|
-
bytes;
|
|
41
|
-
view;
|
|
42
|
-
offset = 0;
|
|
43
|
-
constructor(capacity = 256) {
|
|
44
|
-
this.bytes = new Uint8Array(new ArrayBuffer(capacity));
|
|
45
|
-
this.view = new DataView(this.bytes.buffer);
|
|
46
|
-
}
|
|
47
|
-
#ensure(extra) {
|
|
48
|
-
if (this.offset + extra <= this.bytes.length) return;
|
|
49
|
-
let capacity = Math.max(this.bytes.length * 2, 64);
|
|
50
|
-
while (capacity < this.offset + extra) capacity *= 2;
|
|
51
|
-
const grown = new Uint8Array(new ArrayBuffer(capacity));
|
|
52
|
-
grown.set(this.bytes.subarray(0, this.offset));
|
|
53
|
-
this.bytes = grown;
|
|
54
|
-
this.view = new DataView(grown.buffer);
|
|
55
|
-
}
|
|
56
|
-
u8(value) {
|
|
57
|
-
this.#ensure(1);
|
|
58
|
-
this.view.setUint8(this.offset, value);
|
|
59
|
-
this.offset += 1;
|
|
60
|
-
return this;
|
|
61
|
-
}
|
|
62
|
-
boolean(value) {
|
|
63
|
-
return this.u8(value ? 1 : 0);
|
|
64
|
-
}
|
|
65
|
-
u32(value) {
|
|
66
|
-
this.#ensure(4);
|
|
67
|
-
this.view.setUint32(this.offset, value >>> 0, false);
|
|
68
|
-
this.offset += 4;
|
|
69
|
-
return this;
|
|
70
|
-
}
|
|
71
|
-
raw(bytes) {
|
|
72
|
-
this.#ensure(bytes.length);
|
|
73
|
-
this.bytes.set(bytes, this.offset);
|
|
74
|
-
this.offset += bytes.length;
|
|
75
|
-
return this;
|
|
76
|
-
}
|
|
77
|
-
/** A length-prefixed string. Text is encoded as UTF-8. */
|
|
78
|
-
string(value) {
|
|
79
|
-
const bytes = typeof value === "string" ? encodeUtf8(value) : value;
|
|
80
|
-
return this.u32(bytes.length).raw(bytes);
|
|
81
|
-
}
|
|
82
|
-
nameList(names) {
|
|
83
|
-
return this.string(names.join(","));
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* An mpint: two's complement, big endian, with no leading zero bytes except
|
|
87
|
-
* the one that keeps a positive number from looking negative.
|
|
88
|
-
*/
|
|
89
|
-
mpint(bytes) {
|
|
90
|
-
let start = 0;
|
|
91
|
-
while (start < bytes.length && bytes[start] === 0) start++;
|
|
92
|
-
const magnitude = bytes.subarray(start);
|
|
93
|
-
if (magnitude.length === 0) return this.u32(0);
|
|
94
|
-
if ((magnitude[0] ?? 0) & 128) {
|
|
95
|
-
this.u32(magnitude.length + 1).u8(0);
|
|
96
|
-
return this.raw(magnitude);
|
|
97
|
-
}
|
|
98
|
-
return this.u32(magnitude.length).raw(magnitude);
|
|
99
|
-
}
|
|
100
|
-
get length() {
|
|
101
|
-
return this.offset;
|
|
102
|
-
}
|
|
103
|
-
take() {
|
|
104
|
-
return this.bytes.slice(0, this.offset);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
class SshReader {
|
|
108
|
-
bytes;
|
|
109
|
-
view;
|
|
110
|
-
offset;
|
|
111
|
-
constructor(bytes, offset = 0) {
|
|
112
|
-
this.bytes = bytes;
|
|
113
|
-
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
114
|
-
this.offset = offset;
|
|
115
|
-
}
|
|
116
|
-
get remaining() {
|
|
117
|
-
return this.bytes.length - this.offset;
|
|
118
|
-
}
|
|
119
|
-
#need(count) {
|
|
120
|
-
if (count < 0 || this.remaining < count) {
|
|
121
|
-
throw new Error(`SSH: truncated packet (needed ${count} bytes, ${this.remaining} left)`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
u8() {
|
|
125
|
-
this.#need(1);
|
|
126
|
-
return this.view.getUint8(this.offset++);
|
|
127
|
-
}
|
|
128
|
-
boolean() {
|
|
129
|
-
return this.u8() !== 0;
|
|
130
|
-
}
|
|
131
|
-
u32() {
|
|
132
|
-
this.#need(4);
|
|
133
|
-
const value = this.view.getUint32(this.offset, false);
|
|
134
|
-
this.offset += 4;
|
|
135
|
-
return value;
|
|
136
|
-
}
|
|
137
|
-
raw(count) {
|
|
138
|
-
this.#need(count);
|
|
139
|
-
const slice = this.bytes.subarray(this.offset, this.offset + count);
|
|
140
|
-
this.offset += count;
|
|
141
|
-
return slice;
|
|
142
|
-
}
|
|
143
|
-
/** The bytes of a length-prefixed string. */
|
|
144
|
-
stringBytes() {
|
|
145
|
-
return this.raw(this.u32());
|
|
146
|
-
}
|
|
147
|
-
string() {
|
|
148
|
-
return decodeUtf8(this.stringBytes());
|
|
149
|
-
}
|
|
150
|
-
nameList() {
|
|
151
|
-
const value = this.string();
|
|
152
|
-
return value ? value.split(",") : [];
|
|
153
|
-
}
|
|
154
|
-
/** An mpint's magnitude, with the sign byte removed. */
|
|
155
|
-
mpint() {
|
|
156
|
-
const bytes = this.stringBytes();
|
|
157
|
-
let start = 0;
|
|
158
|
-
while (start < bytes.length && bytes[start] === 0) start++;
|
|
159
|
-
return bytes.subarray(start);
|
|
160
|
-
}
|
|
161
|
-
skip(count) {
|
|
162
|
-
this.#need(count);
|
|
163
|
-
this.offset += count;
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
166
|
-
rest() {
|
|
167
|
-
return this.bytes.subarray(this.offset);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function padStart(bytes, width) {
|
|
171
|
-
if (bytes.length === width) return bytes;
|
|
172
|
-
if (bytes.length > width) return bytes.subarray(bytes.length - width);
|
|
173
|
-
const out = new Uint8Array(width);
|
|
174
|
-
out.set(bytes, width - bytes.length);
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
|
-
export {
|
|
178
|
-
SshReader,
|
|
179
|
-
SshWriter,
|
|
180
|
-
bytesEqual,
|
|
181
|
-
concatBytes,
|
|
182
|
-
decodeUtf8,
|
|
183
|
-
encodeUtf8,
|
|
184
|
-
fromBase64,
|
|
185
|
-
padStart,
|
|
186
|
-
toBase64,
|
|
187
|
-
toBase64Url
|
|
188
|
-
};
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The interface every remote-desktop protocol presents to the UI.
|
|
3
|
-
*
|
|
4
|
-
* The workspace -- toolbar, stats rail, on-screen keyboard, reconnects -- does
|
|
5
|
-
* not care whether a session is VNC or RDP. Everything that does differ is
|
|
6
|
-
* reached through this interface instead of branching at each call site.
|
|
7
|
-
*/
|
|
8
|
-
import type { ConnectionSettings, RemoteAppLaunch } from '../shared/connection';
|
|
9
|
-
import type { RdpSecurity, RemoteProtocol } from '../shared/protocol';
|
|
10
|
-
/** Where the pointer may go, in framebuffer pixels. */
|
|
11
|
-
export interface ScreenGeometry {
|
|
12
|
-
width: number;
|
|
13
|
-
height: number;
|
|
14
|
-
viewport: {
|
|
15
|
-
x: number;
|
|
16
|
-
y: number;
|
|
17
|
-
width: number;
|
|
18
|
-
height: number;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/** Mouse buttons, as the UI names them. */
|
|
22
|
-
export type PointerButton = 0 | 1 | 2;
|
|
23
|
-
/** Wheel movement in discrete remote-protocol notches. Positive is right/down. */
|
|
24
|
-
export interface ScrollDelta {
|
|
25
|
-
x: number;
|
|
26
|
-
y: number;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* A live session. Both drivers expose this, so the AI pane's screen tool and
|
|
30
|
-
* the toolbar never have to ask which protocol they are driving.
|
|
31
|
-
*/
|
|
32
|
-
export interface RemoteSession extends EventTarget {
|
|
33
|
-
readonly protocol: RemoteProtocol;
|
|
34
|
-
disconnect(): void;
|
|
35
|
-
/** True once the desktop has been reached. */
|
|
36
|
-
readonly connected: boolean;
|
|
37
|
-
/** Fit the remote desktop to the pane rather than scrolling it. */
|
|
38
|
-
scaleViewport: boolean;
|
|
39
|
-
/** Watch without sending keyboard or pointer input. */
|
|
40
|
-
viewOnly: boolean;
|
|
41
|
-
/** RFB quality level. RDP ignores it. */
|
|
42
|
-
qualityLevel?: number;
|
|
43
|
-
/** RFB compression level. RDP ignores it. */
|
|
44
|
-
compressionLevel?: number;
|
|
45
|
-
/** The transport the metrics rail instruments. Can appear a tick after construction. */
|
|
46
|
-
readonly socket?: WebSocket | null;
|
|
47
|
-
readonly screenGeometry: ScreenGeometry;
|
|
48
|
-
movePointer(x: number, y: number): boolean;
|
|
49
|
-
clickPointer(x: number, y: number, button?: PointerButton): boolean;
|
|
50
|
-
pointerButton(x: number, y: number, button?: PointerButton, pressed?: boolean): boolean;
|
|
51
|
-
scrollPointer(x: number, y: number, delta: ScrollDelta): boolean;
|
|
52
|
-
typeText(text: string): boolean;
|
|
53
|
-
/** Prefer the protocol clipboard for longer text when it can paste reliably. */
|
|
54
|
-
pasteText?(text: string): boolean | Promise<boolean>;
|
|
55
|
-
/** Press or release one key by keysym. */
|
|
56
|
-
sendKey(keysym: number, code: string | null, down?: boolean): void;
|
|
57
|
-
/** Send text to the host's clipboard, when the protocol carries one. */
|
|
58
|
-
sendClipboard?(text: string): void;
|
|
59
|
-
/**
|
|
60
|
-
* Stop taking keystrokes: the workspace has given the keyboard to something
|
|
61
|
-
* local, such as the terminal or the assistant's message box.
|
|
62
|
-
*
|
|
63
|
-
* Both protocols listen on the element they paint into, so a key only leaves
|
|
64
|
-
* for the host while that element holds focus. Handing focus back is the
|
|
65
|
-
* whole mechanism -- plus, for RDP, letting go of anything still held down,
|
|
66
|
-
* which would otherwise stay down on the far side.
|
|
67
|
-
*/
|
|
68
|
-
releaseKeyboard?(): void;
|
|
69
|
-
/** Ask the host for a different desktop size, when the protocol allows it. */
|
|
70
|
-
requestDesktopSize?(width: number, height: number): void;
|
|
71
|
-
/** The canvas the session paints into, for screenshots and thumbnails. */
|
|
72
|
-
readonly canvas?: HTMLCanvasElement | null;
|
|
73
|
-
}
|
|
74
|
-
/** What the workspace hands a driver to open a session. */
|
|
75
|
-
export interface SessionOptions {
|
|
76
|
-
credentials: {
|
|
77
|
-
username: string;
|
|
78
|
-
password: string;
|
|
79
|
-
};
|
|
80
|
-
domain: string;
|
|
81
|
-
security: RdpSecurity;
|
|
82
|
-
remoteApp: RemoteAppLaunch | null;
|
|
83
|
-
/** Desktop size to ask the host for. */
|
|
84
|
-
width: number;
|
|
85
|
-
height: number;
|
|
86
|
-
/** False on a phone, where resizing the host would make a portrait strip of it. */
|
|
87
|
-
allowRemoteResize: boolean;
|
|
88
|
-
scaleViewport: boolean;
|
|
89
|
-
viewOnly: boolean;
|
|
90
|
-
quality: number;
|
|
91
|
-
compression: number;
|
|
92
|
-
/** Shown in this driver's own log lines. */
|
|
93
|
-
hostLabel: string;
|
|
94
|
-
}
|
|
95
|
-
/** Wording for the one connect dialog, which is shared markup across protocols. */
|
|
96
|
-
export interface DialogCopy {
|
|
97
|
-
usernameLabel: string;
|
|
98
|
-
usernamePlaceholder: string;
|
|
99
|
-
usernameHelp: string;
|
|
100
|
-
passwordLabel: string;
|
|
101
|
-
passwordPlaceholder: string;
|
|
102
|
-
passwordHelp: string;
|
|
103
|
-
/** A VNC session needs some secret; RDP can sign in on the host's own screen. */
|
|
104
|
-
requiresPassword: boolean;
|
|
105
|
-
}
|
|
106
|
-
/** Everything the UI knows about a close, before anyone tries to explain it. */
|
|
107
|
-
export interface DisconnectContext {
|
|
108
|
-
code?: number;
|
|
109
|
-
reason: string;
|
|
110
|
-
/** Pre-rendered ` (WebSocket 1006: ...)` tail, so wording stays consistent. */
|
|
111
|
-
suffix: string;
|
|
112
|
-
}
|
|
113
|
-
/** One protocol implementation. */
|
|
114
|
-
export interface ProtocolDriver {
|
|
115
|
-
readonly protocol: RemoteProtocol;
|
|
116
|
-
/** Used as the prefix of this driver's console lines. */
|
|
117
|
-
readonly label: string;
|
|
118
|
-
readonly defaultPort: number;
|
|
119
|
-
readonly dialog: DialogCopy;
|
|
120
|
-
/** The gateway endpoint for this connection, including any query it needs. */
|
|
121
|
-
buildUrl(origin: string, settings: ConnectionSettings): string;
|
|
122
|
-
createSession(container: HTMLElement, url: string, options: SessionOptions): RemoteSession;
|
|
123
|
-
/** Extra detail worth logging once the first server frame has been parsed. */
|
|
124
|
-
describeState(session: RemoteSession): Record<string, unknown>;
|
|
125
|
-
/** Protocol-specific wording for a close, or null to fall back to the shared text. */
|
|
126
|
-
describeDisconnect(context: DisconnectContext): string | null;
|
|
127
|
-
}
|
package/dist/protocols/types.js
DELETED
|
File without changes
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
function vncWheelMasks(delta) {
|
|
2
|
-
const masks = [];
|
|
3
|
-
const wheels = [
|
|
4
|
-
{ amount: delta.y, negativeMask: 1 << 3, positiveMask: 1 << 4 },
|
|
5
|
-
{ amount: delta.x, negativeMask: 1 << 5, positiveMask: 1 << 6 }
|
|
6
|
-
];
|
|
7
|
-
for (const { amount, negativeMask, positiveMask } of wheels) {
|
|
8
|
-
const mask = amount < 0 ? negativeMask : positiveMask;
|
|
9
|
-
for (let step = 0; step < Math.min(20, Math.abs(amount)); step++) masks.push(mask);
|
|
10
|
-
}
|
|
11
|
-
return masks;
|
|
12
|
-
}
|
|
13
|
-
export {
|
|
14
|
-
vncWheelMasks
|
|
15
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Everything VNC-specific, behind the driver interface the RDP side also
|
|
3
|
-
* implements. noVNC's RFB object does the protocol work; this module decides
|
|
4
|
-
* how it is addressed, constructed, controlled and explained.
|
|
5
|
-
*
|
|
6
|
-
* VNC splits the handshake differently from RDP: noVNC hands authentication to
|
|
7
|
-
* the Worker, because macOS Screen Sharing needs the Apple Diffie-Hellman
|
|
8
|
-
* (type 30) exchange that noVNC does not implement. That is why credentials
|
|
9
|
-
* travel in the URL here and never do for RDP.
|
|
10
|
-
*/
|
|
11
|
-
import type { ProtocolDriver } from '../types';
|
|
12
|
-
export declare const vncDriver: ProtocolDriver;
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import RFB from "@novnc/novnc";
|
|
2
|
-
import { DEFAULT_PORTS } from "../../shared/protocol.js";
|
|
3
|
-
import {
|
|
4
|
-
GATEWAY_KEEPALIVE_SIGNAL,
|
|
5
|
-
GATEWAY_READY_SIGNAL,
|
|
6
|
-
KEEPALIVE_INTERVAL_MS
|
|
7
|
-
} from "../../shared/signals.js";
|
|
8
|
-
import { vncWheelMasks } from "./input.js";
|
|
9
|
-
const DIALOG = {
|
|
10
|
-
usernameLabel: "Username",
|
|
11
|
-
usernamePlaceholder: "macOS username (optional)",
|
|
12
|
-
usernameHelp: "Use your macOS account name. Leave it empty when the VNC server only asks for a password.",
|
|
13
|
-
passwordLabel: "Password",
|
|
14
|
-
passwordPlaceholder: "Screen sharing password",
|
|
15
|
-
passwordHelp: "Use the macOS account password, or the password configured for VNC screen sharing.",
|
|
16
|
-
// A VNC session needs some secret; RDP can sign in on the host's own screen.
|
|
17
|
-
requiresPassword: true
|
|
18
|
-
};
|
|
19
|
-
function buildUrl(origin, settings) {
|
|
20
|
-
const query = new URLSearchParams({ host: settings.host, port: String(settings.port) });
|
|
21
|
-
if (settings.username) query.set("vnc_user", settings.username);
|
|
22
|
-
if (settings.password) query.set("vnc_pass", settings.password);
|
|
23
|
-
return `${origin}/ws?${query}`;
|
|
24
|
-
}
|
|
25
|
-
const BUTTON_MASK = { 0: 1, 1: 2, 2: 4 };
|
|
26
|
-
function eventDetail(event) {
|
|
27
|
-
if (!("detail" in event)) return {};
|
|
28
|
-
const detail = event.detail;
|
|
29
|
-
if (!detail || typeof detail !== "object") return {};
|
|
30
|
-
const copy = {};
|
|
31
|
-
for (const [key, value] of Object.entries(detail)) copy[key] = value;
|
|
32
|
-
return copy;
|
|
33
|
-
}
|
|
34
|
-
class VncSession extends EventTarget {
|
|
35
|
-
protocol = "vnc";
|
|
36
|
-
#rfb;
|
|
37
|
-
#credentials;
|
|
38
|
-
#transport = null;
|
|
39
|
-
#keepalive = null;
|
|
40
|
-
#readySent = false;
|
|
41
|
-
#close = null;
|
|
42
|
-
constructor(container, url, options) {
|
|
43
|
-
super();
|
|
44
|
-
this.#credentials = options.credentials;
|
|
45
|
-
this.#rfb = new RFB(container, url, {
|
|
46
|
-
credentials: {
|
|
47
|
-
username: options.credentials.username,
|
|
48
|
-
password: options.credentials.password
|
|
49
|
-
},
|
|
50
|
-
shared: true,
|
|
51
|
-
wsProtocols: ["binary"]
|
|
52
|
-
});
|
|
53
|
-
this.#rfb._cursor?._canvas?.classList.add("remote-cursor-overlay");
|
|
54
|
-
this.#rfb.showDotCursor = true;
|
|
55
|
-
this.#rfb.resizeSession = options.allowRemoteResize;
|
|
56
|
-
this.#rfb.clipViewport = true;
|
|
57
|
-
this.#rfb.scaleViewport = options.scaleViewport;
|
|
58
|
-
this.#rfb.viewOnly = options.viewOnly;
|
|
59
|
-
this.#rfb.qualityLevel = options.quality;
|
|
60
|
-
this.#rfb.compressionLevel = options.compression;
|
|
61
|
-
this.#bindTransport();
|
|
62
|
-
for (const name of ["connect", "disconnect", "credentialsrequired", "desktopname", "clipboard", "bell"]) {
|
|
63
|
-
this.#rfb.addEventListener(name, (event) => {
|
|
64
|
-
if (name === "credentialsrequired") {
|
|
65
|
-
this.#rfb.sendCredentials({
|
|
66
|
-
username: this.#credentials.username,
|
|
67
|
-
password: this.#credentials.password
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const detail = eventDetail(event);
|
|
71
|
-
if (name === "disconnect" && this.#close) {
|
|
72
|
-
detail.code = this.#close.code;
|
|
73
|
-
detail.reason = this.#close.reason;
|
|
74
|
-
}
|
|
75
|
-
this.dispatchEvent(new CustomEvent(name, { detail }));
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Attach to noVNC's WebSocket once it exists. Construction usually has it
|
|
81
|
-
* already; a retry covers the tick where `_sock._websocket` is still null.
|
|
82
|
-
*/
|
|
83
|
-
#bindTransport() {
|
|
84
|
-
const attach = () => {
|
|
85
|
-
const ws = this.#rfb._sock?._websocket ?? null;
|
|
86
|
-
if (!ws || this.#transport === ws) return Boolean(ws);
|
|
87
|
-
this.#transport = ws;
|
|
88
|
-
const signalReady = () => {
|
|
89
|
-
if (this.#readySent || ws.readyState !== WebSocket.OPEN) return;
|
|
90
|
-
this.#readySent = true;
|
|
91
|
-
ws.send(GATEWAY_READY_SIGNAL);
|
|
92
|
-
};
|
|
93
|
-
ws.addEventListener("open", signalReady);
|
|
94
|
-
ws.addEventListener("close", (event) => {
|
|
95
|
-
this.#close = { code: event.code, reason: event.reason ?? "" };
|
|
96
|
-
this.#stopKeepalive();
|
|
97
|
-
}, true);
|
|
98
|
-
this.#keepalive = setInterval(() => {
|
|
99
|
-
if (ws.readyState !== WebSocket.OPEN) return;
|
|
100
|
-
try {
|
|
101
|
-
ws.send(GATEWAY_KEEPALIVE_SIGNAL);
|
|
102
|
-
const sock = this.#rfb._sock;
|
|
103
|
-
if (sock && this.#rfb._rfbConnectionState === "connected") {
|
|
104
|
-
RFB.messages.fbUpdateRequest(sock, true, 0, 0, 1, 1);
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
}
|
|
108
|
-
}, KEEPALIVE_INTERVAL_MS);
|
|
109
|
-
signalReady();
|
|
110
|
-
return true;
|
|
111
|
-
};
|
|
112
|
-
if (attach()) return;
|
|
113
|
-
let tries = 0;
|
|
114
|
-
const timer = setInterval(() => {
|
|
115
|
-
if (attach() || ++tries > 20) clearInterval(timer);
|
|
116
|
-
}, 25);
|
|
117
|
-
}
|
|
118
|
-
#stopKeepalive() {
|
|
119
|
-
if (this.#keepalive) clearInterval(this.#keepalive);
|
|
120
|
-
this.#keepalive = null;
|
|
121
|
-
}
|
|
122
|
-
get connected() {
|
|
123
|
-
return this.#rfb._rfbConnectionState === "connected";
|
|
124
|
-
}
|
|
125
|
-
get socket() {
|
|
126
|
-
return this.#rfb._sock?._websocket ?? null;
|
|
127
|
-
}
|
|
128
|
-
get canvas() {
|
|
129
|
-
return this.#rfb._canvas;
|
|
130
|
-
}
|
|
131
|
-
get scaleViewport() {
|
|
132
|
-
return this.#rfb.scaleViewport;
|
|
133
|
-
}
|
|
134
|
-
set scaleViewport(enabled) {
|
|
135
|
-
this.#rfb.scaleViewport = enabled;
|
|
136
|
-
}
|
|
137
|
-
get viewOnly() {
|
|
138
|
-
return this.#rfb.viewOnly;
|
|
139
|
-
}
|
|
140
|
-
set viewOnly(enabled) {
|
|
141
|
-
this.#rfb.viewOnly = enabled;
|
|
142
|
-
}
|
|
143
|
-
get qualityLevel() {
|
|
144
|
-
return this.#rfb.qualityLevel;
|
|
145
|
-
}
|
|
146
|
-
set qualityLevel(level) {
|
|
147
|
-
this.#rfb.qualityLevel = level;
|
|
148
|
-
}
|
|
149
|
-
get compressionLevel() {
|
|
150
|
-
return this.#rfb.compressionLevel;
|
|
151
|
-
}
|
|
152
|
-
set compressionLevel(level) {
|
|
153
|
-
this.#rfb.compressionLevel = level;
|
|
154
|
-
}
|
|
155
|
-
get screenGeometry() {
|
|
156
|
-
const width = this.#rfb._fbWidth || 0;
|
|
157
|
-
const height = this.#rfb._fbHeight || 0;
|
|
158
|
-
return { width, height, viewport: { x: 0, y: 0, width, height } };
|
|
159
|
-
}
|
|
160
|
-
/** Internal state worth logging once the first server frame has been parsed. */
|
|
161
|
-
describeState() {
|
|
162
|
-
const socket = this.#rfb._sock;
|
|
163
|
-
return {
|
|
164
|
-
connectionState: this.#rfb._rfbConnectionState,
|
|
165
|
-
initializationState: this.#rfb._rfbInitState,
|
|
166
|
-
queuedReceiveBytes: Math.max(0, (socket?._rQlen ?? 0) - (socket?._rQi ?? 0))
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
disconnect() {
|
|
170
|
-
this.#stopKeepalive();
|
|
171
|
-
this.#rfb.disconnect();
|
|
172
|
-
}
|
|
173
|
-
sendKey(keysym, code, down) {
|
|
174
|
-
this.#rfb.sendKey(keysym, code, down);
|
|
175
|
-
}
|
|
176
|
-
sendClipboard(text) {
|
|
177
|
-
this.#rfb.clipboardPasteFrom(text);
|
|
178
|
-
}
|
|
179
|
-
releaseKeyboard() {
|
|
180
|
-
this.#rfb.blur();
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* noVNC keeps pointer input private -- its own handlers translate browser
|
|
184
|
-
* events into framebuffer coordinates first -- so this reaches for the same
|
|
185
|
-
* wire helper `_sendMouse` ends at, with coordinates already in pixels.
|
|
186
|
-
*/
|
|
187
|
-
#pointer(x, y, mask) {
|
|
188
|
-
const socket = this.#rfb._sock;
|
|
189
|
-
if (!socket || this.#rfb._rfbConnectionState !== "connected" || this.#rfb.viewOnly) return false;
|
|
190
|
-
const width = this.#rfb._fbWidth || 1;
|
|
191
|
-
const height = this.#rfb._fbHeight || 1;
|
|
192
|
-
RFB.messages.pointerEvent(
|
|
193
|
-
socket,
|
|
194
|
-
Math.max(0, Math.min(width - 1, Math.round(x))),
|
|
195
|
-
Math.max(0, Math.min(height - 1, Math.round(y))),
|
|
196
|
-
mask
|
|
197
|
-
);
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
movePointer(x, y) {
|
|
201
|
-
return this.#pointer(x, y, 0);
|
|
202
|
-
}
|
|
203
|
-
clickPointer(x, y, button = 0) {
|
|
204
|
-
if (!this.#pointer(x, y, 0)) return false;
|
|
205
|
-
this.#pointer(x, y, BUTTON_MASK[button]);
|
|
206
|
-
this.#pointer(x, y, 0);
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
pointerButton(x, y, button = 0, pressed = true) {
|
|
210
|
-
return this.#pointer(x, y, pressed ? BUTTON_MASK[button] : 0);
|
|
211
|
-
}
|
|
212
|
-
scrollPointer(x, y, delta) {
|
|
213
|
-
for (const mask of vncWheelMasks(delta)) {
|
|
214
|
-
if (!this.#pointer(x, y, mask)) return false;
|
|
215
|
-
if (!this.#pointer(x, y, 0)) return false;
|
|
216
|
-
}
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* VNC has no Unicode input message, so text is typed as keysyms. For every
|
|
221
|
-
* character in the Basic Multilingual Plane the keysym is the code point.
|
|
222
|
-
*/
|
|
223
|
-
typeText(text) {
|
|
224
|
-
if (this.#rfb._rfbConnectionState !== "connected" || this.#rfb.viewOnly || !text) return false;
|
|
225
|
-
for (const character of text) {
|
|
226
|
-
const point = character.codePointAt(0) ?? 0;
|
|
227
|
-
const keysym = character === "\n" ? 65293 : character === " " ? 65289 : point;
|
|
228
|
-
this.#rfb.sendKey(keysym, null, true);
|
|
229
|
-
this.#rfb.sendKey(keysym, null, false);
|
|
230
|
-
}
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function describeDisconnect({ code, reason, suffix }) {
|
|
235
|
-
if (code === 4001 || /auth|password|credential/i.test(reason)) {
|
|
236
|
-
return `VNC authentication failed${suffix}. Verify the macOS account username and its password.`;
|
|
237
|
-
}
|
|
238
|
-
if (code === 4002 || /apple dh/i.test(reason)) {
|
|
239
|
-
return `macOS VNC authentication handshake failed${suffix}.`;
|
|
240
|
-
}
|
|
241
|
-
if (code === 1011 || /cannot connect|unreachable|tcp error|tcp write/i.test(reason)) {
|
|
242
|
-
return `The Worker could not keep a TCP connection to the VNC host${suffix}. Check that Screen Sharing is running and listening on this address.`;
|
|
243
|
-
}
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
const vncDriver = {
|
|
247
|
-
protocol: "vnc",
|
|
248
|
-
label: "VNC Client",
|
|
249
|
-
defaultPort: DEFAULT_PORTS.vnc,
|
|
250
|
-
dialog: DIALOG,
|
|
251
|
-
buildUrl,
|
|
252
|
-
createSession: (container, url, options) => new VncSession(container, url, options),
|
|
253
|
-
describeState: (session) => session instanceof VncSession ? session.describeState() : {},
|
|
254
|
-
describeDisconnect
|
|
255
|
-
};
|
|
256
|
-
export {
|
|
257
|
-
vncDriver
|
|
258
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One connection, described once.
|
|
3
|
-
*
|
|
4
|
-
* Every way of starting a session -- a saved card on the welcome screen, an
|
|
5
|
-
* address typed by hand, a RemoteApp launcher, a reconnect after a drop --
|
|
6
|
-
* produces exactly this shape and hands it to exactly one dialog. That is the
|
|
7
|
-
* whole point of the type: before it existed, a saved card could only ask for a
|
|
8
|
-
* username and a password, so its port, security layer, domain and picture
|
|
9
|
-
* quality were unreachable once the card had been made.
|
|
10
|
-
*/
|
|
11
|
-
import { type RdpSecurity, type RemoteProtocol } from './protocol';
|
|
12
|
-
/** A published Windows application launched instead of the full desktop. */
|
|
13
|
-
export interface RemoteAppLaunch {
|
|
14
|
-
/** Alias (`||notepad`) or an executable path. */
|
|
15
|
-
program: string;
|
|
16
|
-
workingDir?: string;
|
|
17
|
-
arguments?: string;
|
|
18
|
-
}
|
|
19
|
-
/** The companion terminal offered beside a desktop. */
|
|
20
|
-
export interface SshCompanion {
|
|
21
|
-
enabled: boolean;
|
|
22
|
-
port: number;
|
|
23
|
-
username: string;
|
|
24
|
-
}
|
|
25
|
-
/** Picture and input options. VNC negotiates them; RDP and SSH ignore what does not apply. */
|
|
26
|
-
export interface DisplaySettings {
|
|
27
|
-
/** RFB quality level, 1 (smallest) to 9 (sharpest). */
|
|
28
|
-
quality: number;
|
|
29
|
-
/** RFB compression level, 0 (fastest) to 9 (smallest). */
|
|
30
|
-
compression: number;
|
|
31
|
-
/** Fit the remote desktop to the pane rather than scrolling it. */
|
|
32
|
-
scaling: boolean;
|
|
33
|
-
/** Watch without sending keyboard or pointer input. */
|
|
34
|
-
viewOnly: boolean;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Everything a session needs. `password` is the only field never persisted
|
|
38
|
-
* anywhere -- not in the device library on the gateway, not in localStorage.
|
|
39
|
-
*/
|
|
40
|
-
export interface ConnectionSettings {
|
|
41
|
-
/** Stable identity: protocol, address, port and program. See {@link connectionId}. */
|
|
42
|
-
id: string;
|
|
43
|
-
label: string;
|
|
44
|
-
protocol: RemoteProtocol;
|
|
45
|
-
host: string;
|
|
46
|
-
port: number;
|
|
47
|
-
username: string;
|
|
48
|
-
password: string;
|
|
49
|
-
/** Windows domain or computer name. RDP only. */
|
|
50
|
-
domain: string;
|
|
51
|
-
security: RdpSecurity;
|
|
52
|
-
remoteApp: RemoteAppLaunch | null;
|
|
53
|
-
display: DisplaySettings;
|
|
54
|
-
ssh: SshCompanion;
|
|
55
|
-
/** Open the terminal panel as soon as the desktop appears. */
|
|
56
|
-
openSshOnConnect: boolean;
|
|
57
|
-
}
|
|
58
|
-
export declare const DEFAULT_DISPLAY_SETTINGS: DisplaySettings;
|
|
59
|
-
/**
|
|
60
|
-
* A connection's identity is what it points at, not when it was made. Editing
|
|
61
|
-
* the address therefore makes a different connection rather than silently
|
|
62
|
-
* repointing an existing card, which is the behaviour people expect from a
|
|
63
|
-
* list of machines.
|
|
64
|
-
*/
|
|
65
|
-
export declare function connectionId(input: Pick<ConnectionSettings, 'protocol' | 'host' | 'port'> & {
|
|
66
|
-
remoteApp?: RemoteAppLaunch | null;
|
|
67
|
-
}): string;
|
|
68
|
-
/** A blank connection, ready for the dialog to fill in. */
|
|
69
|
-
export declare function emptyConnection(protocol?: RemoteProtocol): ConnectionSettings;
|
|
70
|
-
export declare function normalizeDisplaySettings(value: unknown): DisplaySettings;
|
|
71
|
-
/**
|
|
72
|
-
* Build a complete, valid connection out of whatever a caller happens to have.
|
|
73
|
-
* Partial input from a saved card, a host list entry, a URL or localStorage all
|
|
74
|
-
* arrive here, so the dialog never has to reason about missing fields.
|
|
75
|
-
*/
|
|
76
|
-
export declare function normalizeConnection(input: Partial<ConnectionSettings> & Record<string, unknown>): ConnectionSettings;
|
|
77
|
-
/** What a connection is worth remembering by -- never the password. */
|
|
78
|
-
export type PersistedConnection = Omit<ConnectionSettings, 'password'>;
|
|
79
|
-
export declare function withoutPassword(settings: ConnectionSettings): PersistedConnection;
|
|
80
|
-
/** A one-line description for the dialog's summary row and the status bar. */
|
|
81
|
-
export declare function describeConnection(settings: ConnectionSettings): string;
|