mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 11:01:17 +00:00
* Use typescript-strict-plugin to iteratively turn on strict * Add strict testing to pipeline Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files. * turn on strict for scripts directory * Use plugin for all tsconfigs in monorepo vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes. * remove plugin from configs that extend one that already has it * Update workspace settings to honor strict plugin * Apply strict-plugin to native message test runner * Update vscode workspace to use root tsc version * `./node_modules/.bin/update-strict-comments` 🤖 This is a one-time operation. All future files should adhere to strict type checking. * Add fixme to `ts-strict-ignore` comments * `update-strict-comments` 🤖 repeated for new merge files
172 lines
6.0 KiB
TypeScript
172 lines
6.0 KiB
TypeScript
// 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 { MessageCommon } from "../../src/models/native-messaging/message-common";
|
|
import { UnencryptedMessageResponse } from "../../src/models/native-messaging/unencrypted-message-response";
|
|
|
|
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 guarantee 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);
|
|
}
|
|
}
|
|
}
|