mirror of
https://github.com/keeweb/keeweb.git
synced 2024-06-27 07:45:08 +02:00
connecting to the native app from the extension
This commit is contained in:
parent
d6e7e3d4a0
commit
25c3c9c565
|
@ -178,7 +178,7 @@ ready(() => {
|
||||||
AppRightsChecker.init();
|
AppRightsChecker.init();
|
||||||
IdleTracker.init();
|
IdleTracker.init();
|
||||||
UsbListener.init();
|
UsbListener.init();
|
||||||
BrowserExtensionConnector.init();
|
BrowserExtensionConnector.init(appModel);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
PluginManager.runAutoUpdate();
|
PluginManager.runAutoUpdate();
|
||||||
}, Timeouts.AutoUpdatePluginsAfterStart);
|
}, Timeouts.AutoUpdatePluginsAfterStart);
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { Events } from 'framework/events';
|
||||||
import { RuntimeInfo } from 'const/runtime-info';
|
import { RuntimeInfo } from 'const/runtime-info';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
import { AppModel } from 'models/app-model';
|
|
||||||
import { Alerts } from 'comp/ui/alerts';
|
import { Alerts } from 'comp/ui/alerts';
|
||||||
import { PasswordGenerator } from 'util/generators/password-generator';
|
import { PasswordGenerator } from 'util/generators/password-generator';
|
||||||
import { GeneratorPresets } from 'comp/app/generator-presets';
|
import { GeneratorPresets } from 'comp/app/generator-presets';
|
||||||
|
|
||||||
|
let appModel;
|
||||||
const connectedClients = {};
|
const connectedClients = {};
|
||||||
|
const MaxIncomingDataLength = 10000;
|
||||||
|
|
||||||
function incrementNonce(nonce) {
|
function incrementNonce(nonce) {
|
||||||
// from libsodium/utils.c, like it is in KeePassXC
|
// from libsodium/utils.c, like it is in KeePassXC
|
||||||
|
@ -107,13 +108,13 @@ const ProtocolHandlers = {
|
||||||
'get-databasehash'(request) {
|
'get-databasehash'(request) {
|
||||||
decryptRequest(request);
|
decryptRequest(request);
|
||||||
|
|
||||||
const firstFile = AppModel.instance.files.firstActiveKdbxFile();
|
const firstFile = appModel.files.firstActiveKdbxFile();
|
||||||
if (firstFile?.defaultGroupHash) {
|
if (firstFile?.defaultGroupHash) {
|
||||||
return encryptResponse(request, {
|
return encryptResponse(request, {
|
||||||
action: 'hash',
|
action: 'hash',
|
||||||
version: RuntimeInfo.version,
|
version: RuntimeInfo.version,
|
||||||
hash: firstFile.defaultGroupHash,
|
hash: firstFile.defaultGroupHash,
|
||||||
hashes: AppModel.instance.files
|
hashes: appModel.files
|
||||||
.filter((file) => file.active && !file.backend)
|
.filter((file) => file.active && !file.backend)
|
||||||
.map((file) => file.defaultGroupHash)
|
.map((file) => file.defaultGroupHash)
|
||||||
});
|
});
|
||||||
|
@ -158,7 +159,9 @@ const ProtocolHandlers = {
|
||||||
const BrowserExtensionConnector = {
|
const BrowserExtensionConnector = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
||||||
init() {
|
init(model) {
|
||||||
|
appModel = model;
|
||||||
|
|
||||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||||
this.fileOpened = this.fileOpened.bind(this);
|
this.fileOpened = this.fileOpened.bind(this);
|
||||||
this.oneFileClosed = this.oneFileClosed.bind(this);
|
this.oneFileClosed = this.oneFileClosed.bind(this);
|
||||||
|
@ -179,7 +182,9 @@ const BrowserExtensionConnector = {
|
||||||
},
|
},
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!Launcher) {
|
if (Launcher) {
|
||||||
|
this.startDesktopAppListener();
|
||||||
|
} else {
|
||||||
this.startWebMessageListener();
|
this.startWebMessageListener();
|
||||||
}
|
}
|
||||||
Events.on('file-opened', this.fileOpened);
|
Events.on('file-opened', this.fileOpened);
|
||||||
|
@ -188,7 +193,9 @@ const BrowserExtensionConnector = {
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (!Launcher) {
|
if (Launcher) {
|
||||||
|
this.stopDesktopAppListener();
|
||||||
|
} else {
|
||||||
this.stopWebMessageListener();
|
this.stopWebMessageListener();
|
||||||
}
|
}
|
||||||
Events.off('file-opened', this.fileOpened);
|
Events.off('file-opened', this.fileOpened);
|
||||||
|
@ -204,6 +211,103 @@ const BrowserExtensionConnector = {
|
||||||
window.removeEventListener('message', this.browserWindowMessage);
|
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) {
|
browserWindowMessage(e) {
|
||||||
if (e.origin !== location.origin) {
|
if (e.origin !== location.origin) {
|
||||||
return;
|
return;
|
||||||
|
@ -225,32 +329,60 @@ const BrowserExtensionConnector = {
|
||||||
response = { error: e.message || 'Unknown error' };
|
response = { error: e.message || 'Unknown error' };
|
||||||
}
|
}
|
||||||
if (response) {
|
if (response) {
|
||||||
this.sendResponse(response);
|
this.sendWebResponse(response);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendResponse(response) {
|
sendWebResponse(response) {
|
||||||
response.kwConnect = 'response';
|
response.kwConnect = 'response';
|
||||||
postMessage(response, window.location.origin);
|
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() {
|
fileOpened() {
|
||||||
this.sendResponse({ action: 'database-unlocked' });
|
this.sendEvent({ action: 'database-unlocked' });
|
||||||
},
|
},
|
||||||
|
|
||||||
oneFileClosed() {
|
oneFileClosed() {
|
||||||
this.sendResponse({ action: 'database-locked' });
|
this.sendEvent({ action: 'database-locked' });
|
||||||
if (AppModel.instance.files.hasOpenFiles()) {
|
if (appModel.files.hasOpenFiles()) {
|
||||||
this.sendResponse({ action: 'database-unlocked' });
|
this.sendEvent({ action: 'database-unlocked' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
allFilesClosed() {
|
allFilesClosed() {
|
||||||
this.sendResponse({ action: 'database-locked' });
|
this.sendEvent({ action: 'database-locked' });
|
||||||
},
|
},
|
||||||
|
|
||||||
focusKeeWeb() {
|
focusKeeWeb() {
|
||||||
this.sendResponse({ action: 'attention-required' });
|
if (Launcher) {
|
||||||
|
Launcher.showMainWindow();
|
||||||
|
} else {
|
||||||
|
this.sendEvent({ action: 'attention-required' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -288,6 +288,18 @@ const Launcher = {
|
||||||
},
|
},
|
||||||
mainWindowMaximized() {
|
mainWindowMaximized() {
|
||||||
return this.getMainWindow().isMaximized();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
// https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-protocol
|
// 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
|
// 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"
|
#define APP_EXECUTABLE_FILE_NAME "KeeWeb.exe"
|
||||||
#elif __APPLE__
|
#elif __APPLE__
|
||||||
#define APP_EXECUTABLE_FILE_NAME "KeeWeb"
|
#define APP_EXECUTABLE_FILE_NAME "KeeWeb"
|
||||||
|
@ -21,10 +21,10 @@
|
||||||
|
|
||||||
constexpr auto kKeeWebLaunchArg = "--browser-extension";
|
constexpr auto kKeeWebLaunchArg = "--browser-extension";
|
||||||
|
|
||||||
constexpr auto kSockName = "keeweb.sock";
|
constexpr auto kSockName = "keeweb-browser.sock";
|
||||||
|
|
||||||
constexpr std::array kAllowedOrigins = {
|
constexpr std::array kAllowedOrigins = {
|
||||||
std::string_view("chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj")};
|
std::string_view("chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj/")};
|
||||||
|
|
||||||
constexpr uint32_t kMaxKeeWebConnectAttempts = 10;
|
constexpr uint32_t kMaxKeeWebConnectAttempts = 10;
|
||||||
constexpr uint32_t kMaxKeeWebConnectRetryTimeoutMillis = 500;
|
constexpr uint32_t kMaxKeeWebConnectRetryTimeoutMillis = 500;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import childProcess from 'child_process';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
describe('KeeWeb extension native module host', function () {
|
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 hostPath = 'build/keeweb-native-messaging-host';
|
||||||
const extensionOrigin = 'chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj';
|
const extensionOrigin = 'chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj';
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user