mirror of https://github.com/keeweb/keeweb.git
moved browser socket connection management to the main process
This commit is contained in:
parent
e06e9ac1f8
commit
4280b84459
|
@ -1,29 +1,51 @@
|
||||||
import kdbxweb from 'kdbxweb';
|
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { ProtocolHandlers, initProtocolImpl } from './protocol-impl';
|
import {
|
||||||
|
ProtocolHandlers,
|
||||||
|
initProtocolImpl,
|
||||||
|
cleanupProtocolImpl,
|
||||||
|
deleteProtocolImplConnection
|
||||||
|
} from './protocol-impl';
|
||||||
|
import { RuntimeInfo } from 'const/runtime-info';
|
||||||
|
|
||||||
const logger = new Logger('browser-extension-connector');
|
const logger = new Logger('browser-extension-connector');
|
||||||
if (!localStorage.debugBrowserExtension) {
|
if (!localStorage.debugBrowserExtension) {
|
||||||
logger.level = Logger.Level.Info;
|
logger.level = Logger.Level.Info;
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedClients = new Map();
|
const WebConnectionInfo = {
|
||||||
|
connectionId: 1,
|
||||||
|
extensionName: 'keeweb-connect',
|
||||||
|
supportsNotifications: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const connections = new Map();
|
||||||
const pendingBrowserMessages = [];
|
const pendingBrowserMessages = [];
|
||||||
let processingBrowserMessage = false;
|
let processingBrowserMessage = false;
|
||||||
const MaxIncomingDataLength = 10_000;
|
|
||||||
|
|
||||||
const BrowserExtensionConnector = {
|
const BrowserExtensionConnector = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
logger,
|
logger,
|
||||||
connectedClients,
|
|
||||||
|
|
||||||
init(appModel) {
|
init(appModel) {
|
||||||
const sendEvent = this.sendEvent.bind(this);
|
const sendEvent = this.sendEvent.bind(this);
|
||||||
initProtocolImpl({ appModel, logger, connectedClients, sendEvent });
|
initProtocolImpl({ appModel, logger, sendEvent });
|
||||||
|
|
||||||
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
this.browserWindowMessage = this.browserWindowMessage.bind(this);
|
||||||
|
|
||||||
|
if (Launcher) {
|
||||||
|
const { ipcRenderer } = Launcher.electron();
|
||||||
|
ipcRenderer.on('browserExtensionSocketConnected', (e, socketId, connectionInfo) =>
|
||||||
|
this.socketConnected(socketId, connectionInfo)
|
||||||
|
);
|
||||||
|
ipcRenderer.on('browserExtensionSocketClosed', (e, socketId) =>
|
||||||
|
this.socketClosed(socketId)
|
||||||
|
);
|
||||||
|
ipcRenderer.on('browserExtensionSocketRequest', (e, socketId, request) =>
|
||||||
|
this.socketRequest(socketId, request)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.start();
|
this.start();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -33,8 +55,6 @@ const BrowserExtensionConnector = {
|
||||||
} else {
|
} else {
|
||||||
this.startWebMessageListener();
|
this.startWebMessageListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Started');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
@ -44,189 +64,31 @@ const BrowserExtensionConnector = {
|
||||||
this.stopWebMessageListener();
|
this.stopWebMessageListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupProtocolImpl();
|
||||||
|
connections.clear();
|
||||||
|
|
||||||
logger.info('Stopped');
|
logger.info('Stopped');
|
||||||
},
|
},
|
||||||
|
|
||||||
startWebMessageListener() {
|
startWebMessageListener() {
|
||||||
window.addEventListener('message', this.browserWindowMessage);
|
window.addEventListener('message', this.browserWindowMessage);
|
||||||
|
logger.info('Started');
|
||||||
},
|
},
|
||||||
|
|
||||||
stopWebMessageListener() {
|
stopWebMessageListener() {
|
||||||
window.removeEventListener('message', this.browserWindowMessage);
|
window.removeEventListener('message', this.browserWindowMessage);
|
||||||
},
|
},
|
||||||
|
|
||||||
isSocketNameTooLong(socketName) {
|
async startDesktopAppListener() {
|
||||||
const maxLength = process.platform === 'win32' ? 256 : 104;
|
const { ipcRenderer } = Launcher.electron();
|
||||||
return socketName.length > maxLength;
|
ipcRenderer.invoke('browserExtensionConnectorStart', {
|
||||||
},
|
appleTeamId: RuntimeInfo.appleTeamId
|
||||||
|
|
||||||
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() {
|
stopDesktopAppListener() {
|
||||||
for (const socket of this.connectedSockets) {
|
const { ipcRenderer } = Launcher.electron();
|
||||||
socket.destroy();
|
ipcRenderer.invoke('browserExtensionConnectorStop');
|
||||||
}
|
|
||||||
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;
|
|
||||||
state.supportsNotifications = true; // TODO: = !isSafari
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (!request) {
|
|
||||||
logger.warn('Empty request', request);
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.clientID) {
|
|
||||||
const clientId = request.clientID;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (request.action !== 'ping') {
|
|
||||||
logger.warn('Empty client ID in request', request);
|
|
||||||
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) {
|
browserWindowMessage(e) {
|
||||||
|
@ -259,7 +121,7 @@ const BrowserExtensionConnector = {
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
throw new Error(`Handler not found: ${request.action}`);
|
throw new Error(`Handler not found: ${request.action}`);
|
||||||
}
|
}
|
||||||
response = await handler(request);
|
response = await handler(request, WebConnectionInfo);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response = this.errorToResponse(e, request);
|
response = this.errorToResponse(e, request);
|
||||||
}
|
}
|
||||||
|
@ -275,7 +137,7 @@ const BrowserExtensionConnector = {
|
||||||
|
|
||||||
errorToResponse(e, request) {
|
errorToResponse(e, request) {
|
||||||
return {
|
return {
|
||||||
action: request.action,
|
action: request?.action,
|
||||||
error: e.message || 'Unknown error',
|
error: e.message || 'Unknown error',
|
||||||
errorCode: e.code || 0
|
errorCode: e.code || 0
|
||||||
};
|
};
|
||||||
|
@ -287,27 +149,14 @@ const BrowserExtensionConnector = {
|
||||||
postMessage(response, window.location.origin);
|
postMessage(response, window.location.origin);
|
||||||
},
|
},
|
||||||
|
|
||||||
sendSocketResponse(socket, response) {
|
sendSocketEvent(data) {
|
||||||
logger.debug('KeeWeb -> Extension', response);
|
const { ipcRenderer } = Launcher.electron();
|
||||||
const responseData = Buffer.from(JSON.stringify(response));
|
ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data);
|
||||||
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) {
|
sendSocketResult(socketId, data) {
|
||||||
for (const socket of this.connectedSockets) {
|
const { ipcRenderer } = Launcher.electron();
|
||||||
const state = this.connectedSocketState.get(socket);
|
ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data);
|
||||||
if (state?.active && state.notifications) {
|
|
||||||
const client = this.connectedClients.get(state.clientId);
|
|
||||||
if (client?.supportsNotifications) {
|
|
||||||
this.sendSocketResponse(socket, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sendEvent(data) {
|
sendEvent(data) {
|
||||||
|
@ -319,6 +168,33 @@ const BrowserExtensionConnector = {
|
||||||
} else {
|
} else {
|
||||||
this.sendWebResponse(data);
|
this.sendWebResponse(data);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
socketConnected(socketId, connectionInfo) {
|
||||||
|
connections.set(socketId, connectionInfo);
|
||||||
|
},
|
||||||
|
|
||||||
|
socketClosed(socketId) {
|
||||||
|
connections.delete(socketId);
|
||||||
|
deleteProtocolImplConnection(socketId);
|
||||||
|
},
|
||||||
|
|
||||||
|
async socketRequest(socketId, request) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
const connectionInfo = connections.get(socketId);
|
||||||
|
if (!connectionInfo) {
|
||||||
|
throw new Error(`Connection not found: ${socketId}`);
|
||||||
|
}
|
||||||
|
const handler = ProtocolHandlers[request.action];
|
||||||
|
if (!handler) {
|
||||||
|
throw new Error(`Handler not found: ${request.action}`);
|
||||||
|
}
|
||||||
|
result = await handler(request, connectionInfo);
|
||||||
|
} catch (e) {
|
||||||
|
result = this.errorToResponse(e, request);
|
||||||
|
}
|
||||||
|
this.sendSocketResult(socketId, result);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,20 +23,32 @@ const Errors = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connectedClients = new Map();
|
||||||
|
|
||||||
let logger;
|
let logger;
|
||||||
let appModel;
|
let appModel;
|
||||||
let connectedClients;
|
|
||||||
let sendEvent;
|
let sendEvent;
|
||||||
|
|
||||||
function initProtocolImpl(vars) {
|
function initProtocolImpl(vars) {
|
||||||
appModel = vars.appModel;
|
appModel = vars.appModel;
|
||||||
logger = vars.logger;
|
logger = vars.logger;
|
||||||
connectedClients = vars.connectedClients;
|
|
||||||
sendEvent = vars.sendEvent;
|
sendEvent = vars.sendEvent;
|
||||||
|
|
||||||
setupListeners();
|
setupListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupProtocolImpl() {
|
||||||
|
connectedClients.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteProtocolImplConnection(connectionId) {
|
||||||
|
for (const client of connectedClients.values()) {
|
||||||
|
if (client.connection.connectionId === connectionId) {
|
||||||
|
connectedClients.delete(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupListeners() {
|
function setupListeners() {
|
||||||
Events.on('file-opened', () => {
|
Events.on('file-opened', () => {
|
||||||
sendEvent({ action: 'database-unlocked' });
|
sendEvent({ action: 'database-unlocked' });
|
||||||
|
@ -189,12 +201,12 @@ function checkContentRequestPermissions(request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersion(request) {
|
function getVersion(request) {
|
||||||
const extensionName = getClient(request).extensionName;
|
const extensionName = getClient(request).connection.extensionName;
|
||||||
return extensionName ? RuntimeInfo.version : KnownAppVersions.KeePassXC;
|
return extensionName ? RuntimeInfo.version : KnownAppVersions.KeePassXC;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKeeWebConnect(request) {
|
function isKeeWebConnect(request) {
|
||||||
return getClient(request).extensionName === 'keeweb-connect';
|
return getClient(request).connection.extensionName === 'keeweb-connect';
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusKeeWeb() {
|
function focusKeeWeb() {
|
||||||
|
@ -211,8 +223,8 @@ const ProtocolHandlers = {
|
||||||
return { data };
|
return { data };
|
||||||
},
|
},
|
||||||
|
|
||||||
'change-public-keys'(request) {
|
'change-public-keys'(request, connection) {
|
||||||
let { publicKey, extensionName, version, clientID: clientId } = request;
|
let { publicKey, version, clientID: clientId } = request;
|
||||||
|
|
||||||
if (connectedClients.has(clientId)) {
|
if (connectedClients.has(clientId)) {
|
||||||
throw new Error('Changing keys is not allowed');
|
throw new Error('Changing keys is not allowed');
|
||||||
|
@ -226,9 +238,9 @@ const ProtocolHandlers = {
|
||||||
const keys = tweetnaclBox.keyPair();
|
const keys = tweetnaclBox.keyPair();
|
||||||
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
|
publicKey = kdbxweb.ByteUtils.base64ToBytes(publicKey);
|
||||||
|
|
||||||
connectedClients.set(clientId, { publicKey, extensionName, version, keys });
|
connectedClients.set(clientId, { connection, publicKey, version, keys });
|
||||||
|
|
||||||
logger.info('New client key created', clientId, extensionName, version);
|
logger.info('New client key created', clientId, version);
|
||||||
|
|
||||||
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
const nonceBytes = kdbxweb.ByteUtils.base64ToBytes(request.nonce);
|
||||||
incrementNonce(nonceBytes);
|
incrementNonce(nonceBytes);
|
||||||
|
@ -394,4 +406,4 @@ const ProtocolHandlers = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ProtocolHandlers, initProtocolImpl };
|
export { ProtocolHandlers, initProtocolImpl, cleanupProtocolImpl, deleteProtocolImplConnection };
|
||||||
|
|
|
@ -288,36 +288,6 @@ const Launcher = {
|
||||||
},
|
},
|
||||||
mainWindowMaximized() {
|
mainWindowMaximized() {
|
||||||
return this.getMainWindow().isMaximized();
|
return this.getMainWindow().isMaximized();
|
||||||
},
|
|
||||||
getBrowserExtensionSocketName() {
|
|
||||||
const { username, uid } = this.req('os').userInfo();
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
const teamId = RuntimeInfo.appleTeamId;
|
|
||||||
return `/Users/${username}/Library/Group Containers/${teamId}.keeweb/conn.sock`;
|
|
||||||
} else if (process.platform === 'win32') {
|
|
||||||
return `\\\\.\\pipe\\keeweb-connect-${username}`;
|
|
||||||
} else {
|
|
||||||
const sockFileName = `keeweb-connect-${uid}.sock`;
|
|
||||||
return this.joinPath(this.remoteApp().getPath('temp'), sockFileName);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepareBrowserExtensionSocket(done) {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
const sockName = this.getBrowserExtensionSocketName();
|
|
||||||
const fs = this.req('fs');
|
|
||||||
fs.access(sockName, fs.constants.F_OK, (err) => {
|
|
||||||
if (err) {
|
|
||||||
const dir = this.req('path').dirname(sockName);
|
|
||||||
fs.mkdir(dir, () => done());
|
|
||||||
} else {
|
|
||||||
fs.unlink(sockName, () => done());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (process.platform === 'win32') {
|
|
||||||
done();
|
|
||||||
} else {
|
|
||||||
this.deleteFile(this.getBrowserExtensionSocketName(), done);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -329,7 +299,6 @@ Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized
|
||||||
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
|
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
|
||||||
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
|
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
|
||||||
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
||||||
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
|
|
||||||
|
|
||||||
window.launcherOpen = (file) => Launcher.openFile(file);
|
window.launcherOpen = (file) => Launcher.openFile(file);
|
||||||
if (window.launcherOpenedFile) {
|
if (window.launcherOpenedFile) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ if (Launcher) {
|
||||||
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
|
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
|
||||||
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
|
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
|
||||||
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
|
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
|
||||||
|
ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args));
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
yubikeys(numYubiKeys) {
|
yubikeys(numYubiKeys) {
|
||||||
|
@ -118,6 +119,14 @@ if (Launcher) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
log(name, level, ...args) {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const logger = new Logger(name);
|
||||||
|
logger[level](...args);
|
||||||
|
},
|
||||||
|
|
||||||
autoRestartHost() {
|
autoRestartHost() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -9,10 +9,12 @@ const electron = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
const { Logger } = require('./scripts/logger');
|
||||||
|
|
||||||
perfTimestamps?.push({ name: 'loading app requires', ts: process.hrtime() });
|
perfTimestamps?.push({ name: 'loading app requires', ts: process.hrtime() });
|
||||||
|
|
||||||
const main = electron.app;
|
const main = electron.app;
|
||||||
|
const logger = new Logger('remote-app');
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let appIcon = null;
|
let appIcon = null;
|
||||||
|
@ -154,14 +156,15 @@ main.on('second-instance', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
main.on('web-contents-created', (event, contents) => {
|
main.on('web-contents-created', (event, contents) => {
|
||||||
contents.on('new-window', async (e, url) => {
|
contents.setWindowOpenHandler((e) => {
|
||||||
e.preventDefault();
|
logger.warn(`Prevented new window: ${e.url}`);
|
||||||
emitRemoteEvent('log', { message: `Prevented new window: ${url}` });
|
emitRemoteEvent('log', `Prevented new window: ${e.url}`);
|
||||||
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
contents.on('will-navigate', (e, url) => {
|
contents.on('will-navigate', (e, url) => {
|
||||||
if (!url.startsWith('https://beta.keeweb.info/') && !url.startsWith(htmlPath)) {
|
if (!url.startsWith('https://beta.keeweb.info/') && !url.startsWith(htmlPath)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
emitRemoteEvent('log', { message: `Prevented navigation: ${url}` });
|
logger.warn(`Prevented navigation: ${url}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,298 @@
|
||||||
const { ipcMain } = require('electron');
|
const os = require('os');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const net = require('net');
|
||||||
|
const { ipcMain, app } = require('electron');
|
||||||
|
const { Logger } = require('../logger');
|
||||||
|
|
||||||
ipcMain.handle('browserExtensionConnectorStart', () => {});
|
ipcMain.handle('browserExtensionConnectorStart', browserExtensionConnectorStart);
|
||||||
ipcMain.handle('browserExtensionConnectorStop', () => {});
|
ipcMain.handle('browserExtensionConnectorStop', browserExtensionConnectorStop);
|
||||||
|
ipcMain.handle('browserExtensionConnectorSocketResult', browserExtensionConnectorSocketResult);
|
||||||
|
ipcMain.handle('browserExtensionConnectorSocketEvent', browserExtensionConnectorSocketEvent);
|
||||||
|
|
||||||
|
const logger = new Logger('browser-extension-connector');
|
||||||
|
|
||||||
|
const MaxIncomingDataLength = 10_000;
|
||||||
|
|
||||||
|
let connectedSockets = new Map();
|
||||||
|
let connectedSocketState = new WeakMap();
|
||||||
|
let server;
|
||||||
|
let socketId = 0;
|
||||||
|
|
||||||
|
async function browserExtensionConnectorStart(e, config) {
|
||||||
|
await prepareBrowserExtensionSocket(config);
|
||||||
|
const sockName = getBrowserExtensionSocketName(config);
|
||||||
|
|
||||||
|
if (isSocketNameTooLong(sockName)) {
|
||||||
|
logger.error(
|
||||||
|
"Socket name is too long, browser connection won't be possible, probably OS username is very long.",
|
||||||
|
sockName
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = net.createServer((socket) => {
|
||||||
|
socketId++;
|
||||||
|
|
||||||
|
logger.info(`New connection with socket ${socketId}`);
|
||||||
|
|
||||||
|
connectedSockets.set(socketId, socket);
|
||||||
|
connectedSocketState.set(socket, { socketId });
|
||||||
|
|
||||||
|
checkSocketIdentity(socket);
|
||||||
|
|
||||||
|
socket.on('data', (data) => onSocketData(socket, data));
|
||||||
|
socket.on('close', () => onSocketClose(socket));
|
||||||
|
});
|
||||||
|
server.listen(sockName);
|
||||||
|
|
||||||
|
logger.info('Started');
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserExtensionConnectorStop() {
|
||||||
|
for (const socket of connectedSockets.values()) {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
if (server) {
|
||||||
|
server.close();
|
||||||
|
server = null;
|
||||||
|
}
|
||||||
|
connectedSockets = new Map();
|
||||||
|
connectedSocketState = new WeakMap();
|
||||||
|
logger.info('Stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserExtensionConnectorSocketResult(e, socketId, result) {
|
||||||
|
sendResultToSocket(socketId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserExtensionConnectorSocketEvent(e, data) {
|
||||||
|
sendEventToAllSockets(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowserExtensionSocketName(config) {
|
||||||
|
const { username, uid } = os.userInfo();
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const appleTeamId = config.appleTeamId;
|
||||||
|
return `/Users/${username}/Library/Group Containers/${appleTeamId}.keeweb/conn.sock`;
|
||||||
|
} else if (process.platform === 'win32') {
|
||||||
|
return `\\\\.\\pipe\\keeweb-connect-${username}`;
|
||||||
|
} else {
|
||||||
|
const sockFileName = `keeweb-connect-${uid}.sock`;
|
||||||
|
return path.join(app.getPath('temp'), sockFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareBrowserExtensionSocket(config) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const sockName = getBrowserExtensionSocketName(config);
|
||||||
|
fs.access(sockName, fs.constants.F_OK, (err) => {
|
||||||
|
if (err) {
|
||||||
|
const dir = path.dirname(sockName);
|
||||||
|
fs.mkdir(dir, () => resolve());
|
||||||
|
} else {
|
||||||
|
fs.unlink(sockName, () => resolve());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (process.platform === 'win32') {
|
||||||
|
return resolve();
|
||||||
|
} else {
|
||||||
|
fs.unlink(getBrowserExtensionSocketName(config), () => resolve());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSocketNameTooLong(socketName) {
|
||||||
|
const maxLength = process.platform === 'win32' ? 256 : 104;
|
||||||
|
return socketName.length > maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSocketIdentity(socket) {
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement this
|
||||||
|
|
||||||
|
state.active = true;
|
||||||
|
state.appName = 'TODO';
|
||||||
|
state.extensionName = 'TODO';
|
||||||
|
state.pid = 0;
|
||||||
|
state.supportsNotifications = state.appName !== 'Safari';
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Socket ${state.socketId} activated`,
|
||||||
|
`app: ${state.appName}`,
|
||||||
|
`extension: ${state.extensionName}`,
|
||||||
|
`pid: ${state.pid}`
|
||||||
|
);
|
||||||
|
|
||||||
|
sendToRenderer('browserExtensionSocketConnected', state.socketId, {
|
||||||
|
connectionId: state.socketId,
|
||||||
|
appName: state.appName,
|
||||||
|
extensionName: state.extensionName,
|
||||||
|
pid: state.pid,
|
||||||
|
supportsNotifications: state.supportsNotifications
|
||||||
|
});
|
||||||
|
|
||||||
|
processPendingSocketData(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSocketClose(socket) {
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
connectedSocketState.delete(socket);
|
||||||
|
|
||||||
|
if (state?.socketId) {
|
||||||
|
connectedSockets.delete(state.socketId);
|
||||||
|
sendToRenderer('browserExtensionSocketClosed', state.socketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Socket ${state?.socketId} closed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSocketData(socket, data) {
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
if (!state) {
|
||||||
|
logger.warn('Received data without connection state');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.byteLength > MaxIncomingDataLength) {
|
||||||
|
logger.warn(`Too many bytes rejected from socket ${state.socketId}`, data.byteLength);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.pendingData) {
|
||||||
|
state.pendingData = Buffer.concat([state.pendingData, data]);
|
||||||
|
} else {
|
||||||
|
state.pendingData = data;
|
||||||
|
}
|
||||||
|
processPendingSocketData(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processPendingSocketData(socket) {
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
if (!state?.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.pendingData || state.processingData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.pendingData.length < 4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lengthBuffer = state.pendingData.buffer.slice(
|
||||||
|
state.pendingData.byteOffset,
|
||||||
|
state.pendingData.byteOffset + 4
|
||||||
|
);
|
||||||
|
const length = new Uint32Array(lengthBuffer)[0];
|
||||||
|
|
||||||
|
if (length > MaxIncomingDataLength) {
|
||||||
|
logger.warn(`Large message rejected from socket ${state.socketId}`, 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 from socket ${state.socketId}`, str);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Extension[${state.socketId}] -> KeeWeb`, request);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
logger.warn(`Empty request for socket ${state.socketId}`, request);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.clientID) {
|
||||||
|
const clientId = request.clientID;
|
||||||
|
if (!state.clientId) {
|
||||||
|
state.clientId = clientId;
|
||||||
|
} else if (state.clientId !== clientId) {
|
||||||
|
logger.warn(
|
||||||
|
`Changing client ID for socket ${state.socketId} is not allowed`,
|
||||||
|
`${state.clientId} => ${clientId}`
|
||||||
|
);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (request.action !== 'ping') {
|
||||||
|
logger.warn(`Empty client ID in socket request ${state.socketId}`, request);
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.processingData = true;
|
||||||
|
|
||||||
|
sendToRenderer('browserExtensionSocketRequest', state.socketId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendResultToSocket(socketId, result) {
|
||||||
|
const socket = connectedSockets.get(socketId);
|
||||||
|
if (socket) {
|
||||||
|
sendMessageToSocket(socket, result);
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
if (state.processingData) {
|
||||||
|
state.processingData = false;
|
||||||
|
processPendingSocketData(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendEventToAllSockets(data) {
|
||||||
|
for (const socket of connectedSockets.values()) {
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
if (state?.active && state?.supportsNotifications) {
|
||||||
|
sendMessageToSocket(socket, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessageToSocket(socket, message) {
|
||||||
|
const state = connectedSocketState.get(socket);
|
||||||
|
if (!state) {
|
||||||
|
logger.warn('Ignoring a socket message without connection state');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.active) {
|
||||||
|
logger.warn(`Ignoring a message to inactive socket ${state.socketId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`KeeWeb -> Extension[${state.socketId}]`, message);
|
||||||
|
|
||||||
|
const responseData = Buffer.from(JSON.stringify(message));
|
||||||
|
const lengthBuf = Buffer.from(new Uint32Array([responseData.byteLength]).buffer);
|
||||||
|
const lengthBytes = Buffer.from(lengthBuf);
|
||||||
|
const data = Buffer.concat([lengthBytes, responseData]);
|
||||||
|
|
||||||
|
socket.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToRenderer(event, socketId, data) {
|
||||||
|
app.getMainWindow().webContents.send(event, socketId, data);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
const { app } = require('electron');
|
||||||
|
|
||||||
|
function log(name, level, ...args) {
|
||||||
|
const mainWindow = app.getMainWindow();
|
||||||
|
mainWindow.webContents.send('log', name, level, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(...args) {
|
||||||
|
log(this.name, 'debug', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(...args) {
|
||||||
|
log(this.name, 'info', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(...args) {
|
||||||
|
log(this.name, 'warn', ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(...args) {
|
||||||
|
log(this.name, 'error', ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Logger };
|
Loading…
Reference in New Issue