mirror of https://github.com/keeweb/keeweb.git
313 lines
8.8 KiB
JavaScript
313 lines
8.8 KiB
JavaScript
import kdbxweb from 'kdbxweb';
|
|
import { Launcher } from 'comp/launcher';
|
|
import { Logger } from 'util/logger';
|
|
import { ProtocolHandlers, initProtocolImpl } from './protocol-impl';
|
|
|
|
const logger = new Logger('browser-extension-connector');
|
|
if (!localStorage.debugBrowserExtension) {
|
|
logger.level = Logger.Level.Info;
|
|
}
|
|
|
|
const connectedClients = new Map();
|
|
const pendingBrowserMessages = [];
|
|
let processingBrowserMessage = false;
|
|
const MaxIncomingDataLength = 10_000;
|
|
|
|
const BrowserExtensionConnector = {
|
|
enabled: true,
|
|
logger,
|
|
connectedClients,
|
|
|
|
init(appModel) {
|
|
const sendEvent = this.sendEvent.bind(this);
|
|
initProtocolImpl({ appModel, logger, connectedClients, sendEvent });
|
|
|
|
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
|
|
|
this.start();
|
|
},
|
|
|
|
start() {
|
|
if (Launcher) {
|
|
this.startDesktopAppListener();
|
|
} else {
|
|
this.startWebMessageListener();
|
|
}
|
|
|
|
logger.info('Started');
|
|
},
|
|
|
|
stop() {
|
|
if (Launcher) {
|
|
this.stopDesktopAppListener();
|
|
} else {
|
|
this.stopWebMessageListener();
|
|
}
|
|
|
|
logger.info('Stopped');
|
|
},
|
|
|
|
startWebMessageListener() {
|
|
window.addEventListener('message', this.browserWindowMessage);
|
|
},
|
|
|
|
stopWebMessageListener() {
|
|
window.removeEventListener('message', this.browserWindowMessage);
|
|
},
|
|
|
|
isSocketNameTooLong(socketName) {
|
|
const maxLength = process.platform === 'win32' ? 256 : 104;
|
|
return socketName.length > maxLength;
|
|
},
|
|
|
|
startDesktopAppListener() {
|
|
Launcher.prepareBrowserExtensionSocket(() => {
|
|
const sockName = Launcher.getBrowserExtensionSocketName();
|
|
if (this.isSocketNameTooLong(sockName)) {
|
|
logger.error(
|
|
"Socket name is too big, browser connection won't be possible, probably OS username is very long.",
|
|
sockName
|
|
);
|
|
return;
|
|
}
|
|
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 (!this.enabled) {
|
|
return;
|
|
}
|
|
if (Launcher) {
|
|
this.sendSocketEvent(data);
|
|
} else {
|
|
this.sendWebResponse(data);
|
|
}
|
|
}
|
|
};
|
|
|
|
export { BrowserExtensionConnector };
|