mirror of https://github.com/keeweb/keeweb
refactored browser extension connector module
parent
5397fa8fd3
commit
81a311e7fa
|
@ -6,7 +6,7 @@ import { ExportApi } from 'comp/app/export-api';
|
|||
import { SingleInstanceChecker } from 'comp/app/single-instance-checker';
|
||||
import { Updater } from 'comp/app/updater';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { BrowserExtensionConnector } from 'comp/app/browser-extension-connector';
|
||||
import { BrowserExtensionConnector } from 'comp/extension/browser-extension-connector';
|
||||
import { FeatureTester } from 'comp/browser/feature-tester';
|
||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||
|
|
|
@ -0,0 +1,337 @@
|
|||
import kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Logger } from 'util/logger';
|
||||
import { ProtocolHandlers, initProtocolHandlers } from './protocol-handlers';
|
||||
|
||||
const logger = new Logger('browser-extension-connector');
|
||||
if (!localStorage.debugBrowserExtension) {
|
||||
logger.level = Logger.Level.Info;
|
||||
}
|
||||
|
||||
let appModel;
|
||||
const connectedClients = new Map();
|
||||
const pendingBrowserMessages = [];
|
||||
let processingBrowserMessage = false;
|
||||
const MaxIncomingDataLength = 10_000;
|
||||
|
||||
const BrowserExtensionConnector = {
|
||||
enabled: false,
|
||||
logger,
|
||||
connectedClients,
|
||||
|
||||
init(model) {
|
||||
appModel = model;
|
||||
|
||||
const sendEvent = this.sendEvent.bind(this);
|
||||
initProtocolHandlers({ appModel, logger, connectedClients, sendEvent });
|
||||
|
||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||
this.fileOpened = this.fileOpened.bind(this);
|
||||
this.oneFileClosed = this.oneFileClosed.bind(this);
|
||||
this.allFilesClosed = this.allFilesClosed.bind(this);
|
||||
|
||||
AppSettingsModel.on('change:browserExtension', (model, enabled) => {
|
||||
this.enabled = enabled;
|
||||
if (enabled) {
|
||||
this.start();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
|
||||
if (AppSettingsModel.browserExtension) {
|
||||
this.enabled = true;
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
if (Launcher) {
|
||||
this.startDesktopAppListener();
|
||||
} else {
|
||||
this.startWebMessageListener();
|
||||
}
|
||||
Events.on('file-opened', this.fileOpened);
|
||||
Events.on('one-file-closed', this.oneFileClosed);
|
||||
Events.on('all-files-closed', this.allFilesClosed);
|
||||
|
||||
logger.info('Started');
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (Launcher) {
|
||||
this.stopDesktopAppListener();
|
||||
} else {
|
||||
this.stopWebMessageListener();
|
||||
}
|
||||
Events.off('file-opened', this.fileOpened);
|
||||
Events.off('one-file-closed', this.oneFileClosed);
|
||||
Events.off('all-files-closed', this.allFilesClosed);
|
||||
|
||||
logger.info('Stopped');
|
||||
},
|
||||
|
||||
startWebMessageListener() {
|
||||
window.addEventListener('message', this.browserWindowMessage);
|
||||
},
|
||||
|
||||
stopWebMessageListener() {
|
||||
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) => {
|
||||
logger.info('New connection');
|
||||
this.connectedSockets.push(socket);
|
||||
this.connectedSocketState.set(socket, {});
|
||||
this.checkSocketIdentity(socket);
|
||||
socket.on('data', (data) => this.onSocketData(socket, data));
|
||||
socket.on('close', () => this.onSocketClose(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();
|
||||
},
|
||||
|
||||
checkSocketIdentity(socket) {
|
||||
const state = this.connectedSocketState.get(socket);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: check the process
|
||||
|
||||
state.active = true;
|
||||
this.processPendingSocketData(socket);
|
||||
},
|
||||
|
||||
onSocketClose(socket) {
|
||||
const state = this.connectedSocketState.get(socket);
|
||||
if (state?.clientId) {
|
||||
connectedClients.delete(state.clientId);
|
||||
}
|
||||
this.connectedSocketState.delete(socket);
|
||||
|
||||
this.connectedSockets = this.connectedSockets.filter((s) => s !== socket);
|
||||
|
||||
logger.info('Connection closed', state?.clientId);
|
||||
},
|
||||
|
||||
onSocketData(socket, data) {
|
||||
if (data.byteLength > MaxIncomingDataLength) {
|
||||
logger.warn('Too many bytes rejected', data.byteLength);
|
||||
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.active) {
|
||||
this.processPendingSocketData(socket);
|
||||
}
|
||||
},
|
||||
|
||||
async processPendingSocketData(socket) {
|
||||
const state = this.connectedSocketState.get(socket);
|
||||
if (!state?.pendingData || state.processingData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.pendingData.length < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lengthBuffer = kdbxweb.ByteUtils.arrayToBuffer(state.pendingData.slice(0, 4));
|
||||
const length = new Uint32Array(lengthBuffer)[0];
|
||||
|
||||
if (length > MaxIncomingDataLength) {
|
||||
logger.warn('Large message rejected', length);
|
||||
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 {
|
||||
logger.warn('Failed to parse message', str);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Extension -> KeeWeb', request);
|
||||
|
||||
const clientId = request?.clientID;
|
||||
if (!clientId) {
|
||||
logger.warn('Empty client ID in request', request);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.clientId) {
|
||||
state.clientId = clientId;
|
||||
} else if (state.clientId !== clientId) {
|
||||
logger.warn(`Changing client ID is not allowed: ${state.clientId} => ${clientId}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
state.processingData = true;
|
||||
|
||||
let response;
|
||||
try {
|
||||
const handler = ProtocolHandlers[request.action];
|
||||
if (!handler) {
|
||||
throw new Error(`Handler not found: ${request.action}`);
|
||||
}
|
||||
response = await handler(request);
|
||||
} catch (e) {
|
||||
response = this.errorToResponse(e, request);
|
||||
}
|
||||
|
||||
state.processingData = false;
|
||||
|
||||
if (response) {
|
||||
this.sendSocketResponse(socket, response);
|
||||
}
|
||||
|
||||
this.processPendingSocketData(socket);
|
||||
},
|
||||
|
||||
browserWindowMessage(e) {
|
||||
if (e.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
if (e.source !== window) {
|
||||
return;
|
||||
}
|
||||
if (e?.data?.kwConnect !== 'request') {
|
||||
return;
|
||||
}
|
||||
logger.debug('Extension -> KeeWeb', e.data);
|
||||
pendingBrowserMessages.push(e.data);
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
async processBrowserMessages() {
|
||||
if (!pendingBrowserMessages.length || processingBrowserMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
processingBrowserMessage = true;
|
||||
|
||||
const request = pendingBrowserMessages.shift();
|
||||
|
||||
let response;
|
||||
try {
|
||||
const handler = ProtocolHandlers[request.action];
|
||||
if (!handler) {
|
||||
throw new Error(`Handler not found: ${request.action}`);
|
||||
}
|
||||
response = await handler(request);
|
||||
} catch (e) {
|
||||
response = this.errorToResponse(e, request);
|
||||
}
|
||||
|
||||
processingBrowserMessage = false;
|
||||
|
||||
if (response) {
|
||||
this.sendWebResponse(response);
|
||||
}
|
||||
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
errorToResponse(e, request) {
|
||||
return {
|
||||
action: request.action,
|
||||
error: e.message || 'Unknown error',
|
||||
errorCode: e.code || 0
|
||||
};
|
||||
},
|
||||
|
||||
sendWebResponse(response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
response.kwConnect = 'response';
|
||||
postMessage(response, window.location.origin);
|
||||
},
|
||||
|
||||
sendSocketResponse(socket, response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
const responseData = Buffer.from(JSON.stringify(response));
|
||||
const lengthBuf = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
new Uint32Array([responseData.byteLength])
|
||||
);
|
||||
const lengthBytes = Buffer.from(lengthBuf);
|
||||
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.sendEvent({ action: 'database-unlocked' });
|
||||
},
|
||||
|
||||
oneFileClosed() {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
this.sendEvent({ action: 'database-locked' });
|
||||
}
|
||||
},
|
||||
|
||||
allFilesClosed() {
|
||||
this.sendEvent({ action: 'database-locked' });
|
||||
}
|
||||
};
|
||||
|
||||
export { BrowserExtensionConnector };
|
|
@ -1,29 +1,40 @@
|
|||
import kdbxweb from 'kdbxweb';
|
||||
import { box as tweetnaclBox } from 'tweetnacl';
|
||||
import { Events } from 'framework/events';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { KnownAppVersions } from 'const/known-app-versions';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { box as tweetnaclBox } from 'tweetnacl';
|
||||
import { PasswordGenerator } from 'util/generators/password-generator';
|
||||
import { GeneratorPresets } from 'comp/app/generator-presets';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
import { Locale } from 'util/locale';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { KnownAppVersions } from 'const/known-app-versions';
|
||||
|
||||
const logger = new Logger('browser-extension-connector');
|
||||
if (!localStorage.debugBrowserExtension) {
|
||||
logger.level = Logger.Level.Info;
|
||||
}
|
||||
|
||||
let appModel;
|
||||
const connectedClients = new Map();
|
||||
const pendingBrowserMessages = [];
|
||||
let processingBrowserMessage = false;
|
||||
const MaxIncomingDataLength = 10_000;
|
||||
const KeeWebAssociationId = 'KeeWeb';
|
||||
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
|
||||
|
||||
const Errors = {
|
||||
noOpenFiles: {
|
||||
message: Locale.extensionErrorNoOpenFiles,
|
||||
code: '1'
|
||||
},
|
||||
userRejected: {
|
||||
message: Locale.extensionErrorUserRejected,
|
||||
code: '6'
|
||||
}
|
||||
};
|
||||
|
||||
let logger;
|
||||
let appModel;
|
||||
let connectedClients;
|
||||
let sendEvent;
|
||||
|
||||
function initProtocolHandlers(vars) {
|
||||
appModel = vars.appModel;
|
||||
logger = vars.logger;
|
||||
connectedClients = vars.connectedClients;
|
||||
sendEvent = vars.sendEvent;
|
||||
}
|
||||
|
||||
function incrementNonce(nonce) {
|
||||
// from libsodium/utils.c, like it is in KeePassXC
|
||||
let i = 0;
|
||||
|
@ -126,7 +137,7 @@ function checkContentRequestPermissions(request) {
|
|||
return reject(new Error(Locale.extensionErrorAlertDisplayed));
|
||||
}
|
||||
|
||||
BrowserExtensionConnector.focusKeeWeb();
|
||||
focusKeeWeb();
|
||||
|
||||
// TODO: make a proper dialog here instead of a simple question
|
||||
|
||||
|
@ -170,16 +181,14 @@ function isKeeWebConnect(request) {
|
|||
return getClient(request).extensionName === 'keeweb-connect';
|
||||
}
|
||||
|
||||
const Errors = {
|
||||
noOpenFiles: {
|
||||
message: Locale.extensionErrorNoOpenFiles,
|
||||
code: '1'
|
||||
},
|
||||
userRejected: {
|
||||
message: Locale.extensionErrorUserRejected,
|
||||
code: '6'
|
||||
function focusKeeWeb() {
|
||||
logger.debug('Focus KeeWeb');
|
||||
if (Launcher) {
|
||||
Launcher.showMainWindow();
|
||||
} else {
|
||||
sendEvent({ action: 'attention-required' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const ProtocolHandlers = {
|
||||
'ping'({ data }) {
|
||||
|
@ -247,7 +256,7 @@ const ProtocolHandlers = {
|
|||
Events.emit('lock-workspace');
|
||||
|
||||
if (Alerts.alertDisplayed) {
|
||||
BrowserExtensionConnector.focusKeeWeb();
|
||||
focusKeeWeb();
|
||||
}
|
||||
|
||||
return encryptResponse(request, {
|
||||
|
@ -369,325 +378,4 @@ const ProtocolHandlers = {
|
|||
}
|
||||
};
|
||||
|
||||
const BrowserExtensionConnector = {
|
||||
enabled: false,
|
||||
|
||||
init(model) {
|
||||
appModel = model;
|
||||
|
||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||
this.fileOpened = this.fileOpened.bind(this);
|
||||
this.oneFileClosed = this.oneFileClosed.bind(this);
|
||||
this.allFilesClosed = this.allFilesClosed.bind(this);
|
||||
|
||||
AppSettingsModel.on('change:browserExtension', (model, enabled) => {
|
||||
this.enabled = enabled;
|
||||
if (enabled) {
|
||||
this.start();
|
||||
} else {
|
||||
this.stop();
|
||||
}
|
||||
});
|
||||
if (AppSettingsModel.browserExtension) {
|
||||
this.enabled = true;
|
||||
this.start();
|
||||
}
|
||||
},
|
||||
|
||||
start() {
|
||||
if (Launcher) {
|
||||
this.startDesktopAppListener();
|
||||
} else {
|
||||
this.startWebMessageListener();
|
||||
}
|
||||
Events.on('file-opened', this.fileOpened);
|
||||
Events.on('one-file-closed', this.oneFileClosed);
|
||||
Events.on('all-files-closed', this.allFilesClosed);
|
||||
|
||||
logger.info('Started');
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (Launcher) {
|
||||
this.stopDesktopAppListener();
|
||||
} else {
|
||||
this.stopWebMessageListener();
|
||||
}
|
||||
Events.off('file-opened', this.fileOpened);
|
||||
Events.off('one-file-closed', this.oneFileClosed);
|
||||
Events.off('all-files-closed', this.allFilesClosed);
|
||||
|
||||
logger.info('Stopped');
|
||||
},
|
||||
|
||||
startWebMessageListener() {
|
||||
window.addEventListener('message', this.browserWindowMessage);
|
||||
},
|
||||
|
||||
stopWebMessageListener() {
|
||||
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) => {
|
||||
logger.info('New connection');
|
||||
this.connectedSockets.push(socket);
|
||||
this.connectedSocketState.set(socket, {});
|
||||
this.checkSocketIdentity(socket);
|
||||
socket.on('data', (data) => this.onSocketData(socket, data));
|
||||
socket.on('close', () => this.onSocketClose(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();
|
||||
},
|
||||
|
||||
checkSocketIdentity(socket) {
|
||||
const state = this.connectedSocketState.get(socket);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: check the process
|
||||
|
||||
state.active = true;
|
||||
this.processPendingSocketData(socket);
|
||||
},
|
||||
|
||||
onSocketClose(socket) {
|
||||
const state = this.connectedSocketState.get(socket);
|
||||
if (state?.clientId) {
|
||||
connectedClients.delete(state.clientId);
|
||||
}
|
||||
this.connectedSocketState.delete(socket);
|
||||
|
||||
this.connectedSockets = this.connectedSockets.filter((s) => s !== socket);
|
||||
|
||||
logger.info('Connection closed', state?.clientId);
|
||||
},
|
||||
|
||||
onSocketData(socket, data) {
|
||||
if (data.byteLength > MaxIncomingDataLength) {
|
||||
logger.warn('Too many bytes rejected', data.byteLength);
|
||||
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.active) {
|
||||
this.processPendingSocketData(socket);
|
||||
}
|
||||
},
|
||||
|
||||
async processPendingSocketData(socket) {
|
||||
const state = this.connectedSocketState.get(socket);
|
||||
if (!state?.pendingData || state.processingData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.pendingData.length < 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lengthBuffer = kdbxweb.ByteUtils.arrayToBuffer(state.pendingData.slice(0, 4));
|
||||
const length = new Uint32Array(lengthBuffer)[0];
|
||||
|
||||
if (length > MaxIncomingDataLength) {
|
||||
logger.warn('Large message rejected', length);
|
||||
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 {
|
||||
logger.warn('Failed to parse message', str);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Extension -> KeeWeb', request);
|
||||
|
||||
const clientId = request?.clientID;
|
||||
if (!clientId) {
|
||||
logger.warn('Empty client ID in request', request);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.clientId) {
|
||||
state.clientId = clientId;
|
||||
} else if (state.clientId !== clientId) {
|
||||
logger.warn(`Changing client ID is not allowed: ${state.clientId} => ${clientId}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
state.processingData = true;
|
||||
|
||||
let response;
|
||||
try {
|
||||
const handler = ProtocolHandlers[request.action];
|
||||
if (!handler) {
|
||||
throw new Error(`Handler not found: ${request.action}`);
|
||||
}
|
||||
response = await handler(request);
|
||||
} catch (e) {
|
||||
response = this.errorToResponse(e, request);
|
||||
}
|
||||
|
||||
state.processingData = false;
|
||||
|
||||
if (response) {
|
||||
this.sendSocketResponse(socket, response);
|
||||
}
|
||||
|
||||
this.processPendingSocketData(socket);
|
||||
},
|
||||
|
||||
browserWindowMessage(e) {
|
||||
if (e.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
if (e.source !== window) {
|
||||
return;
|
||||
}
|
||||
if (e?.data?.kwConnect !== 'request') {
|
||||
return;
|
||||
}
|
||||
logger.debug('Extension -> KeeWeb', e.data);
|
||||
pendingBrowserMessages.push(e.data);
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
async processBrowserMessages() {
|
||||
if (!pendingBrowserMessages.length || processingBrowserMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
processingBrowserMessage = true;
|
||||
|
||||
const request = pendingBrowserMessages.shift();
|
||||
|
||||
let response;
|
||||
try {
|
||||
const handler = ProtocolHandlers[request.action];
|
||||
if (!handler) {
|
||||
throw new Error(`Handler not found: ${request.action}`);
|
||||
}
|
||||
response = await handler(request);
|
||||
} catch (e) {
|
||||
response = this.errorToResponse(e, request);
|
||||
}
|
||||
|
||||
processingBrowserMessage = false;
|
||||
|
||||
if (response) {
|
||||
this.sendWebResponse(response);
|
||||
}
|
||||
|
||||
this.processBrowserMessages();
|
||||
},
|
||||
|
||||
errorToResponse(e, request) {
|
||||
return {
|
||||
action: request.action,
|
||||
error: e.message || 'Unknown error',
|
||||
errorCode: e.code || 0
|
||||
};
|
||||
},
|
||||
|
||||
sendWebResponse(response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
response.kwConnect = 'response';
|
||||
postMessage(response, window.location.origin);
|
||||
},
|
||||
|
||||
sendSocketResponse(socket, response) {
|
||||
logger.debug('KeeWeb -> Extension', response);
|
||||
const responseData = Buffer.from(JSON.stringify(response));
|
||||
const lengthBuf = kdbxweb.ByteUtils.arrayToBuffer(
|
||||
new Uint32Array([responseData.byteLength])
|
||||
);
|
||||
const lengthBytes = Buffer.from(lengthBuf);
|
||||
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.sendEvent({ action: 'database-unlocked' });
|
||||
},
|
||||
|
||||
oneFileClosed() {
|
||||
if (!appModel.files.hasOpenFiles()) {
|
||||
this.sendEvent({ action: 'database-locked' });
|
||||
}
|
||||
},
|
||||
|
||||
allFilesClosed() {
|
||||
this.sendEvent({ action: 'database-locked' });
|
||||
},
|
||||
|
||||
focusKeeWeb() {
|
||||
logger.debug('Focus KeeWeb');
|
||||
if (Launcher) {
|
||||
Launcher.showMainWindow();
|
||||
} else {
|
||||
this.sendEvent({ action: 'attention-required' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { BrowserExtensionConnector };
|
||||
export { ProtocolHandlers, initProtocolHandlers };
|
Loading…
Reference in New Issue