mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
Initial PoC for browser <-> desktop communication
This commit is contained in:
22
proxy/app.ts
Normal file
22
proxy/app.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import NativeMessage from './nativemessage';
|
||||
import IPC from './ipc';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
class Proxy {
|
||||
ipc: IPC;
|
||||
nativeMessage: NativeMessage;
|
||||
|
||||
constructor() {
|
||||
this.ipc = new IPC();
|
||||
this.nativeMessage = new NativeMessage(this.ipc);
|
||||
}
|
||||
|
||||
run() {
|
||||
this.ipc.connect();
|
||||
this.nativeMessage.listen();
|
||||
}
|
||||
}
|
||||
|
||||
const proxy = new Proxy();
|
||||
proxy.run();
|
||||
4
proxy/global.d.ts
vendored
Normal file
4
proxy/global.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module 'node-ipc' {
|
||||
const x: any;
|
||||
export = x;
|
||||
}
|
||||
61
proxy/ipc.ts
Normal file
61
proxy/ipc.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/* tslint:disable:no-console */
|
||||
import * as ipc from 'node-ipc';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
const StartDesktopCooldown = 60 * 1000; // 1 minute delay between attempts to start desktop
|
||||
|
||||
ipc.config.id = 'proxy';
|
||||
ipc.config.retry = 1500;
|
||||
ipc.config.logger = console.warn; // Stdout is used for native messaging
|
||||
|
||||
export default class IPC {
|
||||
private connected = false;
|
||||
private lastStartedDesktop = 0;
|
||||
|
||||
connect() {
|
||||
ipc.connectTo('bitwarden', () => {
|
||||
ipc.of.bitwarden.on('connect', () => {
|
||||
this.connected = true;
|
||||
console.error(
|
||||
'## connected to bitwarden desktop ##',
|
||||
ipc.config.delay
|
||||
);
|
||||
ipc.of.bitwarden.emit('message', 'hello');
|
||||
});
|
||||
|
||||
ipc.of.bitwarden.on('disconnect', () => {
|
||||
this.connected = false;
|
||||
console.error('disconnected from world');
|
||||
});
|
||||
|
||||
ipc.of.bitwarden.on('message', (data: any) => {
|
||||
console.error('got a message from world : ', data);
|
||||
});
|
||||
|
||||
ipc.of.bitwarden.on('error', (err: any) => {
|
||||
if (err.syscall === 'connect' && this.lastStartedDesktop + StartDesktopCooldown < Date.now()) {
|
||||
this.lastStartedDesktop = Date.now();
|
||||
console.error('Attempting to start Bitwarden desktop application');
|
||||
this.startDesktop();
|
||||
}
|
||||
console.error('error', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
isConnected() {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
send(json: object) {
|
||||
ipc.of.bitwarden.emit('message', json);
|
||||
}
|
||||
|
||||
// TODO: Do we want to start the desktop application? How do we get the install path?
|
||||
private startDesktop() {
|
||||
spawn(
|
||||
'C:\\Users\\Oscar\\Documents\\Projects\\Bitwarden\\desktop\\dist\\Bitwarden-Portable-1.22.2.exe',
|
||||
{ detached: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
85
proxy/nativemessage.ts
Normal file
85
proxy/nativemessage.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/* tslint:disable:no-console */
|
||||
import IPC from 'ipc';
|
||||
|
||||
// Mostly copied from the example on
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging
|
||||
export default class NativeMessage {
|
||||
ipc: IPC;
|
||||
|
||||
constructor(ipc: IPC) {
|
||||
this.ipc = ipc;
|
||||
}
|
||||
|
||||
send(message: object) {
|
||||
const messageBuffer = Buffer.from(JSON.stringify(message));
|
||||
|
||||
const headerBuffer = Buffer.alloc(4);
|
||||
headerBuffer.writeUInt32LE(messageBuffer.length, 0);
|
||||
|
||||
process.stdout.write(Buffer.concat([headerBuffer, messageBuffer]));
|
||||
}
|
||||
|
||||
listen() {
|
||||
let payloadSize: number = null;
|
||||
|
||||
// A queue to store the chunks as we read them from stdin.
|
||||
// This queue can be flushed when `payloadSize` data has been read
|
||||
const chunks: any = [];
|
||||
|
||||
// Only read the size once for each payload
|
||||
const sizeHasBeenRead = () => Boolean(payloadSize);
|
||||
|
||||
// All the data has been read, reset everything for the next message
|
||||
const flushChunksQueue = () => {
|
||||
payloadSize = null;
|
||||
chunks.splice(0);
|
||||
};
|
||||
|
||||
const processData = () => {
|
||||
// Create one big buffer with all all the chunks
|
||||
const stringData = Buffer.concat(chunks);
|
||||
console.error(stringData);
|
||||
|
||||
// The browser will emit the size as a header of the payload,
|
||||
// if it hasn't been read yet, do it.
|
||||
// The next time we'll need to read the payload size is when all of the data
|
||||
// of the current payload has been read (ie. data.length >= payloadSize + 4)
|
||||
if (!sizeHasBeenRead()) {
|
||||
payloadSize = stringData.readUInt32LE(0);
|
||||
}
|
||||
|
||||
// If the data we have read so far is >= to the size advertised in the header,
|
||||
// it means we have all of the data sent.
|
||||
// We add 4 here because that's the size of the bytes that old the payloadSize
|
||||
if (stringData.length >= payloadSize + 4) {
|
||||
// Remove the header
|
||||
const contentWithoutSize = stringData
|
||||
.slice(4, payloadSize + 4)
|
||||
.toString();
|
||||
|
||||
// Reset the read size and the queued chunks
|
||||
flushChunksQueue();
|
||||
|
||||
const json = JSON.parse(contentWithoutSize);
|
||||
console.error(json);
|
||||
|
||||
// Forward to desktop application
|
||||
this.ipc.send(json);
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
// A temporary variable holding the nodejs.Buffer of each
|
||||
// chunk of data read off stdin
|
||||
let chunk = null;
|
||||
|
||||
// Read all of the available data
|
||||
// tslint:disable-next-line:no-conditional-assignment
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
processData();
|
||||
});
|
||||
}
|
||||
}
|
||||
56
proxy/tsconfig.json
Normal file
56
proxy/tsconfig.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "CommonJS",
|
||||
"target": "ES2016",
|
||||
"allowJs": true,
|
||||
"sourceMap": true,
|
||||
"types": ["sweetalert2", "node"],
|
||||
"baseUrl": ".",
|
||||
"outDir": "../build/proxy",
|
||||
"paths": {
|
||||
"tldjs": [
|
||||
"jslib/src/misc/tldjs.noop"
|
||||
],
|
||||
"jslib/*": [
|
||||
"jslib/src/*"
|
||||
],
|
||||
"@angular/*": [
|
||||
"node_modules/@angular/*"
|
||||
],
|
||||
"electron": [
|
||||
"node_modules/electron"
|
||||
],
|
||||
"electron-log": [
|
||||
"node_modules/electron-log"
|
||||
],
|
||||
"electron-store": [
|
||||
"node_modules/electron-store"
|
||||
],
|
||||
"node": [
|
||||
"node_modules/@types/node"
|
||||
],
|
||||
"duo_web_sdk": [
|
||||
"node_modules/duo_web_sdk"
|
||||
]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"preserveWhitespaces": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jslib/node_modules",
|
||||
"dist",
|
||||
"dist-safari",
|
||||
"jslib/dist",
|
||||
"build",
|
||||
"jslib/spec",
|
||||
"jslib/src/cli",
|
||||
"jslib/src/services/nodeApi.service.ts",
|
||||
"jslib/src/services/lowdbStorage.service.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user