Skip to content

Commit 240f9f9

Browse files
Make native-messaging-test-runner use desktop_proxy (bitwarden#11923)
* Make native-messaging-test-runner use desktop_proxy * Remove node-ipc * Fix build and implement proxy selection * Remove eslint disable --------- Co-authored-by: Matt Bishop <[email protected]>
1 parent f66446f commit 240f9f9

File tree

6 files changed

+140
-195
lines changed

6 files changed

+140
-195
lines changed

apps/desktop/native-messaging-test-runner/package-lock.json

+6-75
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/native-messaging-test-runner/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,13 @@
1717
"@bitwarden/common": "file:../../../libs/common",
1818
"@bitwarden/node": "file:../../../libs/node",
1919
"module-alias": "2.2.3",
20-
"node-ipc": "9.2.1",
2120
"ts-node": "10.9.2",
2221
"uuid": "11.0.5",
2322
"yargs": "17.7.2"
2423
},
2524
"devDependencies": {
2625
"@types/node": "22.10.7",
27-
"@types/node-ipc": "9.2.3",
28-
"typescript": "4.7.4"
26+
"typescript": "5.4.2"
2927
},
3028
"_moduleAliases": {
3129
"@bitwarden/common": "dist/libs/common/src",

apps/desktop/native-messaging-test-runner/src/ipc.service.ts

+126-43
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
/* eslint-disable no-console */
2-
// FIXME: Update this file to be type safe and remove this and next line
3-
// @ts-strict-ignore
4-
import { homedir } from "os";
5-
6-
import * as NodeIPC from "node-ipc";
2+
import { ChildProcess, spawn } from "child_process";
3+
import * as fs from "fs";
4+
import * as path from "path";
75

86
// eslint-disable-next-line no-restricted-imports
97
import { MessageCommon } from "../../src/models/native-messaging/message-common";
@@ -13,11 +11,6 @@ import { UnencryptedMessageResponse } from "../../src/models/native-messaging/un
1311
import Deferred from "./deferred";
1412
import { race } from "./race";
1513

16-
NodeIPC.config.id = "native-messaging-test-runner";
17-
NodeIPC.config.maxRetries = 0;
18-
NodeIPC.config.silent = true;
19-
20-
const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`;
2114
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
2215

2316
export type MessageHandler = (MessageCommon) => void;
@@ -42,6 +35,10 @@ export default class IPCService {
4235
// A set of deferred promises that are awaiting socket connection
4336
private awaitingConnection = new Set<Deferred<void>>();
4437

38+
// The IPC desktop_proxy process
39+
private process?: ChildProcess;
40+
private processOutputBuffer = Buffer.alloc(0);
41+
4542
constructor(
4643
private socketName: string,
4744
private messageHandler: MessageHandler,
@@ -72,47 +69,47 @@ export default class IPCService {
7269
private _connect() {
7370
this.connectionState = IPCConnectionState.Connecting;
7471

75-
NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => {
76-
// Process incoming message
77-
this.getSocket().on("message", (message: any) => {
78-
this.processMessage(message);
79-
});
72+
const proxyPath = selectProxyPath();
73+
console.log(`[IPCService] connecting to proxy at ${proxyPath}`);
8074

81-
this.getSocket().on("error", (error: Error) => {
82-
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
83-
// invoked multiple times each time a connection error happens
84-
console.log("[IPCService] errored");
85-
console.log(
86-
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m",
87-
);
88-
this.awaitingConnection.forEach((deferred) => {
89-
console.log(`rejecting: ${deferred}`);
90-
deferred.reject(error);
91-
});
92-
this.awaitingConnection.clear();
93-
});
75+
this.process = spawn(proxyPath, process.argv.slice(1), {
76+
cwd: process.cwd(),
77+
stdio: "pipe",
78+
shell: false,
79+
});
9480

95-
this.getSocket().on("connect", () => {
96-
console.log("[IPCService] connected");
97-
this.connectionState = IPCConnectionState.Connected;
81+
this.process.stdout.on("data", (data: Buffer) => {
82+
this.processIpcMessage(data);
83+
});
9884

99-
this.awaitingConnection.forEach((deferred) => {
100-
deferred.resolve(null);
101-
});
102-
this.awaitingConnection.clear();
103-
});
85+
this.process.stderr.on("data", (data: Buffer) => {
86+
console.error(`proxy log: ${data}`);
87+
});
10488

105-
this.getSocket().on("disconnect", () => {
106-
console.log("[IPCService] disconnected");
107-
this.connectionState = IPCConnectionState.Disconnected;
89+
this.process.on("error", (error) => {
90+
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
91+
// invoked multiple times each time a connection error happens
92+
console.log("[IPCService] errored");
93+
console.log(
94+
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m",
95+
);
96+
this.awaitingConnection.forEach((deferred) => {
97+
console.log(`rejecting: ${deferred}`);
98+
deferred.reject(error);
10899
});
100+
this.awaitingConnection.clear();
101+
});
102+
103+
this.process.on("exit", () => {
104+
console.log("[IPCService] disconnected");
105+
this.connectionState = IPCConnectionState.Disconnected;
109106
});
110107
}
111108

112109
disconnect() {
113110
console.log("[IPCService] disconnecting...");
114111
if (this.connectionState !== IPCConnectionState.Disconnected) {
115-
NodeIPC.disconnect(this.socketName);
112+
this.process?.kill();
116113
}
117114
}
118115

@@ -133,7 +130,7 @@ export default class IPCService {
133130

134131
this.pendingMessages.set(message.messageId, deferred);
135132

136-
this.getSocket().emit("message", message);
133+
this.sendIpcMessage(message);
137134

138135
try {
139136
// 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 {
151148
}
152149
}
153150

154-
private getSocket() {
155-
return NodeIPC.of[this.socketName];
151+
// As we're using the desktop_proxy to communicate with the native messaging directly,
152+
// the messages need to follow Native Messaging Host protocol (uint32 size followed by message).
153+
// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
154+
private sendIpcMessage(message: MessageCommon) {
155+
const messageStr = JSON.stringify(message);
156+
const buffer = Buffer.alloc(4 + messageStr.length);
157+
buffer.writeUInt32LE(messageStr.length, 0);
158+
buffer.write(messageStr, 4);
159+
160+
this.process?.stdin.write(buffer);
161+
}
162+
163+
private processIpcMessage(data: Buffer) {
164+
this.processOutputBuffer = Buffer.concat([this.processOutputBuffer, data]);
165+
166+
// We might receive more than one IPC message per data event, so we need to process them all
167+
// We continue as long as we have at least 4 + 1 bytes in the buffer, where the first 4 bytes
168+
// represent the message length and the 5th byte is the message
169+
while (this.processOutputBuffer.length > 4) {
170+
// Read the message length and ensure we have the full message
171+
const msgLength = this.processOutputBuffer.readUInt32LE(0);
172+
if (msgLength + 4 < this.processOutputBuffer.length) {
173+
return;
174+
}
175+
176+
// Parse the message from the buffer
177+
const messageStr = this.processOutputBuffer.subarray(4, msgLength + 4).toString();
178+
const message = JSON.parse(messageStr);
179+
180+
// Store the remaining buffer, which is part of the next message
181+
this.processOutputBuffer = this.processOutputBuffer.subarray(msgLength + 4);
182+
183+
// Process the connect/disconnect messages separately
184+
if (message?.command === "connected") {
185+
console.log("[IPCService] connected");
186+
this.connectionState = IPCConnectionState.Connected;
187+
188+
this.awaitingConnection.forEach((deferred) => {
189+
deferred.resolve(null);
190+
});
191+
this.awaitingConnection.clear();
192+
continue;
193+
} else if (message?.command === "disconnected") {
194+
console.log("[IPCService] disconnected");
195+
this.connectionState = IPCConnectionState.Disconnected;
196+
continue;
197+
}
198+
199+
this.processMessage(message);
200+
}
156201
}
157202

158203
private processMessage(message: any) {
@@ -172,3 +217,41 @@ export default class IPCService {
172217
}
173218
}
174219
}
220+
221+
function selectProxyPath(): string {
222+
const proxyExtension = process.platform === "win32" ? ".exe" : "";
223+
224+
// If the PROXY_PATH environment variable is set, use that
225+
if (process.env.PROXY_PATH) {
226+
if (!fs.existsSync(process.env.PROXY_PATH)) {
227+
throw new Error(`PROXY_PATH is set to ${process.env.PROXY_PATH} but the file does not exist`);
228+
}
229+
return process.env.PROXY_PATH;
230+
}
231+
232+
// Otherwise try the debug build if present
233+
const debugProxyPath = path.join(
234+
__dirname,
235+
"..",
236+
"..",
237+
"..",
238+
"..",
239+
"..",
240+
"..",
241+
"desktop_native",
242+
"target",
243+
"debug",
244+
`desktop_proxy${proxyExtension}`,
245+
);
246+
if (fs.existsSync(debugProxyPath)) {
247+
return debugProxyPath;
248+
}
249+
250+
// On MacOS, try the release build (sandboxed)
251+
const macReleaseProxyPath = `/Applications/Bitwarden.app/Contents/MacOS/desktop_proxy${proxyExtension}`;
252+
if (process.platform === "darwin" && fs.existsSync(macReleaseProxyPath)) {
253+
return macReleaseProxyPath;
254+
}
255+
256+
throw new Error("Could not find the desktop_proxy executable");
257+
}

0 commit comments

Comments
 (0)