connecting to the native app from the extension

This commit is contained in:
antelle 2021-04-10 21:16:48 +02:00
parent d6e7e3d4a0
commit 25c3c9c565
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
5 changed files with 163 additions and 19 deletions

View File

@ -178,7 +178,7 @@ ready(() => {
AppRightsChecker.init();
IdleTracker.init();
UsbListener.init();
BrowserExtensionConnector.init();
BrowserExtensionConnector.init(appModel);
setTimeout(() => {
PluginManager.runAutoUpdate();
}, Timeouts.AutoUpdatePluginsAfterStart);

View File

@ -4,12 +4,13 @@ import { Events } from 'framework/events';
import { RuntimeInfo } from 'const/runtime-info';
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AppModel } from 'models/app-model';
import { Alerts } from 'comp/ui/alerts';
import { PasswordGenerator } from 'util/generators/password-generator';
import { GeneratorPresets } from 'comp/app/generator-presets';
let appModel;
const connectedClients = {};
const MaxIncomingDataLength = 10000;
function incrementNonce(nonce) {
// from libsodium/utils.c, like it is in KeePassXC
@ -107,13 +108,13 @@ const ProtocolHandlers = {
'get-databasehash'(request) {
decryptRequest(request);
const firstFile = AppModel.instance.files.firstActiveKdbxFile();
const firstFile = appModel.files.firstActiveKdbxFile();
if (firstFile?.defaultGroupHash) {
return encryptResponse(request, {
action: 'hash',
version: RuntimeInfo.version,
hash: firstFile.defaultGroupHash,
hashes: AppModel.instance.files
hashes: appModel.files
.filter((file) => file.active && !file.backend)
.map((file) => file.defaultGroupHash)
});
@ -158,7 +159,9 @@ const ProtocolHandlers = {
const BrowserExtensionConnector = {
enabled: false,
init() {
init(model) {
appModel = model;
this.browserWindowMessage = this.browserWindowMessage.bind(this);
this.fileOpened = this.fileOpened.bind(this);
this.oneFileClosed = this.oneFileClosed.bind(this);
@ -179,7 +182,9 @@ const BrowserExtensionConnector = {
},
start() {
if (!Launcher) {
if (Launcher) {
this.startDesktopAppListener();
} else {
this.startWebMessageListener();
}
Events.on('file-opened', this.fileOpened);
@ -188,7 +193,9 @@ const BrowserExtensionConnector = {
},
stop() {
if (!Launcher) {
if (Launcher) {
this.stopDesktopAppListener();
} else {
this.stopWebMessageListener();
}
Events.off('file-opened', this.fileOpened);
@ -204,6 +211,103 @@ const BrowserExtensionConnector = {
window.removeEventListener('message', this.browserWindowMessage);
},
startDesktopAppListener() {
Launcher.closeOldBrowserExtensionSocket(() => {
const sockName = Launcher.getBrowserExtensionSocketName();
const { createServer } = Launcher.req('net');
this.connectedSockets = [];
this.connectedSocketState = new WeakMap();
this.server = createServer((socket) => {
// TODO: identity check
this.connectedSockets.push(socket);
this.connectedSocketState.set(socket, { active: true });
socket.on('data', (data) => {
this.onSocketData(socket, data);
});
socket.on('close', () => {
// TODO: remove the client
this.connectedSockets = this.connectedSockets.filter((s) => s !== socket);
this.connectedSocketState.delete(socket);
});
});
this.server.listen(sockName);
});
},
stopDesktopAppListener() {
for (const socket of this.connectedSockets) {
socket.destroy();
}
if (this.server) {
this.server.close();
}
this.connectedSockets = [];
this.connectedSocketState = new WeakMap();
},
onSocketData(socket, data) {
if (data.byteLength > MaxIncomingDataLength) {
socket.destroy();
return;
}
const state = this.connectedSocketState.get(socket);
if (!state) {
return;
}
if (state.pendingData) {
state.pendingData = Buffer.concat([state.pendingData, data]);
} else {
state.pendingData = data;
}
if (state.pendingData.length < 4) {
return;
}
while (state.pendingData) {
const lengthBuffer = state.pendingData.slice(0, 4);
const length = new Uint32Array(lengthBuffer)[0];
if (length > MaxIncomingDataLength) {
socket.destroy();
return;
}
if (state.pendingData.byteLength < length + 4) {
return;
}
const messageBytes = state.pendingData.slice(4, length + 4);
if (state.pendingData.byteLength > length + 4) {
state.pendingData = state.pendingData.slice(length + 4);
} else {
state.pendingData = null;
}
const str = messageBytes.toString();
let request;
try {
request = JSON.parse(str);
} catch {
socket.destroy();
return;
}
let response;
try {
const handler = ProtocolHandlers[request.action];
if (!handler) {
throw new Error(`Handler not found: ${request.action}`);
}
response = handler(request) || {};
} catch (e) {
response = { error: e.message || 'Unknown error' };
}
if (response) {
this.sendSocketResponse(socket, response);
}
}
},
browserWindowMessage(e) {
if (e.origin !== location.origin) {
return;
@ -225,32 +329,60 @@ const BrowserExtensionConnector = {
response = { error: e.message || 'Unknown error' };
}
if (response) {
this.sendResponse(response);
this.sendWebResponse(response);
}
},
sendResponse(response) {
sendWebResponse(response) {
response.kwConnect = 'response';
postMessage(response, window.location.origin);
},
sendSocketResponse(socket, response) {
const responseData = Buffer.from(JSON.stringify(response));
const lengthBytes = Buffer.from(new Uint32Array([responseData.byteLength]).buffer);
const data = Buffer.concat([lengthBytes, responseData]);
socket.write(data);
},
sendSocketEvent(data) {
for (const socket of this.connectedSockets) {
const state = this.connectedSocketState.get(socket);
if (state?.active) {
this.sendSocketResponse(socket, data);
}
}
},
sendEvent(data) {
if (Launcher) {
this.sendSocketEvent(data);
} else {
this.sendWebResponse(data);
}
},
fileOpened() {
this.sendResponse({ action: 'database-unlocked' });
this.sendEvent({ action: 'database-unlocked' });
},
oneFileClosed() {
this.sendResponse({ action: 'database-locked' });
if (AppModel.instance.files.hasOpenFiles()) {
this.sendResponse({ action: 'database-unlocked' });
this.sendEvent({ action: 'database-locked' });
if (appModel.files.hasOpenFiles()) {
this.sendEvent({ action: 'database-unlocked' });
}
},
allFilesClosed() {
this.sendResponse({ action: 'database-locked' });
this.sendEvent({ action: 'database-locked' });
},
focusKeeWeb() {
this.sendResponse({ action: 'attention-required' });
if (Launcher) {
Launcher.showMainWindow();
} else {
this.sendEvent({ action: 'attention-required' });
}
}
};

View File

@ -288,6 +288,18 @@ const Launcher = {
},
mainWindowMaximized() {
return this.getMainWindow().isMaximized();
},
getBrowserExtensionSocketName() {
if (process.platform === 'win32') {
return '\\\\.\\pipe\\keeweb-browser';
} else {
return this.joinPath(this.remoteApp().getPath('temp'), 'keeweb-browser.sock');
}
},
closeOldBrowserExtensionSocket(done) {
if (process.platform !== 'win32') {
this.deleteFile(this.getBrowserExtensionSocketName(), done);
}
}
};

View File

@ -11,7 +11,7 @@
// https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-protocol
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#define APP_EXECUTABLE_FILE_NAME "KeeWeb.exe"
#elif __APPLE__
#define APP_EXECUTABLE_FILE_NAME "KeeWeb"
@ -21,10 +21,10 @@
constexpr auto kKeeWebLaunchArg = "--browser-extension";
constexpr auto kSockName = "keeweb.sock";
constexpr auto kSockName = "keeweb-browser.sock";
constexpr std::array kAllowedOrigins = {
std::string_view("chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj")};
std::string_view("chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj/")};
constexpr uint32_t kMaxKeeWebConnectAttempts = 10;
constexpr uint32_t kMaxKeeWebConnectRetryTimeoutMillis = 500;

View File

@ -6,7 +6,7 @@ import childProcess from 'child_process';
import { expect } from 'chai';
describe('KeeWeb extension native module host', function () {
const sockPath = path.join(os.tmpdir(), 'keeweb.sock');
const sockPath = path.join(os.tmpdir(), 'keeweb-browser.sock');
const hostPath = 'build/keeweb-native-messaging-host';
const extensionOrigin = 'chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj';