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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/protocols/ssh/transport.ts"],
|
|
4
|
+
"sourcesContent": ["// The SSH transport layer (RFC 4253): version exchange, the binary packet\n// protocol, key exchange, and the AES-GCM cipher that protects everything\n// after it.\n//\n// Like the RDP client, this runs entirely in the browser. The gateway's\n// /ssh-ws endpoint is a byte relay and never sees a decrypted packet or a\n// password; the keys derived here exist only in this tab.\n\nimport { MSG, DISCONNECT_REASON } from './messages';\nimport {\n SshReader,\n SshWriter,\n concatBytes,\n encodeUtf8,\n} from './wire';\nimport {\n createKexClient,\n deriveKey,\n exchangeHash,\n hostKeyFingerprint,\n hostKeyType,\n negotiableAlgorithms,\n verifyHostKeySignature,\n} from './kex';\nimport {\n GATEWAY_KEEPALIVE_SIGNAL,\n GATEWAY_READY_SIGNAL,\n KEEPALIVE_INTERVAL_MS,\n} from '../../shared/signals';\n\nexport const CLIENT_VERSION = 'SSH-2.0-CloudflareGateway_1.0';\n\n/** Only AEAD ciphers are offered, so the MAC list is never actually used. */\nconst CIPHERS = {\n 'aes256-gcm@openssh.com': { keyLength: 32 },\n 'aes128-gcm@openssh.com': { keyLength: 16 },\n};\nconst CIPHER_NAMES = Object.keys(CIPHERS);\nconst MAC_NAMES = ['hmac-sha2-256', 'hmac-sha2-512'];\nconst COMPRESSION_NAMES = ['none'];\n\nconst GCM_BLOCK = 16;\nconst GCM_TAG_BYTES = 16;\nconst GCM_IV_BYTES = 12;\nconst PLAIN_BLOCK = 8;\nconst MIN_PADDING = 4;\nconst MAX_PACKET_BYTES = 262_144;\nconst VERSION_LINE_LIMIT = 8_192;\n\nconst subtle = globalThis.crypto?.subtle;\n\n/** Pick the first client preference the host also offered (RFC 4253 7.1). */\nexport function negotiate(clientNames, serverNames) {\n return clientNames.find((name: string) => serverNames.includes(name)) || null;\n}\n\n/** An AES-GCM direction: one key, and an invocation counter that never repeats. */\nclass GcmCipher {\n key: CryptoKey;\n fixed: Uint8Array;\n counter: Uint8Array;\n\n constructor(key: CryptoKey, iv: Uint8Array) {\n this.key = key;\n this.fixed = iv.slice(0, 4);\n this.counter = iv.slice(4, 12);\n }\n\n static async create(keyBytes: Uint8Array, iv: Uint8Array) {\n const key = await subtle.importKey('raw', keyBytes as unknown as BufferSource, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);\n return new GcmCipher(key, iv);\n }\n\n /** The 12-byte nonce for this packet, then step the 64-bit counter. */\n nextIv() {\n const iv = concatBytes(this.fixed, this.counter);\n for (let index = this.counter.length - 1; index >= 0; index--) {\n this.counter[index] = (this.counter[index] + 1) & 0xff;\n if (this.counter[index] !== 0) break;\n }\n return iv;\n }\n}\n\nexport class SshTransport extends EventTarget {\n url: string;\n verifyHost: (info: any) => Promise<boolean> | boolean;\n log: (...args: any[]) => void;\n socket: WebSocket | null;\n closed: boolean;\n buffer: Uint8Array;\n serverVersion: string;\n greeting: string[];\n sendSequence: number;\n receiveSequence: number;\n outgoing: any;\n incoming: any;\n sessionId: Uint8Array | null;\n kex: any;\n kexQueue: Uint8Array[];\n keepaliveTimer: any;\n bytesSent: number;\n bytesReceived: number;\n encryptChain?: Promise<void>;\n newKeysBarrier?: Promise<void> | null;\n releaseNewKeys?: (() => void) | null;\n pendingServerKexInit?: Uint8Array | null;\n established?: boolean;\n\n /**\n * @param {string} url gateway WebSocket URL\n * @param {object} options\n * @param {(info: object) => Promise<boolean>} options.verifyHost decides\n * whether an unknown or changed host key may be used.\n */\n constructor(url: string, { verifyHost, log }: { verifyHost?: (info: any) => Promise<boolean> | boolean; log?: (...args: any[]) => void } = {}) {\n super();\n\n this.url = url;\n this.verifyHost = verifyHost || (() => true);\n this.log = log || (() => {});\n\n this.socket = null;\n this.closed = false;\n this.buffer = new Uint8Array(0);\n this.serverVersion = '';\n this.greeting = [];\n\n this.sendSequence = 0;\n this.receiveSequence = 0;\n this.outgoing = null;\n this.incoming = null;\n this.sessionId = null;\n\n this.kex = null;\n this.kexQueue = [];\n this.keepaliveTimer = null;\n this.bytesSent = 0;\n this.bytesReceived = 0;\n }\n\n // --- connection ----------------------------------------------------------\n\n connect() {\n this.socket = new WebSocket(this.url);\n this.socket.binaryType = 'arraybuffer';\n\n this.socket.addEventListener('open', () => {\n // The relay holds the host's first bytes until the browser says it is\n // listening, exactly as the RDP client does.\n this.socket.send(GATEWAY_READY_SIGNAL);\n this.#write(encodeUtf8(`${CLIENT_VERSION}\\r\\n`));\n this.keepaliveTimer = setInterval(() => {\n if (this.socket?.readyState !== WebSocket.OPEN) return;\n\n // Keeps the browser-to-gateway WebSocket alive. The gateway absorbs\n // this frame, so it never reaches the host.\n this.socket.send(GATEWAY_KEEPALIVE_SIGNAL);\n\n // Which leaves the TCP leg silent whenever nobody is typing, and an\n // idle TCP connection through a tunnel is a TCP connection about to be\n // dropped. This is the request OpenSSH itself uses to keep one warm:\n // servers answer it with a failure, which is a perfectly good answer --\n // the point is the bytes, not the reply.\n if (this.sessionId) {\n try {\n this.#sendPacket(new SshWriter(64)\n .u8(MSG.GLOBAL_REQUEST)\n .string('keepalive@openssh.com')\n .u8(1)\n .take());\n } catch {\n // A keepalive is not worth breaking a working session over.\n }\n }\n }, KEEPALIVE_INTERVAL_MS);\n });\n\n this.socket.addEventListener('message', (event) => {\n if (typeof event.data === 'string') return; // gateway control frame\n this.#receive(new Uint8Array(event.data));\n });\n\n this.socket.addEventListener('close', (event) => {\n this.#finish({\n clean: event.code === 1000,\n code: event.code,\n message: event.reason || (\n event.code === 1006\n ? 'The gateway rejected or could not reach this SSH target. Confirm that SSH is published for this host.'\n : ''\n ),\n });\n });\n\n // A failed WebSocket upgrade emits `error` immediately followed by\n // `close`. The error event has no HTTP status or response body, while a\n // relay that connected successfully can put the useful TCP failure in the\n // close reason. Waiting for `close` preserves that explanation instead of\n // replacing it with an opaque generic error.\n this.socket.addEventListener('error', () => {\n this.log('The SSH gateway WebSocket reported a transport error; waiting for its close reason.');\n });\n }\n\n disconnect(message = 'Client disconnected') {\n if (this.closed) return;\n try {\n const payload = new SshWriter(64)\n .u8(MSG.DISCONNECT).u32(11).string(message).string('')\n .take();\n this.#sendPacket(payload);\n } catch {}\n // The packet may still be encrypting, and closing the socket under it\n // would drop the goodbye the host is waiting for.\n const socket = this.socket;\n void Promise.resolve(this.encryptChain).finally(() => {\n try { socket?.close(1000, 'Client disconnected'); } catch {}\n });\n this.#finish({ clean: true, code: 1000, message: '' });\n }\n\n #finish(detail) {\n if (this.closed) return;\n this.closed = true;\n clearInterval(this.keepaliveTimer);\n this.keepaliveTimer = null;\n this.dispatchEvent(new CustomEvent('close', { detail }));\n }\n\n fail(error: unknown) {\n const message = error instanceof Error ? error.message : String(error);\n if (this.closed) return;\n this.dispatchEvent(new CustomEvent('error', { detail: { message } }));\n this.#finish({ clean: false, code: 4000, message });\n try { this.socket?.close(4000, message.slice(0, 100)); } catch {}\n }\n\n #write(bytes: Uint8Array) {\n if (this.socket?.readyState !== WebSocket.OPEN) return;\n this.bytesSent += bytes.length;\n this.socket.send(bytes);\n }\n\n // --- framing -------------------------------------------------------------\n\n #receive(chunk: Uint8Array) {\n this.bytesReceived += chunk.length;\n this.buffer = this.buffer.length ? concatBytes(this.buffer, chunk) : chunk;\n\n // Decryption is asynchronous, so the reader is a single chain rather than\n // a loop: two overlapping drains could deliver packets out of order, and\n // an out-of-order packet is a broken session.\n this.drainChain = (this.drainChain || Promise.resolve())\n .then(() => this.#drain())\n .catch((error: unknown) => this.fail(error));\n }\n\n async #drain() {\n if (this.closed) return;\n if (!this.serverVersion && !this.#readVersion()) return;\n\n while (!this.closed) {\n // The host's NEWKEYS is the last packet under the old cipher. Nothing\n // after it can be read until the new one is installed.\n if (this.newKeysBarrier) {\n const barrier = this.newKeysBarrier;\n this.newKeysBarrier = null;\n await barrier;\n continue;\n }\n\n if (!this.incoming) {\n const payload = this.#readPlainPacket();\n if (!payload) return;\n this.#dispatchPacket(payload);\n continue;\n }\n\n const frame = this.#takeEncryptedFrame();\n if (!frame) return;\n this.#dispatchPacket(await this.#decrypt(frame));\n }\n }\n\n /**\n * The host may print any number of lines before its version line, and RFC\n * 4253 says to show them: this is where a \"banned by fail2ban\" notice or a\n * legal banner arrives.\n */\n #readVersion() {\n while (true) {\n const newline = this.buffer.indexOf(0x0a);\n if (newline === -1) {\n if (this.buffer.length > VERSION_LINE_LIMIT) {\n throw new Error('SSH: the host sent no version line.');\n }\n return false;\n }\n\n let end = newline;\n if (end > 0 && this.buffer[end - 1] === 0x0d) end--;\n const line = new TextDecoder().decode(this.buffer.subarray(0, end));\n this.buffer = this.buffer.subarray(newline + 1);\n\n if (!line.startsWith('SSH-')) {\n if (line.trim()) this.greeting.push(line);\n continue;\n }\n if (!line.startsWith('SSH-2.0-') && !line.startsWith('SSH-1.99-')) {\n throw new Error(`SSH: this host speaks ${line.split('-').slice(0, 2).join('-')}, which is too old.`);\n }\n\n this.serverVersion = line;\n this.log('version', { server: line, greeting: this.greeting });\n this.dispatchEvent(new CustomEvent('greeting', { detail: { lines: this.greeting.slice() } }));\n void this.#startKex();\n return true;\n }\n }\n\n /** A packet from before the keys exist. Returns null when more bytes are due. */\n #readPlainPacket() {\n if (this.buffer.length < 5) return null;\n const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);\n const packetLength = view.getUint32(0, false);\n if (packetLength < 8 || packetLength > MAX_PACKET_BYTES) {\n throw new Error('SSH: the host sent a packet of an impossible size.');\n }\n if (this.buffer.length < 4 + packetLength) return null;\n\n const paddingLength = this.buffer[4];\n if (paddingLength + 1 > packetLength) throw new Error('SSH: a packet claimed more padding than it had.');\n const payload = this.buffer.slice(5, 4 + packetLength - paddingLength);\n this.buffer = this.buffer.subarray(4 + packetLength);\n this.receiveSequence = (this.receiveSequence + 1) >>> 0;\n return payload;\n }\n\n /**\n * With AES-GCM the length is authenticated but not encrypted, so the frame\n * can be measured before anything is decrypted (RFC 5647).\n */\n #takeEncryptedFrame() {\n if (this.buffer.length < 4) return null;\n const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);\n const packetLength = view.getUint32(0, false);\n if (packetLength < GCM_BLOCK || packetLength % GCM_BLOCK !== 0 || packetLength > MAX_PACKET_BYTES) {\n throw new Error('SSH: the host sent a packet of an impossible size.');\n }\n\n const total = 4 + packetLength + GCM_TAG_BYTES;\n if (this.buffer.length < total) return null;\n\n const frame = this.buffer.slice(0, total);\n this.buffer = this.buffer.subarray(total);\n return frame;\n }\n\n async #decrypt(frame: Uint8Array) {\n let plain;\n try {\n plain = new Uint8Array(await subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: this.incoming.nextIv(),\n additionalData: frame.subarray(0, 4),\n tagLength: GCM_TAG_BYTES * 8,\n },\n this.incoming.key,\n frame.subarray(4)\n ));\n } catch {\n throw new Error('SSH: a packet failed its integrity check, so the session was dropped.');\n }\n\n const paddingLength = plain[0];\n if (paddingLength + 1 > plain.length) throw new Error('SSH: a packet claimed more padding than it had.');\n this.receiveSequence = (this.receiveSequence + 1) >>> 0;\n return plain.slice(1, plain.length - paddingLength);\n }\n\n #dispatchPacket(payload: Uint8Array) {\n if (payload.length === 0) return;\n const type = payload[0];\n\n switch (type) {\n case MSG.DISCONNECT: {\n const reader = new SshReader(payload, 1);\n const code = reader.u32();\n const description = reader.string();\n const reason = description || DISCONNECT_REASON[code] || `reason ${code}`;\n this.dispatchEvent(new CustomEvent('hostdisconnect', { detail: { code, reason } }));\n this.#finish({ clean: code === 11, code: 1000, message: reason });\n try { this.socket?.close(1000, 'Host disconnected'); } catch {}\n return;\n }\n case MSG.IGNORE:\n case MSG.UNIMPLEMENTED:\n case MSG.EXT_INFO:\n return;\n case MSG.DEBUG: {\n const reader = new SshReader(payload, 1);\n const alwaysDisplay = reader.boolean();\n const message = reader.string();\n this.log('debug', { message, alwaysDisplay });\n if (alwaysDisplay) {\n this.dispatchEvent(new CustomEvent('banner', { detail: { text: `${message}\\r\\n` } }));\n }\n return;\n }\n case MSG.KEXINIT:\n void this.#onKexInit(payload);\n return;\n case MSG.NEWKEYS:\n this.#onNewKeys();\n return;\n case MSG.GLOBAL_REQUEST: {\n const reader = new SshReader(payload, 1);\n reader.string();\n if (reader.boolean()) this.#sendPacket(new Uint8Array([MSG.REQUEST_FAILURE]));\n return;\n }\n // The answer to the keepalive global request. Either one means the host\n // is still there, which is the whole point of having asked.\n case MSG.REQUEST_SUCCESS:\n case MSG.REQUEST_FAILURE:\n return;\n default:\n break;\n }\n\n // A key exchange packet can only be answered by the exchange that is\n // running, and everything else belongs to the layers above.\n if (this.kex && type >= 30 && type <= 49) {\n this.#deliverKexPacket(payload);\n return;\n }\n this.dispatchEvent(new CustomEvent('packet', { detail: { type, payload } }));\n }\n\n // --- sending -------------------------------------------------------------\n\n /** Application packets wait while a key exchange is in flight (RFC 4253 7.1). */\n send(payload: Uint8Array) {\n if (this.closed) return false;\n if (this.kex?.running) {\n this.kexQueue.push(payload);\n return true;\n }\n this.#sendPacket(payload);\n return true;\n }\n\n #sendPacket(payload: Uint8Array) {\n if (this.closed || this.socket?.readyState !== WebSocket.OPEN) return;\n if (!this.outgoing) {\n this.#write(framePlain(payload));\n this.sendSequence = (this.sendSequence + 1) >>> 0;\n return;\n }\n void this.#encrypt(payload);\n }\n\n async #encrypt(payload: Uint8Array) {\n // Packets must reach the host in the order they were built, and encryption\n // is asynchronous, so each one waits for the one before it.\n const previous = this.encryptChain || Promise.resolve();\n this.encryptChain = previous.then(async () => {\n if (this.closed || !this.outgoing) return;\n\n const paddingLength = gcmPaddingLength(payload.length);\n const plain = new Uint8Array(1 + payload.length + paddingLength);\n plain[0] = paddingLength;\n plain.set(payload, 1);\n crypto.getRandomValues(plain.subarray(1 + payload.length));\n\n const header = new SshWriter(4).u32(plain.length).take();\n const sealed = new Uint8Array(await subtle.encrypt(\n { name: 'AES-GCM', iv: this.outgoing.nextIv(), additionalData: header, tagLength: GCM_TAG_BYTES * 8 },\n this.outgoing.key,\n plain\n ));\n\n this.#write(concatBytes(header, sealed));\n this.sendSequence = (this.sendSequence + 1) >>> 0;\n }).catch((error: unknown) => this.fail(error));\n\n return this.encryptChain;\n }\n\n // --- key exchange --------------------------------------------------------\n\n async #startKex() {\n if (this.kex?.running) return;\n\n // Claimed before the first await: the host's own KEXINIT is usually already\n // on the wire, and two exchanges starting at once would send two of ours.\n this.kex = { running: true, clientKexInit: null, pending: null, inbox: [] };\n\n try {\n const offer = await negotiableAlgorithms();\n const clientKexInit = buildKexInit(offer);\n this.kex.clientKexInit = clientKexInit;\n this.#sendPacket(clientKexInit);\n\n const serverKexInit = this.pendingServerKexInit || await this.#nextKexPacket(MSG.KEXINIT);\n this.pendingServerKexInit = null;\n await this.#runKex(offer, clientKexInit, serverKexInit);\n } catch (error) {\n this.fail(error);\n }\n }\n\n /** The host may ask for new keys at any point in a session. */\n async #onKexInit(payload: Uint8Array) {\n if (this.kex?.running) {\n this.#deliverKexPacket(payload);\n return;\n }\n\n this.log('rekey', { reason: 'the host asked for new keys' });\n this.pendingServerKexInit = payload;\n await this.#startKex();\n }\n\n /**\n * Key exchange packets can arrive before the step that wants them: the host\n * sends its reply and NEWKEYS back to back, while this side is still\n * deriving a secret or waiting for the user to accept a host key. They are\n * queued rather than dropped.\n */\n #deliverKexPacket(payload: Uint8Array) {\n if (!this.kex) return;\n if (this.kex.pending) {\n const pending = this.kex.pending;\n this.kex.pending = null;\n pending(payload);\n return;\n }\n this.kex.inbox.push(payload);\n }\n\n #nextKexPacket(expectedType) {\n return new Promise((resolve, reject) => {\n const check = (payload: Uint8Array) => {\n if (expectedType !== undefined && payload[0] !== expectedType) {\n reject(new Error(`SSH: expected message ${expectedType} during key exchange but got ${payload[0]}.`));\n return;\n }\n resolve(payload);\n };\n\n const queued = this.kex.inbox.shift();\n if (queued) {\n check(queued);\n return;\n }\n this.kex.pending = (payload: Uint8Array) => {\n this.kex.pending = null;\n check(payload);\n };\n });\n }\n\n async #runKex(offer, clientKexInit: Uint8Array, serverKexInit: Uint8Array) {\n const server = parseKexInit(serverKexInit);\n const kexAlgorithm = negotiate(offer.kex, server.kex);\n const hostKeyAlgorithm = negotiate(offer.hostKey, server.hostKey);\n const cipherToServer = negotiate(CIPHER_NAMES, server.cipherClientToServer);\n const cipherToClient = negotiate(CIPHER_NAMES, server.cipherServerToClient);\n\n if (!kexAlgorithm) throw new Error(`SSH: no shared key exchange method. The host offers ${server.kex.join(', ')}.`);\n if (!hostKeyAlgorithm) throw new Error(`SSH: no host key type this browser can verify. The host offers ${server.hostKey.join(', ')}.`);\n if (!cipherToServer || !cipherToClient) {\n throw new Error(`SSH: the host does not offer AES-GCM, which is the only cipher this client can use. It offers ${server.cipherServerToClient.join(', ')}.`);\n }\n\n this.log('negotiated', {\n kex: kexAlgorithm,\n hostKey: hostKeyAlgorithm,\n cipher: cipherToClient,\n rekey: Boolean(this.sessionId),\n });\n\n const client = await createKexClient(kexAlgorithm);\n this.#sendPacket(new SshWriter(128).u8(MSG.KEX_ECDH_INIT).string(client.publicKey).take());\n\n const reply = new SshReader(await this.#nextKexPacket(MSG.KEX_ECDH_REPLY), 1);\n const hostKeyBlob = reply.stringBytes();\n const serverPublicKey = reply.stringBytes();\n const signatureBlob = reply.stringBytes();\n\n const sharedSecret = await client.sharedSecret(serverPublicKey);\n const hash = await exchangeHash({\n hash: client.hash,\n clientVersion: CLIENT_VERSION,\n serverVersion: this.serverVersion,\n clientKexInit,\n serverKexInit,\n hostKeyBlob,\n clientPublicKey: client.publicKey,\n serverPublicKey,\n sharedSecret,\n });\n\n const signatureValid = await verifyHostKeySignature({\n algorithm: hostKeyAlgorithm,\n keyBlob: hostKeyBlob,\n signatureBlob,\n exchangeHash: hash,\n });\n if (!signatureValid) {\n throw new Error('SSH: the host key signature is invalid, so this is not the machine it claims to be.');\n }\n\n const fingerprint = await hostKeyFingerprint(hostKeyBlob);\n if (!this.sessionId) {\n // Trust is decided once per connection, against the key that has just\n // proved it owns this exchange.\n const accepted = await this.verifyHost({\n fingerprint,\n keyType: hostKeyType(hostKeyBlob),\n algorithm: hostKeyAlgorithm,\n });\n if (!accepted) throw new Error('SSH: the host key was not accepted, so the connection was stopped.');\n this.sessionId = hash;\n }\n\n this.#sendPacket(new Uint8Array([MSG.NEWKEYS]));\n await this.#nextKexPacket(MSG.NEWKEYS);\n\n const parameters = {\n hash: client.hash,\n sharedSecret,\n exchangeHash: hash,\n sessionId: this.sessionId,\n };\n const [ivToServer, ivToClient, keyToServer, keyToClient] = await Promise.all([\n deriveKey({ ...parameters, letter: 'A', length: GCM_IV_BYTES }),\n deriveKey({ ...parameters, letter: 'B', length: GCM_IV_BYTES }),\n deriveKey({ ...parameters, letter: 'C', length: CIPHERS[cipherToServer].keyLength }),\n deriveKey({ ...parameters, letter: 'D', length: CIPHERS[cipherToClient].keyLength }),\n ]);\n\n this.outgoing = await GcmCipher.create(keyToServer, ivToServer);\n this.incoming = await GcmCipher.create(keyToClient, ivToClient);\n this.kex.running = false;\n\n // Both ciphers exist now, so the reader may go on.\n this.releaseNewKeys?.();\n this.releaseNewKeys = null;\n\n const queued = this.kexQueue;\n this.kexQueue = [];\n for (const payload of queued) this.#sendPacket(payload);\n\n const firstTime = !this.established;\n this.established = true;\n this.dispatchEvent(new CustomEvent('keys', {\n detail: { fingerprint, kexAlgorithm, hostKeyAlgorithm, cipher: cipherToClient, firstTime },\n }));\n }\n\n #onNewKeys() {\n if (!this.kex) return;\n\n // Hold the reader here: the host's very next packet is already encrypted\n // with keys this side has not derived yet.\n this.newKeysBarrier = new Promise((resolve) => { this.releaseNewKeys = resolve; });\n this.#deliverKexPacket(new Uint8Array([MSG.NEWKEYS]));\n }\n}\n\n/** Padding for a GCM packet: only the encrypted part is block aligned. */\nexport function gcmPaddingLength(payloadLength) {\n const unpadded = 1 + payloadLength;\n let padding = GCM_BLOCK - (unpadded % GCM_BLOCK);\n while (padding < MIN_PADDING) padding += GCM_BLOCK;\n return padding;\n}\n\n/** A packet before any keys exist: the whole thing is block aligned. */\nexport function framePlain(payload: Uint8Array) {\n const unpadded = 5 + payload.length;\n let padding = PLAIN_BLOCK - (unpadded % PLAIN_BLOCK);\n if (padding < MIN_PADDING) padding += PLAIN_BLOCK;\n\n const writer = new SshWriter(unpadded + padding);\n writer.u32(1 + payload.length + padding);\n writer.u8(padding);\n writer.raw(payload);\n const frame = writer.take();\n const filler = new Uint8Array(padding);\n crypto.getRandomValues(filler);\n return concatBytes(frame, filler);\n}\n\nexport function buildKexInit(offer) {\n const cookie = new Uint8Array(16);\n crypto.getRandomValues(cookie);\n\n const writer = new SshWriter(512);\n writer.u8(MSG.KEXINIT).raw(cookie);\n writer.nameList(offer.kex);\n writer.nameList(offer.hostKey);\n writer.nameList(CIPHER_NAMES); // client to server\n writer.nameList(CIPHER_NAMES); // server to client\n writer.nameList(MAC_NAMES);\n writer.nameList(MAC_NAMES);\n writer.nameList(COMPRESSION_NAMES);\n writer.nameList(COMPRESSION_NAMES);\n writer.nameList([]); // languages\n writer.nameList([]);\n writer.boolean(false); // no guessed key exchange packet\n writer.u32(0); // reserved\n return writer.take();\n}\n\nexport function parseKexInit(payload: Uint8Array) {\n const reader = new SshReader(payload, 17); // message number and cookie\n return {\n kex: reader.nameList(),\n hostKey: reader.nameList(),\n cipherClientToServer: reader.nameList(),\n cipherServerToClient: reader.nameList(),\n macClientToServer: reader.nameList(),\n macServerToClient: reader.nameList(),\n compressionClientToServer: reader.nameList(),\n compressionServerToClient: reader.nameList(),\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
};
|
|
189
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/protocols/ssh/wire.ts"],
|
|
4
|
+
"sourcesContent": ["// SSH wire types (RFC 4251 section 5).\n//\n// Everything in SSH is big endian, so unlike the RDP reader and writer the byte\n// order is not part of each call. The four types that matter here are the\n// 32-bit length-prefixed string, the multiple-precision integer, the name-list\n// and the byte.\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\nexport function encodeUtf8(text: string): Uint8Array {\n return textEncoder.encode(text);\n}\n\nexport function decodeUtf8(bytes: Uint8Array): string {\n return textDecoder.decode(bytes);\n}\n\nexport function concatBytes(...parts: readonly Uint8Array[]): Uint8Array {\n const total = parts.reduce((sum, part) => sum + part.length, 0);\n const out = new Uint8Array(total);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\n/** Constant-time-ish equality. Used for authentication tags and fingerprints. */\nexport function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n let difference = 0;\n for (let index = 0; index < a.length; index++) difference |= (a[index] ?? 0) ^ (b[index] ?? 0);\n return difference === 0;\n}\n\nexport function toBase64(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nexport function fromBase64(text: string): Uint8Array {\n const binary = atob(text);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\n/** Base64url without padding, which is what a JSON Web Key wants. */\nexport function toBase64Url(bytes: Uint8Array): string {\n return toBase64(bytes).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\nexport class SshWriter {\n private bytes: Uint8Array<ArrayBuffer>;\n private view: DataView;\n private offset = 0;\n\n constructor(capacity = 256) {\n this.bytes = new Uint8Array(new ArrayBuffer(capacity));\n this.view = new DataView(this.bytes.buffer);\n }\n\n #ensure(extra: number): void {\n if (this.offset + extra <= this.bytes.length) return;\n let capacity = Math.max(this.bytes.length * 2, 64);\n while (capacity < this.offset + extra) capacity *= 2;\n const grown = new Uint8Array(new ArrayBuffer(capacity));\n grown.set(this.bytes.subarray(0, this.offset));\n this.bytes = grown;\n this.view = new DataView(grown.buffer);\n }\n\n u8(value: number): this {\n this.#ensure(1);\n this.view.setUint8(this.offset, value);\n this.offset += 1;\n return this;\n }\n\n boolean(value: boolean): this {\n return this.u8(value ? 1 : 0);\n }\n\n u32(value: number): this {\n this.#ensure(4);\n this.view.setUint32(this.offset, value >>> 0, false);\n this.offset += 4;\n return this;\n }\n\n raw(bytes: Uint8Array): this {\n this.#ensure(bytes.length);\n this.bytes.set(bytes, this.offset);\n this.offset += bytes.length;\n return this;\n }\n\n /** A length-prefixed string. Text is encoded as UTF-8. */\n string(value: string | Uint8Array): this {\n const bytes = typeof value === 'string' ? encodeUtf8(value) : value;\n return this.u32(bytes.length).raw(bytes);\n }\n\n nameList(names: readonly string[]): this {\n return this.string(names.join(','));\n }\n\n /**\n * An mpint: two's complement, big endian, with no leading zero bytes except\n * the one that keeps a positive number from looking negative.\n */\n mpint(bytes: Uint8Array): this {\n let start = 0;\n while (start < bytes.length && bytes[start] === 0) start++;\n const magnitude = bytes.subarray(start);\n if (magnitude.length === 0) return this.u32(0);\n if ((magnitude[0] ?? 0) & 0x80) {\n this.u32(magnitude.length + 1).u8(0);\n return this.raw(magnitude);\n }\n return this.u32(magnitude.length).raw(magnitude);\n }\n\n get length(): number {\n return this.offset;\n }\n\n take(): Uint8Array {\n return this.bytes.slice(0, this.offset);\n }\n}\n\nexport class SshReader {\n private readonly bytes: Uint8Array;\n private readonly view: DataView;\n offset: number;\n\n constructor(bytes: Uint8Array, offset = 0) {\n this.bytes = bytes;\n this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n this.offset = offset;\n }\n\n get remaining(): number {\n return this.bytes.length - this.offset;\n }\n\n #need(count: number): void {\n if (count < 0 || this.remaining < count) {\n throw new Error(`SSH: truncated packet (needed ${count} bytes, ${this.remaining} left)`);\n }\n }\n\n u8(): number {\n this.#need(1);\n return this.view.getUint8(this.offset++);\n }\n\n boolean(): boolean {\n return this.u8() !== 0;\n }\n\n u32(): number {\n this.#need(4);\n const value = this.view.getUint32(this.offset, false);\n this.offset += 4;\n return value;\n }\n\n raw(count: number): Uint8Array {\n this.#need(count);\n const slice = this.bytes.subarray(this.offset, this.offset + count);\n this.offset += count;\n return slice;\n }\n\n /** The bytes of a length-prefixed string. */\n stringBytes(): Uint8Array {\n return this.raw(this.u32());\n }\n\n string(): string {\n return decodeUtf8(this.stringBytes());\n }\n\n nameList(): string[] {\n const value = this.string();\n return value ? value.split(',') : [];\n }\n\n /** An mpint's magnitude, with the sign byte removed. */\n mpint(): Uint8Array {\n const bytes = this.stringBytes();\n let start = 0;\n while (start < bytes.length && bytes[start] === 0) start++;\n return bytes.subarray(start);\n }\n\n skip(count: number): this {\n this.#need(count);\n this.offset += count;\n return this;\n }\n\n rest(): Uint8Array {\n return this.bytes.subarray(this.offset);\n }\n}\n\n/** Pad a magnitude out to a fixed width, which raw curve points need. */\nexport function padStart(bytes: Uint8Array, width: number): Uint8Array {\n if (bytes.length === width) return bytes;\n if (bytes.length > width) return bytes.subarray(bytes.length - width);\n const out = new Uint8Array(width);\n out.set(bytes, width - bytes.length);\n return out;\n}\n"],
|
|
5
|
+
"mappings": "AAOA,MAAM,cAAc,IAAI,YAAY;AACpC,MAAM,cAAc,IAAI,YAAY;AAE7B,SAAS,WAAW,MAA0B;AACnD,SAAO,YAAY,OAAO,IAAI;AAChC;AAEO,SAAS,WAAW,OAA2B;AACpD,SAAO,YAAY,OAAO,KAAK;AACjC;AAEO,SAAS,eAAe,OAA0C;AACvE,QAAM,QAAQ,MAAM,OAAO,CAAC,KAAK,SAAS,MAAM,KAAK,QAAQ,CAAC;AAC9D,QAAM,MAAM,IAAI,WAAW,KAAK;AAChC,MAAI,SAAS;AACb,aAAW,QAAQ,OAAO;AACxB,QAAI,IAAI,MAAM,MAAM;AACpB,cAAU,KAAK;AAAA,EACjB;AACA,SAAO;AACT;AAGO,SAAS,WAAW,GAAe,GAAwB;AAChE,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,aAAa;AACjB,WAAS,QAAQ,GAAG,QAAQ,EAAE,QAAQ,QAAS,gBAAe,EAAE,KAAK,KAAK,MAAM,EAAE,KAAK,KAAK;AAC5F,SAAO,eAAe;AACxB;AAEO,SAAS,SAAS,OAA2B;AAClD,MAAI,SAAS;AACb,aAAW,QAAQ,MAAO,WAAU,OAAO,aAAa,IAAI;AAC5D,SAAO,KAAK,MAAM;AACpB;AAEO,SAAS,WAAW,MAA0B;AACnD,QAAM,SAAS,KAAK,IAAI;AACxB,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,QAAQ,GAAG,QAAQ,OAAO,QAAQ,QAAS,OAAM,KAAK,IAAI,OAAO,WAAW,KAAK;AAC1F,SAAO;AACT;AAGO,SAAS,YAAY,OAA2B;AACrD,SAAO,SAAS,KAAK,EAAE,WAAW,KAAK,GAAG,EAAE,WAAW,KAAK,GAAG,EAAE,WAAW,KAAK,EAAE;AACrF;AAEO,MAAM,UAAU;AAAA,EACb;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EAEjB,YAAY,WAAW,KAAK;AAC1B,SAAK,QAAQ,IAAI,WAAW,IAAI,YAAY,QAAQ,CAAC;AACrD,SAAK,OAAO,IAAI,SAAS,KAAK,MAAM,MAAM;AAAA,EAC5C;AAAA,EAEA,QAAQ,OAAqB;AAC3B,QAAI,KAAK,SAAS,SAAS,KAAK,MAAM,OAAQ;AAC9C,QAAI,WAAW,KAAK,IAAI,KAAK,MAAM,SAAS,GAAG,EAAE;AACjD,WAAO,WAAW,KAAK,SAAS,MAAO,aAAY;AACnD,UAAM,QAAQ,IAAI,WAAW,IAAI,YAAY,QAAQ,CAAC;AACtD,UAAM,IAAI,KAAK,MAAM,SAAS,GAAG,KAAK,MAAM,CAAC;AAC7C,SAAK,QAAQ;AACb,SAAK,OAAO,IAAI,SAAS,MAAM,MAAM;AAAA,EACvC;AAAA,EAEA,GAAG,OAAqB;AACtB,SAAK,QAAQ,CAAC;AACd,SAAK,KAAK,SAAS,KAAK,QAAQ,KAAK;AACrC,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,QAAQ,OAAsB;AAC5B,WAAO,KAAK,GAAG,QAAQ,IAAI,CAAC;AAAA,EAC9B;AAAA,EAEA,IAAI,OAAqB;AACvB,SAAK,QAAQ,CAAC;AACd,SAAK,KAAK,UAAU,KAAK,QAAQ,UAAU,GAAG,KAAK;AACnD,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,OAAyB;AAC3B,SAAK,QAAQ,MAAM,MAAM;AACzB,SAAK,MAAM,IAAI,OAAO,KAAK,MAAM;AACjC,SAAK,UAAU,MAAM;AACrB,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,OAAkC;AACvC,UAAM,QAAQ,OAAO,UAAU,WAAW,WAAW,KAAK,IAAI;AAC9D,WAAO,KAAK,IAAI,MAAM,MAAM,EAAE,IAAI,KAAK;AAAA,EACzC;AAAA,EAEA,SAAS,OAAgC;AACvC,WAAO,KAAK,OAAO,MAAM,KAAK,GAAG,CAAC;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAyB;AAC7B,QAAI,QAAQ;AACZ,WAAO,QAAQ,MAAM,UAAU,MAAM,KAAK,MAAM,EAAG;AACnD,UAAM,YAAY,MAAM,SAAS,KAAK;AACtC,QAAI,UAAU,WAAW,EAAG,QAAO,KAAK,IAAI,CAAC;AAC7C,SAAK,UAAU,CAAC,KAAK,KAAK,KAAM;AAC9B,WAAK,IAAI,UAAU,SAAS,CAAC,EAAE,GAAG,CAAC;AACnC,aAAO,KAAK,IAAI,SAAS;AAAA,IAC3B;AACA,WAAO,KAAK,IAAI,UAAU,MAAM,EAAE,IAAI,SAAS;AAAA,EACjD;AAAA,EAEA,IAAI,SAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,OAAmB;AACjB,WAAO,KAAK,MAAM,MAAM,GAAG,KAAK,MAAM;AAAA,EACxC;AACF;AAEO,MAAM,UAAU;AAAA,EACJ;AAAA,EACA;AAAA,EACjB;AAAA,EAEA,YAAY,OAAmB,SAAS,GAAG;AACzC,SAAK,QAAQ;AACb,SAAK,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;AACzE,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM,SAAS,KAAK;AAAA,EAClC;AAAA,EAEA,MAAM,OAAqB;AACzB,QAAI,QAAQ,KAAK,KAAK,YAAY,OAAO;AACvC,YAAM,IAAI,MAAM,iCAAiC,KAAK,WAAW,KAAK,SAAS,QAAQ;AAAA,IACzF;AAAA,EACF;AAAA,EAEA,KAAa;AACX,SAAK,MAAM,CAAC;AACZ,WAAO,KAAK,KAAK,SAAS,KAAK,QAAQ;AAAA,EACzC;AAAA,EAEA,UAAmB;AACjB,WAAO,KAAK,GAAG,MAAM;AAAA,EACvB;AAAA,EAEA,MAAc;AACZ,SAAK,MAAM,CAAC;AACZ,UAAM,QAAQ,KAAK,KAAK,UAAU,KAAK,QAAQ,KAAK;AACpD,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,OAA2B;AAC7B,SAAK,MAAM,KAAK;AAChB,UAAM,QAAQ,KAAK,MAAM,SAAS,KAAK,QAAQ,KAAK,SAAS,KAAK;AAClE,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,cAA0B;AACxB,WAAO,KAAK,IAAI,KAAK,IAAI,CAAC;AAAA,EAC5B;AAAA,EAEA,SAAiB;AACf,WAAO,WAAW,KAAK,YAAY,CAAC;AAAA,EACtC;AAAA,EAEA,WAAqB;AACnB,UAAM,QAAQ,KAAK,OAAO;AAC1B,WAAO,QAAQ,MAAM,MAAM,GAAG,IAAI,CAAC;AAAA,EACrC;AAAA;AAAA,EAGA,QAAoB;AAClB,UAAM,QAAQ,KAAK,YAAY;AAC/B,QAAI,QAAQ;AACZ,WAAO,QAAQ,MAAM,UAAU,MAAM,KAAK,MAAM,EAAG;AACnD,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B;AAAA,EAEA,KAAK,OAAqB;AACxB,SAAK,MAAM,KAAK;AAChB,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AAAA,EAEA,OAAmB;AACjB,WAAO,KAAK,MAAM,SAAS,KAAK,MAAM;AAAA,EACxC;AACF;AAGO,SAAS,SAAS,OAAmB,OAA2B;AACrE,MAAI,MAAM,WAAW,MAAO,QAAO;AACnC,MAAI,MAAM,SAAS,MAAO,QAAO,MAAM,SAAS,MAAM,SAAS,KAAK;AACpE,QAAM,MAAM,IAAI,WAAW,KAAK;AAChC,MAAI,IAAI,OAAO,QAAQ,MAAM,MAAM;AACnC,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
};
|
|
16
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/protocols/vnc/input.ts"],
|
|
4
|
+
"sourcesContent": ["/** RFB wheel buttons for a bounded two-axis scroll, in wire order. */\nexport function vncWheelMasks(delta: { x: number; y: number }): number[] {\n const masks: number[] = [];\n const wheels = [\n { amount: delta.y, negativeMask: 1 << 3, positiveMask: 1 << 4 },\n { amount: delta.x, negativeMask: 1 << 5, positiveMask: 1 << 6 },\n ];\n for (const { amount, negativeMask, positiveMask } of wheels) {\n const mask = amount < 0 ? negativeMask : positiveMask;\n for (let step = 0; step < Math.min(20, Math.abs(amount)); step++) masks.push(mask);\n }\n return masks;\n}\n"],
|
|
5
|
+
"mappings": "AACO,SAAS,cAAc,OAA2C;AACvE,QAAM,QAAkB,CAAC;AACzB,QAAM,SAAS;AAAA,IACb,EAAE,QAAQ,MAAM,GAAG,cAAc,KAAK,GAAG,cAAc,KAAK,EAAE;AAAA,IAC9D,EAAE,QAAQ,MAAM,GAAG,cAAc,KAAK,GAAG,cAAc,KAAK,EAAE;AAAA,EAChE;AACA,aAAW,EAAE,QAAQ,cAAc,aAAa,KAAK,QAAQ;AAC3D,UAAM,OAAO,SAAS,IAAI,eAAe;AACzC,aAAS,OAAO,GAAG,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,MAAM,CAAC,GAAG,OAAQ,OAAM,KAAK,IAAI;AAAA,EACnF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import RFB from "@novnc/novnc";
|
|
2
|
+
import { DEFAULT_PORTS } from "../../shared/protocol";
|
|
3
|
+
import {
|
|
4
|
+
GATEWAY_KEEPALIVE_SIGNAL,
|
|
5
|
+
GATEWAY_READY_SIGNAL,
|
|
6
|
+
KEEPALIVE_INTERVAL_MS
|
|
7
|
+
} from "../../shared/signals";
|
|
8
|
+
import { vncWheelMasks } from "./input";
|
|
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
|
+
};
|
|
259
|
+
//# sourceMappingURL=session.js.map
|