mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 18:53:29 +00:00
[SG-520] Native messaging handler (#3566)
* [SG-523] Base test runner app for native messages (#3269) * Base test runner app for native messages * Remove default test script * Add case for canceled status * Modify to allow usage of libs crypto services and functions * Small adjustments * Handshake request (#3277) * Handshake request * Fix capitalization * Update info text * lock node-ipc to 9.2.1 * [SG-569] Native Messaging settings bug (#3285) * Fix bug where updating setting wasn't starting the native messaging listener * Update test runner error message * [SG-532] Implement Status command in Native Messaging Service (#3310) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Remove in progress file (merge error) * Move models to their own folder and add index.ts * Remove file that got un-deleted * Remove file that will be added in separate command * Fix imports that got borked * [SG-533] Implement bw-credential-retrieval (#3334) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Add error handling for passing a bad public key to handshake * [SG-534] and [SG-535] Implement Credential Create and Update commands (#3342) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Add bw-credential-create * Better response handling in test runner * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * bw-cipher-create move type into its own file * Use LogUtils for all logging * Implement bw-credential-update * Give naming conventions for types * Rename file correctly * Update handleEncyptedMessage with EncString changes * [SG-626] Fix Desktop app not showing updated credentials from native messages (#3380) * Add MessagingService to send messages on login create and update * Add `not-active-user` error to create and update and other refactors * [SG-536] Implement bw-generate-password (#3370) * implement bw-generate-password * Fix merge conflict resolution errors * Update apps/desktop/native-messaging-test-runner/src/bw-generate-password.ts Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Logging improvements * Add NativeMessagingVersion enum * Add version check in NativeMessagingHandler Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Refactor account status checks and check for locked state in generate command (#3461) * Add feawture flag to show/hide ddg setting (#3506) * [SG-649] Add confirmation dialog and tweak shared key retrieval (#3451) * Add confirmation dialog when completing handshake * Copy updates for dialog * HandshakeResponse type fixes * Add longer timeout for handshake command * [SG-663] RefactorNativeMessagingHandlerService and strengthen typing (#3551) * NativeMessageHandlerService refactor and additional types * Return empty array if no uri to retrieve command * Move commands from test runner into a separate folder * Fix bug where confirmation dialog messes with styling * Enable DDG feature * Fix generated password not saving to history * Take credentialId as parameter to update * Add applicationName to handshake payload * Add warning text to confirmation modal Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
This commit is contained in:
166
apps/desktop/native-messaging-test-runner/src/ipcService.ts
Normal file
166
apps/desktop/native-messaging-test-runner/src/ipcService.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { homedir } from "os";
|
||||
|
||||
import * as NodeIPC from "node-ipc";
|
||||
|
||||
import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon";
|
||||
import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse";
|
||||
|
||||
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;
|
||||
|
||||
export enum IPCConnectionState {
|
||||
Disconnected = "disconnected",
|
||||
Connecting = "connecting",
|
||||
Connected = "connected",
|
||||
}
|
||||
|
||||
export type IPCOptions = {
|
||||
overrideTimeout?: number;
|
||||
};
|
||||
|
||||
export default class IPCService {
|
||||
// The current connection state of the socket.
|
||||
private connectionState: IPCConnectionState = IPCConnectionState.Disconnected;
|
||||
|
||||
// Messages that have been sent, but have not yet received responses
|
||||
private pendingMessages = new Map<string, Deferred<UnencryptedMessageResponse>>();
|
||||
|
||||
// A set of deferred promises that are awaiting socket connection
|
||||
private awaitingConnection = new Set<Deferred<void>>();
|
||||
|
||||
constructor(private socketName: string, private messageHandler: MessageHandler) {}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
console.log("[IPCService] connecting...");
|
||||
if (this.connectionState === IPCConnectionState.Connected) {
|
||||
// Socket is already connected. Don't throw, just allow the callsite to proceed
|
||||
return;
|
||||
}
|
||||
|
||||
const deferredConnections = new Deferred<void>();
|
||||
|
||||
this.awaitingConnection.add(deferredConnections);
|
||||
|
||||
// If the current connection state is disconnected, we should start trying to connect.
|
||||
// The only other possible connection state at this point is "connecting" and if this
|
||||
// is the case, we just want to add a deferred promise to the awaitingConnection collection
|
||||
// and not try to initiate the connection again.
|
||||
if (this.connectionState === IPCConnectionState.Disconnected) {
|
||||
this._connect();
|
||||
}
|
||||
|
||||
return deferredConnections.getPromise();
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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.getSocket().on("connect", () => {
|
||||
console.log("[IPCService] connected");
|
||||
this.connectionState = IPCConnectionState.Connected;
|
||||
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
deferred.resolve(null);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
});
|
||||
|
||||
this.getSocket().on("disconnect", () => {
|
||||
console.log("[IPCService] disconnected");
|
||||
this.connectionState = IPCConnectionState.Disconnected;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
console.log("[IPCService] disconnecting...");
|
||||
if (this.connectionState !== IPCConnectionState.Disconnected) {
|
||||
NodeIPC.disconnect(this.socketName);
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(
|
||||
message: MessageCommon,
|
||||
options: IPCOptions = {}
|
||||
): Promise<UnencryptedMessageResponse> {
|
||||
console.log("[IPCService] sendMessage");
|
||||
if (this.pendingMessages.has(message.messageId)) {
|
||||
throw new Error(`A message with the id: ${message.messageId} has already been sent.`);
|
||||
}
|
||||
|
||||
// Creates a new deferred promise that allows us to convert a message received over the IPC socket
|
||||
// into a response for a message that we previously sent. This mechanism relies on the fact that we
|
||||
// create a unique message id and attach it with each message. Response messages are expected to
|
||||
// include the message id of the message they are responding to.
|
||||
const deferred = new Deferred<UnencryptedMessageResponse>();
|
||||
|
||||
this.pendingMessages.set(message.messageId, deferred);
|
||||
|
||||
this.getSocket().emit("message", message);
|
||||
|
||||
try {
|
||||
// Since we can not guarentee that a response message will ever be sent, we put a timeout
|
||||
// on messages
|
||||
return race({
|
||||
promise: deferred.getPromise(),
|
||||
timeout: options?.overrideTimeout ?? DEFAULT_MESSAGE_TIMEOUT,
|
||||
error: new Error(`Message: ${message.messageId} timed out`),
|
||||
});
|
||||
} catch (error) {
|
||||
// If there is a timeout, remove the message from the pending messages set
|
||||
// before triggering error handling elsewhere.
|
||||
this.pendingMessages.delete(message.messageId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private getSocket() {
|
||||
return NodeIPC.of[this.socketName];
|
||||
}
|
||||
|
||||
private processMessage(message: any) {
|
||||
// If the message is a response to a previous message, resolve the deferred promise that
|
||||
// is awaiting that response. Otherwise, assume this was a new message that wasn't sent as a
|
||||
// response and invoke the message handler.
|
||||
if (message.messageId && this.pendingMessages.has(message.messageId)) {
|
||||
const deferred = this.pendingMessages.get(message.messageId);
|
||||
|
||||
// In the future, this could be improved to add ability to reject, but most messages coming in are
|
||||
// encrypted at this point so we're unable to determine if they contain error info.
|
||||
deferred.resolve(message);
|
||||
|
||||
this.pendingMessages.delete(message.messageId);
|
||||
} else {
|
||||
this.messageHandler(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user