diff --git a/app/scripts/comp/extension/browser-extension-connector.js b/app/scripts/comp/extension/browser-extension-connector.js index f1f01602..a1413e5b 100644 --- a/app/scripts/comp/extension/browser-extension-connector.js +++ b/app/scripts/comp/extension/browser-extension-connector.js @@ -1,29 +1,51 @@ -import kdbxweb from 'kdbxweb'; import { Launcher } from 'comp/launcher'; 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'); if (!localStorage.debugBrowserExtension) { logger.level = Logger.Level.Info; } -const connectedClients = new Map(); +const WebConnectionInfo = { + connectionId: 1, + extensionName: 'keeweb-connect', + supportsNotifications: true +}; + +const connections = 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 }); + initProtocolImpl({ appModel, logger, sendEvent }); 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(); }, @@ -33,8 +55,6 @@ const BrowserExtensionConnector = { } else { this.startWebMessageListener(); } - - logger.info('Started'); }, stop() { @@ -44,189 +64,31 @@ const BrowserExtensionConnector = { this.stopWebMessageListener(); } + cleanupProtocolImpl(); + connections.clear(); + logger.info('Stopped'); }, startWebMessageListener() { window.addEventListener('message', this.browserWindowMessage); + logger.info('Started'); }, 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); + async startDesktopAppListener() { + const { ipcRenderer } = Launcher.electron(); + ipcRenderer.invoke('browserExtensionConnectorStart', { + appleTeamId: RuntimeInfo.appleTeamId }); }, 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; - 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); + const { ipcRenderer } = Launcher.electron(); + ipcRenderer.invoke('browserExtensionConnectorStop'); }, browserWindowMessage(e) { @@ -259,7 +121,7 @@ const BrowserExtensionConnector = { if (!handler) { throw new Error(`Handler not found: ${request.action}`); } - response = await handler(request); + response = await handler(request, WebConnectionInfo); } catch (e) { response = this.errorToResponse(e, request); } @@ -275,7 +137,7 @@ const BrowserExtensionConnector = { errorToResponse(e, request) { return { - action: request.action, + action: request?.action, error: e.message || 'Unknown error', errorCode: e.code || 0 }; @@ -287,27 +149,14 @@ const BrowserExtensionConnector = { 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) { + const { ipcRenderer } = Launcher.electron(); + ipcRenderer.invoke('browserExtensionConnectorSocketEvent', data); }, - sendSocketEvent(data) { - for (const socket of this.connectedSockets) { - const state = this.connectedSocketState.get(socket); - if (state?.active && state.notifications) { - const client = this.connectedClients.get(state.clientId); - if (client?.supportsNotifications) { - this.sendSocketResponse(socket, data); - } - } - } + sendSocketResult(socketId, data) { + const { ipcRenderer } = Launcher.electron(); + ipcRenderer.invoke('browserExtensionConnectorSocketResult', socketId, data); }, sendEvent(data) { @@ -319,6 +168,33 @@ const BrowserExtensionConnector = { } else { 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); } }; diff --git a/app/scripts/comp/extension/protocol-impl.js b/app/scripts/comp/extension/protocol-impl.js index ad7e3c84..2ffc013a 100644 --- a/app/scripts/comp/extension/protocol-impl.js +++ b/app/scripts/comp/extension/protocol-impl.js @@ -23,20 +23,32 @@ const Errors = { } }; +const connectedClients = new Map(); + let logger; let appModel; -let connectedClients; let sendEvent; function initProtocolImpl(vars) { appModel = vars.appModel; logger = vars.logger; - connectedClients = vars.connectedClients; sendEvent = vars.sendEvent; setupListeners(); } +function cleanupProtocolImpl() { + connectedClients.clear(); +} + +function deleteProtocolImplConnection(connectionId) { + for (const client of connectedClients.values()) { + if (client.connection.connectionId === connectionId) { + connectedClients.delete(client); + } + } +} + function setupListeners() { Events.on('file-opened', () => { sendEvent({ action: 'database-unlocked' }); @@ -189,12 +201,12 @@ function checkContentRequestPermissions(request) { } function getVersion(request) { - const extensionName = getClient(request).extensionName; + const extensionName = getClient(request).connection.extensionName; return extensionName ? RuntimeInfo.version : KnownAppVersions.KeePassXC; } function isKeeWebConnect(request) { - return getClient(request).extensionName === 'keeweb-connect'; + return getClient(request).connection.extensionName === 'keeweb-connect'; } function focusKeeWeb() { @@ -211,8 +223,8 @@ const ProtocolHandlers = { return { data }; }, - 'change-public-keys'(request) { - let { publicKey, extensionName, version, clientID: clientId } = request; + 'change-public-keys'(request, connection) { + let { publicKey, version, clientID: clientId } = request; if (connectedClients.has(clientId)) { throw new Error('Changing keys is not allowed'); @@ -226,9 +238,9 @@ const ProtocolHandlers = { const keys = tweetnaclBox.keyPair(); 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); incrementNonce(nonceBytes); @@ -394,4 +406,4 @@ const ProtocolHandlers = { } }; -export { ProtocolHandlers, initProtocolImpl }; +export { ProtocolHandlers, initProtocolImpl, cleanupProtocolImpl, deleteProtocolImplConnection }; diff --git a/app/scripts/comp/launcher/launcher-electron.js b/app/scripts/comp/launcher/launcher-electron.js index b76d01bc..f1c924fe 100644 --- a/app/scripts/comp/launcher/launcher-electron.js +++ b/app/scripts/comp/launcher/launcher-electron.js @@ -288,36 +288,6 @@ const Launcher = { }, mainWindowMaximized() { 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-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0)); 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); if (window.launcherOpenedFile) { diff --git a/app/scripts/comp/launcher/native-modules.js b/app/scripts/comp/launcher/native-modules.js index 9bee3360..85406b4b 100644 --- a/app/scripts/comp/launcher/native-modules.js +++ b/app/scripts/comp/launcher/native-modules.js @@ -20,6 +20,7 @@ if (Launcher) { ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err)); ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig)); ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect()); + ipcRenderer.on('log', (e, ...args) => NativeModules.log(...args)); const handlers = { yubikeys(numYubiKeys) { @@ -118,6 +119,14 @@ if (Launcher) { } }, + log(name, level, ...args) { + if (!name) { + return; + } + const logger = new Logger(name); + logger[level](...args); + }, + autoRestartHost() { setTimeout(() => { try { diff --git a/desktop/main.js b/desktop/main.js index 929830c3..0cac5200 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -9,10 +9,12 @@ const electron = require('electron'); const path = require('path'); const fs = require('fs'); const url = require('url'); +const { Logger } = require('./scripts/logger'); perfTimestamps?.push({ name: 'loading app requires', ts: process.hrtime() }); const main = electron.app; +const logger = new Logger('remote-app'); let mainWindow = null; let appIcon = null; @@ -154,14 +156,15 @@ main.on('second-instance', () => { } }); main.on('web-contents-created', (event, contents) => { - contents.on('new-window', async (e, url) => { - e.preventDefault(); - emitRemoteEvent('log', { message: `Prevented new window: ${url}` }); + contents.setWindowOpenHandler((e) => { + logger.warn(`Prevented new window: ${e.url}`); + emitRemoteEvent('log', `Prevented new window: ${e.url}`); + return { action: 'deny' }; }); contents.on('will-navigate', (e, url) => { if (!url.startsWith('https://beta.keeweb.info/') && !url.startsWith(htmlPath)) { e.preventDefault(); - emitRemoteEvent('log', { message: `Prevented navigation: ${url}` }); + logger.warn(`Prevented navigation: ${url}`); } }); }); diff --git a/desktop/scripts/ipc-handlers/browser-extension-connector.js b/desktop/scripts/ipc-handlers/browser-extension-connector.js index b09340e1..b8d0195f 100644 --- a/desktop/scripts/ipc-handlers/browser-extension-connector.js +++ b/desktop/scripts/ipc-handlers/browser-extension-connector.js @@ -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('browserExtensionConnectorStop', () => {}); +ipcMain.handle('browserExtensionConnectorStart', browserExtensionConnectorStart); +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); +} diff --git a/desktop/scripts/logger.js b/desktop/scripts/logger.js new file mode 100644 index 00000000..c1ad7094 --- /dev/null +++ b/desktop/scripts/logger.js @@ -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 };