diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index f9c5f0709e..d3f6c53a37 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -12,15 +12,13 @@ "@bitwarden/common": "file:../../../libs/common", "@bitwarden/node": "file:../../../libs/node", "module-alias": "2.2.3", - "node-ipc": "9.2.1", "ts-node": "10.9.2", "uuid": "11.0.5", "yargs": "17.7.2" }, "devDependencies": { "@types/node": "22.10.7", - "@types/node-ipc": "9.2.3", - "typescript": "4.7.4" + "typescript": "5.4.2" } }, "../../../libs/common": { @@ -31,10 +29,7 @@ "../../../libs/node": { "name": "@bitwarden/node", "version": "0.0.0", - "license": "GPL-3.0", - "dependencies": { - "@bitwarden/common": "file:../common" - } + "license": "GPL-3.0" }, "node_modules/@bitwarden/common": { "resolved": "../../../libs/common", @@ -114,16 +109,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/node-ipc": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", - "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -225,15 +210,6 @@ "node": ">=0.3.1" } }, - "node_modules/easy-stack": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", - "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -249,15 +225,6 @@ "node": ">=6" } }, - "node_modules/event-pubsub": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", - "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", - "license": "Unlicense", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -276,27 +243,6 @@ "node": ">=8" } }, - "node_modules/js-message": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", - "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", - "license": "MIT", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/js-queue": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", - "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", - "license": "MIT", - "dependencies": { - "easy-stack": "^1.0.1" - }, - "engines": { - "node": ">=1.0.0" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -309,20 +255,6 @@ "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "license": "MIT" }, - "node_modules/node-ipc": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", - "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", - "license": "MIT", - "dependencies": { - "event-pubsub": "4.3.0", - "js-message": "1.0.7", - "js-queue": "2.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -402,16 +334,15 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "license": "Apache-2.0", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/undici-types": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 977b93e70d..0df272c142 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -17,15 +17,13 @@ "@bitwarden/common": "file:../../../libs/common", "@bitwarden/node": "file:../../../libs/node", "module-alias": "2.2.3", - "node-ipc": "9.2.1", "ts-node": "10.9.2", "uuid": "11.0.5", "yargs": "17.7.2" }, "devDependencies": { "@types/node": "22.10.7", - "@types/node-ipc": "9.2.3", - "typescript": "4.7.4" + "typescript": "5.4.2" }, "_moduleAliases": { "@bitwarden/common": "dist/libs/common/src", diff --git a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts index 68c7ac73ab..b02ff1a422 100644 --- a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts @@ -1,9 +1,7 @@ /* eslint-disable no-console */ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { homedir } from "os"; - -import * as NodeIPC from "node-ipc"; +import { ChildProcess, spawn } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; // eslint-disable-next-line no-restricted-imports import { MessageCommon } from "../../src/models/native-messaging/message-common"; @@ -13,11 +11,6 @@ import { UnencryptedMessageResponse } from "../../src/models/native-messaging/un import Deferred from "./deferred"; import { race } from "./race"; -NodeIPC.config.id = "native-messaging-test-runner"; -NodeIPC.config.maxRetries = 0; -NodeIPC.config.silent = true; - -const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`; const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds export type MessageHandler = (MessageCommon) => void; @@ -42,6 +35,10 @@ export default class IPCService { // A set of deferred promises that are awaiting socket connection private awaitingConnection = new Set>(); + // The IPC desktop_proxy process + private process?: ChildProcess; + private processOutputBuffer = Buffer.alloc(0); + constructor( private socketName: string, private messageHandler: MessageHandler, @@ -72,47 +69,47 @@ export default class IPCService { private _connect() { this.connectionState = IPCConnectionState.Connecting; - NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => { - // Process incoming message - this.getSocket().on("message", (message: any) => { - this.processMessage(message); - }); + const proxyPath = selectProxyPath(); + console.log(`[IPCService] connecting to proxy at ${proxyPath}`); - this.getSocket().on("error", (error: Error) => { - // Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be - // invoked multiple times each time a connection error happens - console.log("[IPCService] errored"); - console.log( - "\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m", - ); - this.awaitingConnection.forEach((deferred) => { - console.log(`rejecting: ${deferred}`); - deferred.reject(error); - }); - this.awaitingConnection.clear(); - }); + this.process = spawn(proxyPath, process.argv.slice(1), { + cwd: process.cwd(), + stdio: "pipe", + shell: false, + }); - this.getSocket().on("connect", () => { - console.log("[IPCService] connected"); - this.connectionState = IPCConnectionState.Connected; + this.process.stdout.on("data", (data: Buffer) => { + this.processIpcMessage(data); + }); - this.awaitingConnection.forEach((deferred) => { - deferred.resolve(null); - }); - this.awaitingConnection.clear(); - }); + this.process.stderr.on("data", (data: Buffer) => { + console.error(`proxy log: ${data}`); + }); - this.getSocket().on("disconnect", () => { - console.log("[IPCService] disconnected"); - this.connectionState = IPCConnectionState.Disconnected; + this.process.on("error", (error) => { + // Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be + // invoked multiple times each time a connection error happens + console.log("[IPCService] errored"); + console.log( + "\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m", + ); + this.awaitingConnection.forEach((deferred) => { + console.log(`rejecting: ${deferred}`); + deferred.reject(error); }); + this.awaitingConnection.clear(); + }); + + this.process.on("exit", () => { + console.log("[IPCService] disconnected"); + this.connectionState = IPCConnectionState.Disconnected; }); } disconnect() { console.log("[IPCService] disconnecting..."); if (this.connectionState !== IPCConnectionState.Disconnected) { - NodeIPC.disconnect(this.socketName); + this.process?.kill(); } } @@ -133,7 +130,7 @@ export default class IPCService { this.pendingMessages.set(message.messageId, deferred); - this.getSocket().emit("message", message); + this.sendIpcMessage(message); try { // Since we can not guarantee that a response message will ever be sent, we put a timeout @@ -151,8 +148,56 @@ export default class IPCService { } } - private getSocket() { - return NodeIPC.of[this.socketName]; + // As we're using the desktop_proxy to communicate with the native messaging directly, + // the messages need to follow Native Messaging Host protocol (uint32 size followed by message). + // https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol + private sendIpcMessage(message: MessageCommon) { + const messageStr = JSON.stringify(message); + const buffer = Buffer.alloc(4 + messageStr.length); + buffer.writeUInt32LE(messageStr.length, 0); + buffer.write(messageStr, 4); + + this.process?.stdin.write(buffer); + } + + private processIpcMessage(data: Buffer) { + this.processOutputBuffer = Buffer.concat([this.processOutputBuffer, data]); + + // We might receive more than one IPC message per data event, so we need to process them all + // We continue as long as we have at least 4 + 1 bytes in the buffer, where the first 4 bytes + // represent the message length and the 5th byte is the message + while (this.processOutputBuffer.length > 4) { + // Read the message length and ensure we have the full message + const msgLength = this.processOutputBuffer.readUInt32LE(0); + if (msgLength + 4 < this.processOutputBuffer.length) { + return; + } + + // Parse the message from the buffer + const messageStr = this.processOutputBuffer.subarray(4, msgLength + 4).toString(); + const message = JSON.parse(messageStr); + + // Store the remaining buffer, which is part of the next message + this.processOutputBuffer = this.processOutputBuffer.subarray(msgLength + 4); + + // Process the connect/disconnect messages separately + if (message?.command === "connected") { + console.log("[IPCService] connected"); + this.connectionState = IPCConnectionState.Connected; + + this.awaitingConnection.forEach((deferred) => { + deferred.resolve(null); + }); + this.awaitingConnection.clear(); + continue; + } else if (message?.command === "disconnected") { + console.log("[IPCService] disconnected"); + this.connectionState = IPCConnectionState.Disconnected; + continue; + } + + this.processMessage(message); + } } private processMessage(message: any) { @@ -172,3 +217,41 @@ export default class IPCService { } } } + +function selectProxyPath(): string { + const proxyExtension = process.platform === "win32" ? ".exe" : ""; + + // If the PROXY_PATH environment variable is set, use that + if (process.env.PROXY_PATH) { + if (!fs.existsSync(process.env.PROXY_PATH)) { + throw new Error(`PROXY_PATH is set to ${process.env.PROXY_PATH} but the file does not exist`); + } + return process.env.PROXY_PATH; + } + + // Otherwise try the debug build if present + const debugProxyPath = path.join( + __dirname, + "..", + "..", + "..", + "..", + "..", + "..", + "desktop_native", + "target", + "debug", + `desktop_proxy${proxyExtension}`, + ); + if (fs.existsSync(debugProxyPath)) { + return debugProxyPath; + } + + // On MacOS, try the release build (sandboxed) + const macReleaseProxyPath = `/Applications/Bitwarden.app/Contents/MacOS/desktop_proxy${proxyExtension}`; + if (process.platform === "darwin" && fs.existsSync(macReleaseProxyPath)) { + return macReleaseProxyPath; + } + + throw new Error("Could not find the desktop_proxy executable"); +} diff --git a/apps/desktop/native-messaging-test-runner/tsconfig.json b/apps/desktop/native-messaging-test-runner/tsconfig.json index 72a28de3f7..59c7040e50 100644 --- a/apps/desktop/native-messaging-test-runner/tsconfig.json +++ b/apps/desktop/native-messaging-test-runner/tsconfig.json @@ -5,12 +5,17 @@ "target": "es6", "module": "CommonJS", "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, "sourceMap": false, "declaration": false, "paths": { "@src/*": ["src/*"], - "@bitwarden/node/*": ["../../../libs/node/src/*"], - "@bitwarden/common/*": ["../../../libs/common/src/*"] + "@bitwarden/admin-console/*": ["../../../libs/admin-console/src/*"], + "@bitwarden/auth/*": ["../../../libs/auth/src/*"], + "@bitwarden/common/*": ["../../../libs/common/src/*"], + "@bitwarden/key-management": ["../../../libs/key-management/src/"], + "@bitwarden/node/*": ["../../../libs/node/src/*"] }, "plugins": [ { diff --git a/package-lock.json b/package-lock.json index 622ee9bf9d..01aace3228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,7 +113,6 @@ "@types/node": "22.10.7", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.15", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", @@ -157,7 +156,6 @@ "json5": "2.2.3", "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", - "node-ipc": "9.2.1", "postcss": "8.5.1", "postcss-loader": "8.1.1", "prettier": "3.4.2", @@ -10047,16 +10045,6 @@ "@types/node": "*" } }, - "node_modules/@types/node-ipc": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", - "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node/node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -15862,16 +15850,6 @@ "dev": true, "license": "MIT" }, - "node_modules/easy-stack": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", - "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -17182,16 +17160,6 @@ "node": ">= 0.6" } }, - "node_modules/event-pubsub": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", - "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", - "dev": true, - "license": "Unlicense", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/event-stream": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", @@ -21964,29 +21932,6 @@ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", "license": "MIT" }, - "node_modules/js-message": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", - "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/js-queue": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", - "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "easy-stack": "^1.0.1" - }, - "engines": { - "node": ">=1.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -25710,21 +25655,6 @@ "license": "MIT", "peer": true }, - "node_modules/node-ipc": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", - "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-pubsub": "4.3.0", - "js-message": "1.0.7", - "js-queue": "2.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", diff --git a/package.json b/package.json index 44733e4ab0..32b5b2b6af 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "@types/node": "22.10.7", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.15", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", @@ -118,7 +117,6 @@ "json5": "2.2.3", "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", - "node-ipc": "9.2.1", "postcss": "8.5.1", "postcss-loader": "8.1.1", "prettier": "3.4.2",