mirror of https://github.com/keeweb/keeweb.git
moved native modules to a native module host
This commit is contained in:
parent
c1d354a6fb
commit
082c6bee93
|
@ -41,6 +41,7 @@ const ChalRespCalculator = {
|
||||||
complete() {
|
complete() {
|
||||||
const err = new Error(Locale.yubiKeyDisabledErrorHeader);
|
const err = new Error(Locale.yubiKeyDisabledErrorHeader);
|
||||||
err.userCanceled = true;
|
err.userCanceled = true;
|
||||||
|
err.ykError = true;
|
||||||
|
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
@ -158,6 +159,7 @@ const ChalRespCalculator = {
|
||||||
|
|
||||||
const err = new Error('User canceled the YubiKey no key prompt');
|
const err = new Error('User canceled the YubiKey no key prompt');
|
||||||
err.userCanceled = true;
|
err.userCanceled = true;
|
||||||
|
err.ykError = true;
|
||||||
|
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
@ -177,6 +179,7 @@ const ChalRespCalculator = {
|
||||||
|
|
||||||
const err = new Error('User canceled the YubiKey touch prompt');
|
const err = new Error('User canceled the YubiKey touch prompt');
|
||||||
err.userCanceled = true;
|
err.userCanceled = true;
|
||||||
|
err.ykError = true;
|
||||||
|
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
import { Events } from 'framework/events';
|
import { Events } from 'framework/events';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
import { YubiKeyVendorId } from 'const/hardware';
|
|
||||||
import { Features } from 'util/features';
|
import { Features } from 'util/features';
|
||||||
|
|
||||||
const logger = new Logger('usb-listener');
|
const logger = new Logger('usb-listener');
|
||||||
|
|
||||||
const UsbListener = {
|
const UsbListener = {
|
||||||
supported: Features.isDesktop,
|
supported: Features.isDesktop,
|
||||||
attachedYubiKeys: [],
|
attachedYubiKeys: 0,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (!this.supported) {
|
if (!this.supported) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Events.on('native-modules-yubikeys', (e) => {
|
||||||
|
if (e.numYubiKeys !== this.attachedYubiKeys) {
|
||||||
|
logger.debug(`YubiKeys changed ${this.attachedYubiKeys} => ${e.numYubiKeys}`);
|
||||||
|
this.attachedYubiKeys = e.numYubiKeys;
|
||||||
|
Events.emit('usb-devices-changed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
AppSettingsModel.on('change:enableUsb', (model, enabled) => {
|
AppSettingsModel.on('change:enableUsb', (model, enabled) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.start();
|
this.start();
|
||||||
|
@ -37,73 +44,25 @@ const UsbListener = {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ts = logger.ts();
|
NativeModules.startUsbListener();
|
||||||
|
|
||||||
this.usb = Launcher.reqNative('usb');
|
|
||||||
|
|
||||||
this.listen();
|
|
||||||
|
|
||||||
this.attachedYubiKeys = this.usb
|
|
||||||
.getDeviceList()
|
|
||||||
.filter(this.isYubiKey)
|
|
||||||
.map((device) => ({ device }));
|
|
||||||
|
|
||||||
if (this.attachedYubiKeys.length > 0) {
|
|
||||||
logger.info(`${this.attachedYubiKeys.length} YubiKey(s) found`, logger.ts(ts));
|
|
||||||
Events.emit('usb-devices-changed');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error loading USB module', e);
|
logger.error('Error starting USB listener', e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
logger.info('Stopping USB listener');
|
logger.info('Stopping USB listener');
|
||||||
|
|
||||||
if (this.usb) {
|
try {
|
||||||
if (this.attachedYubiKeys.length) {
|
NativeModules.stopUsbListener();
|
||||||
this.attachedYubiKeys = [];
|
} catch (e) {
|
||||||
Events.emit('usb-devices-changed');
|
logger.error('Error stopping USB listener', e);
|
||||||
}
|
|
||||||
|
|
||||||
this.usb.off('attach', UsbListener.deviceAttached);
|
|
||||||
this.usb.off('detach', UsbListener.deviceDetached);
|
|
||||||
|
|
||||||
this.usb = null;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
listen() {
|
if (this.attachedYubiKeys) {
|
||||||
this.usb.on('attach', UsbListener.deviceAttached);
|
this.attachedYubiKeys = 0;
|
||||||
this.usb.on('detach', UsbListener.deviceDetached);
|
|
||||||
},
|
|
||||||
|
|
||||||
deviceAttached(device) {
|
|
||||||
if (UsbListener.isYubiKey(device)) {
|
|
||||||
UsbListener.attachedYubiKeys.push({ device });
|
|
||||||
logger.info(`YubiKey attached, total: ${UsbListener.attachedYubiKeys.length}`, device);
|
|
||||||
Events.emit('usb-devices-changed');
|
Events.emit('usb-devices-changed');
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
deviceDetached(device) {
|
|
||||||
if (UsbListener.isYubiKey(device)) {
|
|
||||||
const index = UsbListener.attachedYubiKeys.findIndex(
|
|
||||||
(yk) => yk.device.deviceAddress === device.deviceAddress
|
|
||||||
);
|
|
||||||
if (index >= 0) {
|
|
||||||
UsbListener.attachedYubiKeys.splice(index, 1);
|
|
||||||
logger.info(
|
|
||||||
`YubiKey detached, total: ${UsbListener.attachedYubiKeys.length}`,
|
|
||||||
device
|
|
||||||
);
|
|
||||||
Events.emit('usb-devices-changed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isYubiKey(device) {
|
|
||||||
return device.deviceDescriptor.idVendor === YubiKeyVendorId;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Events } from 'framework/events';
|
import { Events } from 'framework/events';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { UsbListener } from 'comp/app/usb-listener';
|
import { UsbListener } from 'comp/app/usb-listener';
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
|
@ -14,13 +15,6 @@ const YubiKey = {
|
||||||
process: null,
|
process: null,
|
||||||
aborted: false,
|
aborted: false,
|
||||||
|
|
||||||
get ykChalResp() {
|
|
||||||
if (!this._ykChalResp) {
|
|
||||||
this._ykChalResp = Launcher.reqNative('yubikey-chalresp');
|
|
||||||
}
|
|
||||||
return this._ykChalResp;
|
|
||||||
},
|
|
||||||
|
|
||||||
cmd() {
|
cmd() {
|
||||||
if (this._cmd) {
|
if (this._cmd) {
|
||||||
return this._cmd;
|
return this._cmd;
|
||||||
|
@ -70,21 +64,20 @@ const YubiKey = {
|
||||||
},
|
},
|
||||||
|
|
||||||
list(callback) {
|
list(callback) {
|
||||||
this.ykChalResp.getYubiKeys({}, (err, yubiKeys) => {
|
NativeModules.getYubiKeys({})
|
||||||
if (err) {
|
.then((yubiKeys) => {
|
||||||
return callback(err);
|
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
|
||||||
}
|
return {
|
||||||
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
|
vid,
|
||||||
return {
|
pid,
|
||||||
vid,
|
serial,
|
||||||
pid,
|
slots,
|
||||||
serial,
|
fullName: this.getKeyFullName(pid, version, serial)
|
||||||
slots,
|
};
|
||||||
fullName: this.getKeyFullName(pid, version, serial)
|
});
|
||||||
};
|
callback(null, yubiKeys);
|
||||||
});
|
})
|
||||||
callback(null, yubiKeys);
|
.catch(callback);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getKeyFullName(pid, version, serial) {
|
getKeyFullName(pid, version, serial) {
|
||||||
|
@ -113,7 +106,7 @@ const YubiKey = {
|
||||||
|
|
||||||
logger.info('Listing YubiKeys');
|
logger.info('Listing YubiKeys');
|
||||||
|
|
||||||
if (UsbListener.attachedYubiKeys.length === 0) {
|
if (!UsbListener.attachedYubiKeys) {
|
||||||
return callback(null, []);
|
return callback(null, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,9 +164,9 @@ const YubiKey = {
|
||||||
logger.info('Repairing a stuck YubiKey');
|
logger.info('Repairing a stuck YubiKey');
|
||||||
|
|
||||||
let openTimeout;
|
let openTimeout;
|
||||||
const countYubiKeys = UsbListener.attachedYubiKeys.length;
|
const countYubiKeys = UsbListener.attachedYubiKeys;
|
||||||
const onDevicesChangedDuringRepair = () => {
|
const onDevicesChangedDuringRepair = () => {
|
||||||
if (UsbListener.attachedYubiKeys.length === countYubiKeys) {
|
if (UsbListener.attachedYubiKeys === countYubiKeys) {
|
||||||
logger.info('YubiKey was reconnected');
|
logger.info('YubiKey was reconnected');
|
||||||
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
|
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
|
||||||
clearTimeout(openTimeout);
|
clearTimeout(openTimeout);
|
||||||
|
@ -274,22 +267,24 @@ const YubiKey = {
|
||||||
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
|
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
|
||||||
challenge.copy(paddedChallenge);
|
challenge.copy(paddedChallenge);
|
||||||
|
|
||||||
this.ykChalResp.challengeResponse(yubiKey, paddedChallenge, slot, (err, response) => {
|
NativeModules.yubiKeyChallengeResponse(
|
||||||
if (err) {
|
yubiKey,
|
||||||
if (err.code === this.ykChalResp.YK_ENOKEY) {
|
[...paddedChallenge],
|
||||||
err.noKey = true;
|
slot,
|
||||||
|
(err, result) => {
|
||||||
|
if (result) {
|
||||||
|
result = Buffer.from(result);
|
||||||
}
|
}
|
||||||
if (err.code === this.ykChalResp.YK_ETIMEOUT) {
|
if (err) {
|
||||||
err.timeout = true;
|
err.ykError = true;
|
||||||
}
|
}
|
||||||
return callback(err);
|
return callback(err, result);
|
||||||
}
|
}
|
||||||
callback(null, response);
|
);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelChalResp() {
|
cancelChalResp() {
|
||||||
this.ykChalResp.cancelChallengeResponse();
|
NativeModules.yubiKeyCancelChallengeResponse();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,6 @@ const Launcher = {
|
||||||
remReq(mod) {
|
remReq(mod) {
|
||||||
return this.electron().remote.require(mod);
|
return this.electron().remote.require(mod);
|
||||||
},
|
},
|
||||||
reqNative(mod) {
|
|
||||||
return this.electron().remote.app.reqNative(mod);
|
|
||||||
},
|
|
||||||
openLink(href) {
|
openLink(href) {
|
||||||
if (/^(http|https|ftp|sftp|mailto):/i.test(href)) {
|
if (/^(http|https|ftp|sftp|mailto):/i.test(href)) {
|
||||||
this.electron().shell.openExternal(href);
|
this.electron().shell.openExternal(href);
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { Events } from 'framework/events';
|
||||||
|
import { Logger } from 'util/logger';
|
||||||
|
import { Launcher } from 'comp/launcher';
|
||||||
|
import { Timeouts } from 'const/timeouts';
|
||||||
|
|
||||||
|
let NativeModules;
|
||||||
|
|
||||||
|
if (Launcher) {
|
||||||
|
const logger = new Logger('native-module-connector');
|
||||||
|
|
||||||
|
let host;
|
||||||
|
let callId = 0;
|
||||||
|
let promises = {};
|
||||||
|
let ykChalRespCallbacks = {};
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
yubikeys(numYubiKeys) {
|
||||||
|
Events.emit('native-modules-yubikeys', { numYubiKeys });
|
||||||
|
},
|
||||||
|
|
||||||
|
log(...args) {
|
||||||
|
logger.info('Message from host', ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
result({ callId, result, error }) {
|
||||||
|
const promise = promises[callId];
|
||||||
|
if (promise) {
|
||||||
|
delete promises[callId];
|
||||||
|
if (error) {
|
||||||
|
logger.error('Received an error', promise.cmd, error);
|
||||||
|
promise.reject(error);
|
||||||
|
} else {
|
||||||
|
promise.resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'yk-chal-resp-result'({ callbackId, error, result }) {
|
||||||
|
const callback = ykChalRespCallbacks[callbackId];
|
||||||
|
if (callback) {
|
||||||
|
const willBeCalledAgain = error && error.touchRequested;
|
||||||
|
if (!willBeCalledAgain) {
|
||||||
|
delete ykChalRespCallbacks[callbackId];
|
||||||
|
}
|
||||||
|
callback(error, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NativeModules = {
|
||||||
|
startHost() {
|
||||||
|
if (host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Starting native module host');
|
||||||
|
|
||||||
|
const path = Launcher.req('path');
|
||||||
|
const appContentRoot = Launcher.remoteApp().getAppContentRoot();
|
||||||
|
const mainModulePath = path.join(appContentRoot, 'native-module-host.js');
|
||||||
|
|
||||||
|
const { fork } = Launcher.req('child_process');
|
||||||
|
|
||||||
|
host = fork(mainModulePath);
|
||||||
|
|
||||||
|
host.on('message', (message) => this.hostCallback(message));
|
||||||
|
|
||||||
|
host.on('error', (e) => this.hostError(e));
|
||||||
|
host.on('exit', (code, sig) => this.hostExit(code, sig));
|
||||||
|
|
||||||
|
this.call('init', Launcher.remoteApp().getAppMainRoot());
|
||||||
|
|
||||||
|
if (this.usbListenerRunning) {
|
||||||
|
this.call('start-usb');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hostError(e) {
|
||||||
|
logger.error('Host error', e);
|
||||||
|
},
|
||||||
|
|
||||||
|
hostExit(code, sig) {
|
||||||
|
logger.error(`Host exited with code ${code} and signal ${sig}`);
|
||||||
|
host = null;
|
||||||
|
|
||||||
|
const err = new Error('Native module host crashed');
|
||||||
|
|
||||||
|
for (const promise of Object.values(promises)) {
|
||||||
|
promise.reject(err);
|
||||||
|
}
|
||||||
|
promises = {};
|
||||||
|
|
||||||
|
for (const callback of Object.values(ykChalRespCallbacks)) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
ykChalRespCallbacks = {};
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
this.autoRestartHost();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hostCallback(message) {
|
||||||
|
const { cmd, args } = message;
|
||||||
|
// logger.debug('Callback', cmd, args);
|
||||||
|
if (handlers[cmd]) {
|
||||||
|
handlers[cmd](...args);
|
||||||
|
} else {
|
||||||
|
logger.error('No callback', cmd);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
autoRestartHost() {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
this.startHost();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Native module host failed to auto-restart', e);
|
||||||
|
}
|
||||||
|
}, Timeouts.NativeModuleHostRestartTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
call(cmd, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!host) {
|
||||||
|
try {
|
||||||
|
this.startHost();
|
||||||
|
} catch (e) {
|
||||||
|
return reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callId++;
|
||||||
|
if (callId === Number.MAX_SAFE_INTEGER) {
|
||||||
|
callId = 1;
|
||||||
|
}
|
||||||
|
// logger.debug('Call', cmd, args, callId);
|
||||||
|
promises[callId] = { cmd, resolve, reject };
|
||||||
|
host.send({ cmd, args, callId });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
startUsbListener() {
|
||||||
|
this.call('start-usb');
|
||||||
|
this.usbListenerRunning = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
stopUsbListener() {
|
||||||
|
this.usbListenerRunning = false;
|
||||||
|
if (host) {
|
||||||
|
this.call('stop-usb');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getYubiKeys(config) {
|
||||||
|
return this.call('get-yubikeys', config);
|
||||||
|
},
|
||||||
|
|
||||||
|
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
||||||
|
ykChalRespCallbacks[callId] = callback;
|
||||||
|
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId);
|
||||||
|
},
|
||||||
|
|
||||||
|
yubiKeyCancelChallengeResponse() {
|
||||||
|
if (host) {
|
||||||
|
this.call('yk-cancel-chal-resp');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
argon2(password, salt, options) {
|
||||||
|
return this.call('argon2', password, salt, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NativeModules };
|
|
@ -15,7 +15,8 @@ const Timeouts = {
|
||||||
DefaultHttpRequest: 60000,
|
DefaultHttpRequest: 60000,
|
||||||
ExternalDeviceReconnect: 3000,
|
ExternalDeviceReconnect: 3000,
|
||||||
ExternalDeviceAfterReconnect: 1000,
|
ExternalDeviceAfterReconnect: 1000,
|
||||||
FieldLabelDoubleClick: 300
|
FieldLabelDoubleClick: 300,
|
||||||
|
NativeModuleHostRestartTime: 3000
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Timeouts };
|
export { Timeouts };
|
||||||
|
|
|
@ -532,7 +532,7 @@ class AppModel {
|
||||||
params,
|
params,
|
||||||
(err, file) => {
|
(err, file) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.name === 'KdbxError' || err.userCanceled) {
|
if (err.name === 'KdbxError' || err.ykError) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -558,7 +558,7 @@ class AppModel {
|
||||||
setTimeout(() => this.syncFile(file), 0);
|
setTimeout(() => this.syncFile(file), 0);
|
||||||
callback(err);
|
callback(err);
|
||||||
} else {
|
} else {
|
||||||
if (err.name === 'KdbxError' || err.userCanceled) {
|
if (err.name === 'KdbxError' || err.ykError) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -1239,13 +1239,13 @@ class AppModel {
|
||||||
usbDevicesChanged() {
|
usbDevicesChanged() {
|
||||||
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
||||||
|
|
||||||
this.attachedYubiKeysCount = UsbListener.attachedYubiKeys.length;
|
this.attachedYubiKeysCount = UsbListener.attachedYubiKeys;
|
||||||
|
|
||||||
if (!this.settings.yubiKeyAutoOpen) {
|
if (!this.settings.yubiKeyAutoOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNewYubiKey = UsbListener.attachedYubiKeys.length > attachedYubiKeysCount;
|
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
|
||||||
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
|
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
|
||||||
|
|
||||||
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
|
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
onUsbDevicesChanged = () => {
|
onUsbDevicesChanged = () => {
|
||||||
if (UsbListener.attachedYubiKeys.length === 0) {
|
if (UsbListener.attachedYubiKeys === 0) {
|
||||||
this.emit('ejected');
|
this.emit('ejected');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import kdbxweb from 'kdbxweb';
|
import kdbxweb from 'kdbxweb';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { Features } from 'util/features';
|
import { Features } from 'util/features';
|
||||||
import { Launcher } from 'comp/launcher';
|
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
|
|
||||||
const logger = new Logger('argon2');
|
const logger = new Logger('argon2');
|
||||||
|
|
||||||
|
@ -29,36 +29,69 @@ const KdbxwebInit = {
|
||||||
if (!global.WebAssembly) {
|
if (!global.WebAssembly) {
|
||||||
return Promise.reject('WebAssembly is not supported');
|
return Promise.reject('WebAssembly is not supported');
|
||||||
}
|
}
|
||||||
if (Launcher && AppSettingsModel.nativeArgon2) {
|
if (Features.isDesktop && AppSettingsModel.nativeArgon2) {
|
||||||
const ts = logger.ts();
|
logger.debug('Using native argon2');
|
||||||
const argon2 = Launcher.reqNative('argon2');
|
|
||||||
logger.debug('Native argon2 runtime loaded (main thread)', logger.ts(ts));
|
|
||||||
this.runtimeModule = {
|
this.runtimeModule = {
|
||||||
hash(args) {
|
hash(args) {
|
||||||
return new Promise((resolve, reject) => {
|
const ts = logger.ts();
|
||||||
const ts = logger.ts();
|
|
||||||
argon2.hash(
|
const password = makeXoredValue(args.password);
|
||||||
Buffer.from(args.password),
|
const salt = makeXoredValue(args.salt);
|
||||||
Buffer.from(args.salt),
|
|
||||||
{
|
return NativeModules.argon2(password, salt, {
|
||||||
type: args.type,
|
type: args.type,
|
||||||
version: args.version,
|
version: args.version,
|
||||||
hashLength: args.length,
|
hashLength: args.length,
|
||||||
saltLength: args.salt.length,
|
saltLength: args.salt.length,
|
||||||
timeCost: args.iterations,
|
timeCost: args.iterations,
|
||||||
parallelism: args.parallelism,
|
parallelism: args.parallelism,
|
||||||
memoryCost: args.memory
|
memoryCost: args.memory
|
||||||
},
|
})
|
||||||
(err, res) => {
|
.then((res) => {
|
||||||
if (err) {
|
password.data.fill(0);
|
||||||
logger.error('Argon2 error', err);
|
salt.data.fill(0);
|
||||||
return reject(err);
|
|
||||||
}
|
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
|
||||||
resolve(res);
|
return readXoredValue(res);
|
||||||
}
|
})
|
||||||
);
|
.catch((err) => {
|
||||||
});
|
password.data.fill(0);
|
||||||
|
salt.data.fill(0);
|
||||||
|
|
||||||
|
logger.error('Argon2 error', err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeXoredValue(val) {
|
||||||
|
const data = Buffer.from(val);
|
||||||
|
const random = Buffer.from(kdbxweb.Random.getBytes(data.length));
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] ^= random[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { data: [...data], random: [...random] };
|
||||||
|
|
||||||
|
data.fill(0);
|
||||||
|
random.fill(0);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readXoredValue(val) {
|
||||||
|
const data = Buffer.from(val.data);
|
||||||
|
const random = Buffer.from(val.random);
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] ^= random[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
val.data.fill(0);
|
||||||
|
val.random.fill(0);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Promise.resolve(this.runtimeModule);
|
return Promise.resolve(this.runtimeModule);
|
||||||
|
|
|
@ -106,7 +106,7 @@ class OpenView extends View {
|
||||||
!this.model.settings.canOpen &&
|
!this.model.settings.canOpen &&
|
||||||
!this.model.settings.canCreate &&
|
!this.model.settings.canCreate &&
|
||||||
!(this.model.settings.canOpenDemo && !this.model.settings.demoOpened);
|
!(this.model.settings.canOpenDemo && !this.model.settings.demoOpened);
|
||||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys.length;
|
const hasYubiKeys = !!UsbListener.attachedYubiKeys;
|
||||||
const canOpenYubiKey =
|
const canOpenYubiKey =
|
||||||
hasYubiKeys &&
|
hasYubiKeys &&
|
||||||
this.model.settings.canOpenOtpDevice &&
|
this.model.settings.canOpenOtpDevice &&
|
||||||
|
@ -992,7 +992,7 @@ class OpenView extends View {
|
||||||
|
|
||||||
usbDevicesChanged() {
|
usbDevicesChanged() {
|
||||||
if (this.model.settings.canOpenOtpDevice) {
|
if (this.model.settings.canOpenOtpDevice) {
|
||||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys.length;
|
const hasYubiKeys = !!UsbListener.attachedYubiKeys;
|
||||||
|
|
||||||
const showOpenIcon = hasYubiKeys && this.model.settings.yubiKeyShowIcon;
|
const showOpenIcon = hasYubiKeys && this.model.settings.yubiKeyShowIcon;
|
||||||
this.$el.find('.open__icon-yubikey').toggleClass('hide', !showOpenIcon);
|
this.$el.find('.open__icon-yubikey').toggleClass('hide', !showOpenIcon);
|
||||||
|
|
|
@ -724,7 +724,7 @@ class SettingsFileView extends View {
|
||||||
if (!Launcher || !AppSettingsModel.enableUsb || !AppSettingsModel.yubiKeyShowChalResp) {
|
if (!Launcher || !AppSettingsModel.enableUsb || !AppSettingsModel.yubiKeyShowChalResp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!UsbListener.attachedYubiKeys.length) {
|
if (!UsbListener.attachedYubiKeys) {
|
||||||
if (this.yubiKeys.length) {
|
if (this.yubiKeys.length) {
|
||||||
this.yubiKeys = [];
|
this.yubiKeys = [];
|
||||||
this.render();
|
this.render();
|
||||||
|
@ -738,7 +738,7 @@ class SettingsFileView extends View {
|
||||||
this.render();
|
this.render();
|
||||||
if (
|
if (
|
||||||
userInitiated &&
|
userInitiated &&
|
||||||
UsbListener.attachedYubiKeys.length &&
|
UsbListener.attachedYubiKeys &&
|
||||||
!yubiKeys.length &&
|
!yubiKeys.length &&
|
||||||
Features.isMac
|
Features.isMac
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { EventEmitter } = require('events');
|
|
||||||
|
|
||||||
let perfTimestamps = global.perfTimestamps;
|
let perfTimestamps = global.perfTimestamps;
|
||||||
perfTimestamps.push({ name: 'loading app requires', ts: process.hrtime() });
|
perfTimestamps.push({ name: 'loading app requires', ts: process.hrtime() });
|
||||||
|
@ -16,7 +15,6 @@ let restartPending = false;
|
||||||
let mainWindowPosition = {};
|
let mainWindowPosition = {};
|
||||||
let updateMainWindowPositionTimeout = null;
|
let updateMainWindowPositionTimeout = null;
|
||||||
let mainWindowMaximized = false;
|
let mainWindowMaximized = false;
|
||||||
let usbBinding = null;
|
|
||||||
|
|
||||||
const windowPositionFileName = 'window-position.json';
|
const windowPositionFileName = 'window-position.json';
|
||||||
const portableConfigFileName = 'keeweb-portable.json';
|
const portableConfigFileName = 'keeweb-portable.json';
|
||||||
|
@ -187,10 +185,11 @@ app.setHookBeforeQuitEvent = (hooked) => {
|
||||||
app.hookBeforeQuitEvent = !!hooked;
|
app.hookBeforeQuitEvent = !!hooked;
|
||||||
};
|
};
|
||||||
app.setGlobalShortcuts = setGlobalShortcuts;
|
app.setGlobalShortcuts = setGlobalShortcuts;
|
||||||
app.reqNative = reqNative;
|
|
||||||
app.showAndFocusMainWindow = showAndFocusMainWindow;
|
app.showAndFocusMainWindow = showAndFocusMainWindow;
|
||||||
app.loadConfig = loadConfig;
|
app.loadConfig = loadConfig;
|
||||||
app.saveConfig = saveConfig;
|
app.saveConfig = saveConfig;
|
||||||
|
app.getAppMainRoot = getAppMainRoot;
|
||||||
|
app.getAppContentRoot = getAppContentRoot;
|
||||||
|
|
||||||
function setSystemAppearance() {
|
function setSystemAppearance() {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
|
@ -402,7 +401,6 @@ function mainWindowClosing() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mainWindowClosed() {
|
function mainWindowClosed() {
|
||||||
usbBinding?.removeAllListeners();
|
|
||||||
app.removeAllListeners('remote-app-event');
|
app.removeAllListeners('remote-app-event');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,35 +565,20 @@ function setUserDataPaths() {
|
||||||
|
|
||||||
perfTimestamps?.push({ name: 'portable check', ts: process.hrtime() });
|
perfTimestamps?.push({ name: 'portable check', ts: process.hrtime() });
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Is portable:', isPortable);
|
|
||||||
|
|
||||||
if (isPortable) {
|
if (isPortable) {
|
||||||
const portableConfigDir = path.dirname(execPath);
|
const portableConfigDir = path.dirname(execPath);
|
||||||
const portableConfigPath = path.join(portableConfigDir, portableConfigFileName);
|
const portableConfigPath = path.join(portableConfigDir, portableConfigFileName);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Portable config path:', portableConfigPath);
|
|
||||||
|
|
||||||
if (fs.existsSync(portableConfigPath)) {
|
if (fs.existsSync(portableConfigPath)) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Portable config path exists');
|
|
||||||
|
|
||||||
const portableConfig = JSON.parse(fs.readFileSync(portableConfigPath, 'utf8'));
|
const portableConfig = JSON.parse(fs.readFileSync(portableConfigPath, 'utf8'));
|
||||||
const portableUserDataDir = path.resolve(portableConfigDir, portableConfig.userDataDir);
|
const portableUserDataDir = path.resolve(portableConfigDir, portableConfig.userDataDir);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Portable user data dir:', portableUserDataDir);
|
|
||||||
|
|
||||||
if (!fs.existsSync(portableUserDataDir)) {
|
if (!fs.existsSync(portableUserDataDir)) {
|
||||||
fs.mkdirSync(portableUserDataDir);
|
fs.mkdirSync(portableUserDataDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setPath('userData', portableUserDataDir);
|
app.setPath('userData', portableUserDataDir);
|
||||||
usingPortableUserDataDir = true;
|
usingPortableUserDataDir = true;
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`Portable config path doesn't exist`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -743,55 +726,25 @@ function reportStartProfile() {
|
||||||
emitRemoteEvent('start-profile', startProfile);
|
emitRemoteEvent('start-profile', startProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAppMainRoot() {
|
||||||
|
if (isDev) {
|
||||||
|
return __dirname;
|
||||||
|
} else {
|
||||||
|
return process.mainModule.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppContentRoot() {
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
|
||||||
function reqNative(mod) {
|
function reqNative(mod) {
|
||||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||||
|
|
||||||
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
|
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
|
||||||
let fullPath;
|
const fullPath = path.join(getAppMainRoot(), modulePath);
|
||||||
|
|
||||||
if (isDev) {
|
return require(fullPath);
|
||||||
fullPath = path.join(__dirname, modulePath);
|
|
||||||
} else {
|
|
||||||
const mainAsarPath = process.mainModule.path;
|
|
||||||
fullPath = path.join(mainAsarPath, modulePath);
|
|
||||||
|
|
||||||
// Currently native modules can't be updated
|
|
||||||
// const latestAsarPath = __dirname;
|
|
||||||
//
|
|
||||||
// fullPath = path.join(latestAsarPath, modulePath);
|
|
||||||
//
|
|
||||||
// if (!fs.existsSync(fullPath)) {
|
|
||||||
// fullPath = path.join(mainAsarPath, modulePath);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
const binding = require(fullPath);
|
|
||||||
|
|
||||||
if (mod === 'usb') {
|
|
||||||
usbBinding = initUsb(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initUsb(binding) {
|
|
||||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
|
||||||
binding[key] = EventEmitter.prototype[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
binding.on('newListener', () => {
|
|
||||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
|
||||||
binding._enableHotplugEvents();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
binding.on('removeListener', () => {
|
|
||||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
|
||||||
binding._disableHotplugEvents();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return binding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSettingsEncryptionKey() {
|
function loadSettingsEncryptionKey() {
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
|
||||||
|
let appMainRoot;
|
||||||
|
const nativeModules = {};
|
||||||
|
|
||||||
|
const YubiKeyVendorIds = [0x1050];
|
||||||
|
const attachedYubiKeys = [];
|
||||||
|
let usbListenerRunning = false;
|
||||||
|
|
||||||
|
startListener();
|
||||||
|
|
||||||
|
const messageHandlers = {
|
||||||
|
init(root) {
|
||||||
|
appMainRoot = root;
|
||||||
|
},
|
||||||
|
|
||||||
|
'start-usb'() {
|
||||||
|
if (usbListenerRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usb = reqNative('usb');
|
||||||
|
|
||||||
|
fillAttachedYubiKeys();
|
||||||
|
|
||||||
|
usb.on('attach', usbDeviceAttached);
|
||||||
|
usb.on('detach', usbDeviceDetached);
|
||||||
|
|
||||||
|
usb._enableHotplugEvents();
|
||||||
|
|
||||||
|
usbListenerRunning = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
'stop-usb'() {
|
||||||
|
if (!usbListenerRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usb = reqNative('usb');
|
||||||
|
|
||||||
|
usb.off('attach', usbDeviceAttached);
|
||||||
|
usb.off('detach', usbDeviceDetached);
|
||||||
|
|
||||||
|
usb._disableHotplugEvents();
|
||||||
|
|
||||||
|
usbListenerRunning = false;
|
||||||
|
attachedYubiKeys.length = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
'get-yubikeys'(config) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ykChapResp = reqNative('yubikey-chalresp');
|
||||||
|
ykChapResp.getYubiKeys(config, (err, yubiKeys) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(yubiKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'yk-chal-resp'(yubiKey, challenge, slot, callbackId) {
|
||||||
|
const ykChalResp = reqNative('yubikey-chalresp');
|
||||||
|
challenge = Buffer.from(challenge);
|
||||||
|
ykChalResp.challengeResponse(yubiKey, challenge, slot, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.code === ykChalResp.YK_ENOKEY) {
|
||||||
|
error.noKey = true;
|
||||||
|
}
|
||||||
|
if (error.code === ykChalResp.YK_ETIMEOUT) {
|
||||||
|
error.timeout = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
result = [...result];
|
||||||
|
}
|
||||||
|
return callback('yk-chal-resp-result', { callbackId, error, result });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'yk-cancel-chal-resp'() {
|
||||||
|
const ykChalResp = reqNative('yubikey-chalresp');
|
||||||
|
ykChalResp.cancelChallengeResponse();
|
||||||
|
},
|
||||||
|
|
||||||
|
argon2(password, salt, options) {
|
||||||
|
const argon2 = reqNative('argon2');
|
||||||
|
|
||||||
|
password = readXoredValue(password);
|
||||||
|
salt = readXoredValue(salt);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
argon2.hash(password, salt, options, (err, res) => {
|
||||||
|
password.fill(0);
|
||||||
|
salt.fill(0);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
const xoredRes = makeXoredValue(res);
|
||||||
|
res.fill(0);
|
||||||
|
|
||||||
|
resolve(xoredRes);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
xoredRes.data.fill(0);
|
||||||
|
xoredRes.random.fill(0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const moduleInit = {
|
||||||
|
usb(binding) {
|
||||||
|
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||||
|
binding[key] = EventEmitter.prototype[key];
|
||||||
|
});
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isYubiKey(device) {
|
||||||
|
return YubiKeyVendorIds.includes(device.deviceDescriptor.idVendor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usbDeviceAttached(device) {
|
||||||
|
if (isYubiKey(device)) {
|
||||||
|
attachedYubiKeys.push(device);
|
||||||
|
reportYubiKeys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function usbDeviceDetached(device) {
|
||||||
|
if (isYubiKey(device)) {
|
||||||
|
const index = attachedYubiKeys.findIndex((yk) => yk.deviceAddress === device.deviceAddress);
|
||||||
|
if (index >= 0) {
|
||||||
|
attachedYubiKeys.splice(index, 1);
|
||||||
|
}
|
||||||
|
reportYubiKeys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillAttachedYubiKeys() {
|
||||||
|
const usb = reqNative('usb');
|
||||||
|
attachedYubiKeys.push(...usb.getDeviceList().filter(isYubiKey));
|
||||||
|
reportYubiKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportYubiKeys() {
|
||||||
|
callback('yubikeys', attachedYubiKeys.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reqNative(mod) {
|
||||||
|
if (nativeModules[mod]) {
|
||||||
|
return nativeModules[mod];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||||
|
|
||||||
|
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
|
||||||
|
const fullPath = path.join(appMainRoot, modulePath);
|
||||||
|
|
||||||
|
let binding = require(fullPath);
|
||||||
|
|
||||||
|
if (moduleInit[mod]) {
|
||||||
|
binding = moduleInit[mod](binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeModules[mod] = binding;
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readXoredValue(val) {
|
||||||
|
const data = Buffer.from(val.data);
|
||||||
|
const random = Buffer.from(val.random);
|
||||||
|
|
||||||
|
val.data.fill(0);
|
||||||
|
val.random.fill(0);
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] ^= random[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
random.fill(0);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeXoredValue(val) {
|
||||||
|
const data = Buffer.from(val);
|
||||||
|
const random = crypto.randomBytes(data.length);
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
data[i] ^= random[i];
|
||||||
|
}
|
||||||
|
const result = { data: [...data], random: [...random] };
|
||||||
|
data.fill(0);
|
||||||
|
random.fill(0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startListener() {
|
||||||
|
process.on('message', ({ callId, cmd, args }) => {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const handler = messageHandlers[cmd];
|
||||||
|
if (handler) {
|
||||||
|
return handler(...args);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Handler not found: ${cmd}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
callback('result', { callId, result });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
error = {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
code: error.code
|
||||||
|
};
|
||||||
|
callback('result', { callId, error });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('disconnect', () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function callback(cmd, ...args) {
|
||||||
|
try {
|
||||||
|
process.send({ cmd, args });
|
||||||
|
} catch {}
|
||||||
|
}
|
Loading…
Reference in New Issue