keeweb/desktop/main.js

1045 lines
32 KiB
JavaScript

let perfTimestamps = [{ name: 'pre-init', ts: process.hrtime() }];
if (process.send && process.argv.includes('--native-module-host')) {
require('./native-module-host').startInOwnProcess();
return;
}
const electron = require('electron');
const path = require('path');
const fs = require('fs');
const url = require('url');
const { locale, setLocale, getLocaleValues } = require('./scripts/locale');
const { Logger } = require('./scripts/logger');
const { isDev } = require('./scripts/util/app-info');
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;
let ready = false;
let appReady = false;
let pendingUpdateFilePath;
let mainWindowPosition = {};
let updateMainWindowPositionTimeout = null;
let mainWindowMaximized = false;
const windowPositionFileName = 'window-position.json';
const portableConfigFileName = 'keeweb-portable.json';
const startupLogging =
process.argv.some((arg) => arg.startsWith('--startup-logging')) ||
process.env.KEEWEB_STARTUP_LOGGING === '1';
const gotTheLock = main.requestSingleInstanceLock();
if (!gotTheLock) {
main.quit();
}
logProgress('single instance lock');
let usingPortableUserDataDir = false;
let execPath;
setUserDataPaths();
let openFile = process.argv.filter((arg) => /\.kdbx$/i.test(arg))[0];
const htmlPath =
(isDev && process.env.KEEWEB_HTML_PATH) ||
url.format({ protocol: 'file', slashes: true, pathname: path.join(__dirname, 'index.html') });
const showDevToolsOnStart =
process.argv.some((arg) => arg.startsWith('--devtools')) ||
process.env.KEEWEB_OPEN_DEVTOOLS === '1';
const loginItemSettings = process.platform === 'darwin' ? main.getLoginItemSettings() : {};
const startMinimized =
loginItemSettings.wasOpenedAsHidden ||
process.argv.some((arg) => arg.startsWith('--minimized'));
const themeBgColors = {
dark: '#1e1e1e',
light: '#f6f6f6',
db: '#342f2e',
fb: '#282c34',
wh: '#fafafa',
te: '#222',
hc: '#fafafa',
sd: '#002b36',
sl: '#fdf6e3'
};
const darkLightThemes = {
dark: 'light',
sd: 'sl',
fb: 'bl',
db: 'lb',
te: 'lt',
dc: 'hc'
};
const defaultBgColor = '#282C34';
logProgress('defining args');
setEnv();
setDevAppIcon();
let configEncryptionKey;
let appSettings;
const settingsPromise = loadSettingsEncryptionKey().then((key) => {
configEncryptionKey = key;
logProgress('loading settings key');
return loadConfig('app-settings').then((settings) => {
try {
appSettings = settings ? JSON.parse(settings) : {};
} catch (e) {
logStartupMessage(`Error loading app settings: ${e}`);
}
logProgress('reading app settings');
});
});
main.on('window-all-closed', () => {
if (pendingUpdateFilePath) {
exitAndStartUpdate();
} else {
if (process.platform !== 'darwin') {
main.quit();
}
}
});
main.on('ready', () => {
logProgress('app on ready');
appReady = true;
settingsPromise
.then(() => {
createMainWindow();
setupIpcHandlers();
setGlobalShortcuts(appSettings);
subscribePowerEvents();
hookRequestHeaders();
loadLocale().then(() => {
setMenu();
});
})
.catch((e) => {
electron.dialog.showErrorBox('KeeWeb', 'Error loading app: ' + e);
main.exit(2);
});
});
main.on('open-file', (e, path) => {
e.preventDefault();
openFile = path;
notifyOpenFile();
});
main.on('activate', () => {
if (process.platform === 'darwin') {
if (appReady && !mainWindow && appSettings) {
createMainWindow();
} else if (appIcon) {
restoreMainWindow();
}
}
});
main.on('before-quit', (e) => {
if (main.hookBeforeQuitEvent && mainWindow) {
e.preventDefault();
emitRemoteEvent('launcher-before-quit');
}
});
main.on('will-quit', () => {
electron.globalShortcut.unregisterAll();
});
main.on('second-instance', () => {
if (mainWindow) {
restoreMainWindow();
}
});
main.on('web-contents-created', (event, contents) => {
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();
logger.warn(`Prevented navigation: ${url}`);
}
});
});
main.restartAndUpdate = function (updateFilePath) {
pendingUpdateFilePath = updateFilePath;
mainWindow.close();
setTimeout(() => {
pendingUpdateFilePath = undefined;
}, 1000);
};
main.minimizeApp = function (menuItemLabels) {
let imagePath;
// a workaround to correctly restore focus on windows platform
// without this workaround, focus is not restored to the previously focused field
if (process.platform === 'win32') {
mainWindow.minimize();
}
mainWindow.hide();
if (process.platform === 'darwin') {
main.dock.hide();
imagePath = 'macOS-MenubarTemplate.png';
} else {
imagePath = 'icon.png';
}
mainWindow.setSkipTaskbar(true);
if (!appIcon) {
const image = electron.nativeImage.createFromPath(path.join(__dirname, 'img', imagePath));
appIcon = new electron.Tray(image);
if (process.platform !== 'darwin') {
appIcon.on('click', restoreMainWindow);
}
const contextMenu = electron.Menu.buildFromTemplate([
{ label: menuItemLabels.restore, click: restoreMainWindow },
{ label: menuItemLabels.quit, click: closeMainWindow }
]);
appIcon.setContextMenu(contextMenu);
appIcon.setToolTip('KeeWeb');
}
};
main.minimizeThenHideIfInTray = function () {
// This function is called when auto-type has displayed a selection list and a selection was made.
// To ensure focus returns to the previous window we must minimize first even if we're going to hide.
mainWindow.minimize();
if (appIcon) mainWindow.hide();
};
main.getMainWindow = function () {
return mainWindow;
};
main.setHookBeforeQuitEvent = (hooked) => {
main.hookBeforeQuitEvent = !!hooked;
};
main.setGlobalShortcuts = setGlobalShortcuts;
main.showAndFocusMainWindow = showAndFocusMainWindow;
main.loadConfig = loadConfig;
main.saveConfig = saveConfig;
main.getAppMainRoot = getAppMainRoot;
main.getAppContentRoot = getAppContentRoot;
main.httpRequest = httpRequest;
function logProgress(name) {
perfTimestamps?.push({ name, ts: process.hrtime() });
logStartupMessage(name);
}
function logStartupMessage(msg) {
if (startupLogging) {
// eslint-disable-next-line no-console
console.log('[startup]', msg);
}
}
function checkSettingsTheme(theme) {
// old settings migration
if (theme === 'macdark') {
return 'dark';
}
if (theme === 'wh') {
return 'light';
}
return theme;
}
function getDefaultTheme() {
return 'dark';
}
function selectDarkOrLightTheme(theme) {
const dark = electron.nativeTheme.shouldUseDarkColors;
for (const [darkTheme, lightTheme] of Object.entries(darkLightThemes)) {
if (darkTheme === theme || lightTheme === theme) {
return dark ? darkTheme : lightTheme;
}
}
return theme;
}
function createMainWindow() {
let theme = checkSettingsTheme(appSettings.theme) || getDefaultTheme();
if (appSettings.autoSwitchTheme) {
theme = selectDarkOrLightTheme(theme);
}
const bgColor = themeBgColors[theme] || defaultBgColor;
const isWindows = process.platform === 'win32';
let titlebarStyle = appSettings.titlebarStyle;
if (titlebarStyle === 'hidden-inset') {
titlebarStyle = 'hiddenInset';
}
const frameless = isWindows && ['hidden', 'hiddenInset'].includes(titlebarStyle);
const windowOptions = {
show: false,
width: 1000,
height: 700,
minWidth: 700,
minHeight: 400,
titleBarStyle: titlebarStyle,
frame: !frameless,
backgroundColor: bgColor,
webPreferences: {
contextIsolation: false,
backgroundThrottling: false,
nodeIntegration: true,
nodeIntegrationInWorker: true,
enableRemoteModule: true,
spellcheck: false,
v8CacheOptions: 'none'
}
};
if (process.platform !== 'win32') {
windowOptions.icon = path.join(__dirname, 'img', 'icon.png');
}
mainWindow = new electron.BrowserWindow(windowOptions);
logProgress('creating main window');
mainWindow.loadURL(htmlPath);
mainWindow.once('ready-to-show', () => {
logProgress('main window ready');
if (startMinimized) {
emitRemoteEvent('launcher-started-minimized');
} else {
mainWindow.show();
}
ready = true;
notifyOpenFile();
logProgress('main window shown');
reportStartProfile();
if (showDevToolsOnStart) {
mainWindow.webContents.openDevTools({ mode: 'bottom' });
}
});
mainWindow.webContents.on('context-menu', onContextMenu);
mainWindow.on('resize', delaySaveMainWindowPosition);
mainWindow.on('move', delaySaveMainWindowPosition);
mainWindow.on('restore', coerceMainWindowPositionToConnectedDisplay);
mainWindow.on('close', mainWindowClosing);
mainWindow.on('closed', mainWindowClosed);
mainWindow.on('focus', mainWindowFocus);
mainWindow.on('blur', mainWindowBlur);
mainWindow.on('closed', () => {
mainWindow = null;
saveMainWindowPosition();
});
mainWindow.on('minimize', () => {
emitRemoteEvent('launcher-minimize');
});
mainWindow.on('maximize', () => {
mainWindowMaximized = true;
emitRemoteEvent('launcher-maximize');
});
mainWindow.on('unmaximize', () => {
mainWindowMaximized = false;
emitRemoteEvent('launcher-unmaximize');
});
mainWindow.on('leave-full-screen', () => {
emitRemoteEvent('leave-full-screen');
});
mainWindow.on('enter-full-screen', () => {
emitRemoteEvent('enter-full-screen');
});
mainWindow.on('session-end', () => {
emitRemoteEvent('os-lock');
});
logProgress('configuring main window');
restoreMainWindowPosition();
logProgress('restoring main window position');
}
function restoreMainWindow() {
if (process.platform === 'darwin' && !main.dock.isVisible()) {
main.dock.show();
}
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.setSkipTaskbar(false);
mainWindow.show();
coerceMainWindowPositionToConnectedDisplay();
setTimeout(destroyAppIcon, 0);
}
function showAndFocusMainWindow() {
if (appIcon) {
restoreMainWindow();
}
if (mainWindowMaximized) {
mainWindow.maximize();
} else {
mainWindow.show();
}
mainWindow.focus();
if (process.platform === 'darwin' && !main.dock.isVisible()) {
main.dock.show();
}
}
function closeMainWindow() {
emitRemoteEvent('launcher-exit-request');
setTimeout(destroyAppIcon, 0);
}
function destroyAppIcon() {
if (appIcon) {
appIcon.destroy();
appIcon = null;
}
}
function delaySaveMainWindowPosition() {
if (updateMainWindowPositionTimeout) {
clearTimeout(updateMainWindowPositionTimeout);
}
updateMainWindowPositionTimeout = setTimeout(updateMainWindowPosition, 500);
}
function updateMainWindowPositionIfPending() {
if (updateMainWindowPositionTimeout) {
clearTimeout(updateMainWindowPositionTimeout);
updateMainWindowPosition();
}
}
function updateMainWindowPosition() {
if (!mainWindow) {
return;
}
updateMainWindowPositionTimeout = null;
const bounds = mainWindow.getBounds();
if (!mainWindow.isMaximized() && !mainWindow.isMinimized() && !mainWindow.isFullScreen()) {
mainWindowPosition.x = bounds.x;
mainWindowPosition.y = bounds.y;
mainWindowPosition.width = bounds.width;
mainWindowPosition.height = bounds.height;
}
mainWindowPosition.maximized = mainWindow.isMaximized();
mainWindowPosition.fullScreen = mainWindow.isFullScreen();
mainWindowPosition.changed = true;
}
function saveMainWindowPosition() {
if (!mainWindowPosition.changed) {
return;
}
delete mainWindowPosition.changed;
try {
fs.writeFileSync(
path.join(main.getPath('userData'), windowPositionFileName),
JSON.stringify(mainWindowPosition),
'utf8'
);
} catch (e) {}
}
function restoreMainWindowPosition() {
const fileName = path.join(main.getPath('userData'), windowPositionFileName);
fs.readFile(fileName, 'utf8', (e, data) => {
if (data) {
try {
mainWindowPosition = JSON.parse(data);
} catch (e) {
logStartupMessage(`Error loading main window position: ${e}`);
}
if (mainWindow && mainWindowPosition) {
if (mainWindowPosition.width && mainWindowPosition.height) {
mainWindow.setBounds(mainWindowPosition);
coerceMainWindowPositionToConnectedDisplay();
}
if (mainWindowPosition.maximized) {
mainWindow.maximize();
mainWindowMaximized = true;
}
if (mainWindowPosition.fullScreen) {
mainWindow.setFullScreen(true);
}
}
}
});
}
function mainWindowBlur() {
emitRemoteEvent('main-window-blur');
}
function mainWindowFocus() {
emitRemoteEvent('main-window-focus');
}
function mainWindowClosing() {
updateMainWindowPositionIfPending();
}
function mainWindowClosed() {
main.removeAllListeners('remote-app-event');
}
function emitRemoteEvent(e, arg) {
if (mainWindow && mainWindow.webContents) {
main.emit('remote-app-event', {
name: e,
data: arg
});
}
}
function setMenu() {
if (process.platform === 'darwin') {
const name = require('electron').app.name;
const template = [
{
label: name,
submenu: [
{ role: 'about', label: locale.sysMenuAboutKeeWeb?.replace('{}', 'KeeWeb') },
{ type: 'separator' },
{ role: 'services', submenu: [], label: locale.sysMenuServices },
{ type: 'separator' },
{
accelerator: 'Command+H',
role: 'hide',
label: locale.sysMenuHide?.replace('{}', 'KeeWeb')
},
{
accelerator: 'Command+Shift+H',
role: 'hideothers',
label: locale.sysMenuHideOthers
},
{ role: 'unhide', label: locale.sysMenuUnhide },
{ type: 'separator' },
{
role: 'quit',
accelerator: 'Command+Q',
label: locale.sysMenuQuit?.replace('{}', 'KeeWeb')
}
]
},
{
label: locale.sysMenuEdit || 'Edit',
submenu: [
{ accelerator: 'CmdOrCtrl+Z', role: 'undo', label: locale.sysMenuUndo },
{ accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo', label: locale.sysMenuRedo },
{ type: 'separator' },
{ accelerator: 'CmdOrCtrl+X', role: 'cut', label: locale.sysMenuCut },
{ accelerator: 'CmdOrCtrl+C', role: 'copy', label: locale.sysMenuCopy },
{ accelerator: 'CmdOrCtrl+V', role: 'paste', label: locale.sysMenuPaste },
{
accelerator: 'CmdOrCtrl+A',
role: 'selectall',
label: locale.sysMenuSelectAll
}
]
},
{
label: locale.sysMenuWindow || 'Window',
submenu: [
{ accelerator: 'CmdOrCtrl+M', role: 'minimize', label: locale.sysMenuMinimize },
{ accelerator: 'Command+W', role: 'close', label: locale.sysMenuClose }
]
}
];
const menu = electron.Menu.buildFromTemplate(template);
electron.Menu.setApplicationMenu(menu);
} else {
mainWindow.setMenuBarVisibility(false);
mainWindow.setMenu(null);
electron.Menu.setApplicationMenu(null);
}
}
function onContextMenu(e, props) {
if (props.inputFieldType !== 'plainText' || !props.isEditable) {
return;
}
const Menu = electron.Menu;
const inputMenu = Menu.buildFromTemplate([
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ type: 'separator' },
{ role: 'selectall' }
]);
inputMenu.popup(mainWindow);
}
function notifyOpenFile() {
if (ready && openFile && mainWindow) {
const openKeyfile = process.argv
.filter((arg) => arg.startsWith('--keyfile='))
.map((arg) => arg.replace('--keyfile=', ''))[0];
const fileInfo = JSON.stringify({ data: openFile, key: openKeyfile });
mainWindow.webContents.executeJavaScript(
'if (window.launcherOpen) { window.launcherOpen(' +
fileInfo +
'); } ' +
' else { window.launcherOpenedFile=' +
fileInfo +
'; }'
);
openFile = null;
}
}
function setGlobalShortcuts(appSettings) {
const defaultShortcutModifiers = process.platform === 'darwin' ? 'Ctrl+Alt+' : 'Shift+Alt+';
const defaultShortcuts = {
AutoType: { shortcut: defaultShortcutModifiers + 'T', event: 'auto-type' },
CopyPassword: { shortcut: defaultShortcutModifiers + 'C', event: 'copy-password' },
CopyUser: { shortcut: defaultShortcutModifiers + 'B', event: 'copy-user' },
CopyUrl: { shortcut: defaultShortcutModifiers + 'U', event: 'copy-url' },
CopyOtp: { event: 'copy-otp' },
RestoreApp: { action: restoreMainWindow }
};
electron.globalShortcut.unregisterAll();
for (const [key, shortcutDef] of Object.entries(defaultShortcuts)) {
const fromSettings = appSettings[`globalShortcut${key}`];
const shortcut = fromSettings || shortcutDef.shortcut;
if (shortcut) {
try {
electron.globalShortcut.register(shortcut, () => {
if (shortcutDef.event) {
emitRemoteEvent(shortcutDef.event);
}
if (shortcutDef.action) {
shortcutDef.action();
}
});
} catch (e) {}
}
}
logProgress('setting global shortcuts');
}
function subscribePowerEvents() {
electron.powerMonitor.on('suspend', () => {
emitRemoteEvent('power-monitor-suspend');
});
electron.powerMonitor.on('resume', () => {
emitRemoteEvent('power-monitor-resume');
});
electron.powerMonitor.on('lock-screen', () => {
emitRemoteEvent('os-lock');
});
logProgress('subscribing to power events');
}
function setUserDataPaths() {
execPath = process.execPath;
let isPortable = false;
switch (process.platform) {
case 'darwin':
isPortable = !execPath.includes('/Applications/');
if (isPortable) {
execPath = execPath.substring(0, execPath.indexOf('.app'));
}
break;
case 'win32':
isPortable = !execPath.includes('Program Files');
break;
case 'linux':
isPortable = !execPath.startsWith('/usr/') && !execPath.startsWith('/opt/');
break;
}
if (isDev && process.env.KEEWEB_IS_PORTABLE) {
try {
isPortable = !!JSON.parse(process.env.KEEWEB_IS_PORTABLE);
} catch {}
}
logProgress('portable check');
if (isPortable) {
const portableConfigDir = path.dirname(execPath);
const portableConfigPath = path.join(portableConfigDir, portableConfigFileName);
if (fs.existsSync(portableConfigPath)) {
try {
const portableConfig = JSON.parse(fs.readFileSync(portableConfigPath, 'utf8'));
const portableUserDataDir = path.resolve(
portableConfigDir,
portableConfig.userDataDir
);
if (!fs.existsSync(portableUserDataDir)) {
fs.mkdirSync(portableUserDataDir, { recursive: true });
}
main.setPath('userData', portableUserDataDir);
usingPortableUserDataDir = true;
} catch (e) {
logStartupMessage(`Error loading portable config: ${e}`);
}
}
}
logProgress('userdata dir');
}
function setEnv() {
if (
process.platform === 'linux' &&
['Pantheon', 'Unity:Unity7'].indexOf(process.env.XDG_CURRENT_DESKTOP) !== -1
) {
// https://github.com/electron/electron/issues/9046
process.env.XDG_CURRENT_DESKTOP = 'Unity';
}
main.commandLine.appendSwitch('disable-background-timer-throttling');
// disable all caching, since we're not using old profile data anyway
main.commandLine.appendSwitch('disable-http-cache');
main.commandLine.appendSwitch('disable-gpu-shader-disk-cache');
if (process.platform === 'linux') {
// fixes colors on Linux, see #1621
main.commandLine.appendSwitch('force-color-profile', 'srgb');
}
main.allowRendererProcessReuse = true;
logProgress('setting env');
}
function setDevAppIcon() {
if (isDev && htmlPath && process.platform === 'darwin') {
const icon = electron.nativeImage.createFromPath(
path.join(__dirname, '../graphics/512x512.png')
);
main.dock.setIcon(icon);
}
}
// When sending a PUT XMLHttpRequest Chromium includes the header "Origin: file://".
// This confuses some WebDAV clients, notably OwnCloud.
// The header is invalid, so removing it everywhere it occurs should do no harm.
function hookRequestHeaders() {
electron.session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
if (
!details.url.startsWith('ws:') &&
!details.url.startsWith('https://plugins.keeweb.info/')
) {
delete details.requestHeaders.Origin;
}
callback({ requestHeaders: details.requestHeaders });
});
logProgress('setting request handlers');
}
// If a display is disconnected while KeeWeb is minimized, Electron does not
// ensure that the restored window appears on a display that is still connected.
// This checks to be sure the title bar is somewhere the user can grab it,
// without making it impossible to minimize and restore a window keeping it
// partially off-screen or straddling two displays if the user desires that.
function coerceMainWindowPositionToConnectedDisplay() {
const eScreen = electron.screen;
const displays = eScreen.getAllDisplays();
if (!displays || !displays.length) return;
const windowBounds = mainWindow.getBounds();
const contentBounds = mainWindow.getContentBounds();
const tbLeft = windowBounds.x;
const tbRight = windowBounds.x + windowBounds.width;
const tbTop = windowBounds.y;
const tbBottom = contentBounds.y;
// 160px width and 2/3s the title bar height should be enough that the user can grab it
for (let i = 0; i < displays.length; ++i) {
const workArea = displays[i].workArea;
const overlapWidth =
Math.min(tbRight, workArea.x + workArea.width) - Math.max(tbLeft, workArea.x);
const overlapHeight =
Math.min(tbBottom, workArea.y + workArea.height) - Math.max(tbTop, workArea.y);
if (overlapWidth >= 160 && 3 * overlapHeight >= 2 * (tbBottom - tbTop)) return;
}
// If we get here, no display contains a big enough strip of the title bar
// that we can be confident the user can drag it into visibility. Rather than
// attempt to guess what the user wants, just center it on the primary display.
// Try to keep the previous height and width, but clamp each to 90% of the workarea.
const workArea = eScreen.getPrimaryDisplay().workArea;
const newWidth = Math.min(windowBounds.width, Math.floor(0.9 * workArea.width));
const newHeight = Math.min(windowBounds.height, Math.floor(0.9 * workArea.height));
mainWindow.setBounds({
'x': workArea.x + Math.floor((workArea.width - newWidth) / 2),
'y': workArea.y + Math.floor((workArea.height - newHeight) / 2),
'width': newWidth,
'height': newHeight
});
updateMainWindowPosition();
}
function reportStartProfile() {
if (!perfTimestamps) {
return;
}
const processCreationTime = process.getCreationTime();
const totalTime = Math.round(Date.now() - processCreationTime);
let lastTs = 0;
const timings = perfTimestamps
.map((milestone) => {
const ts = milestone.ts;
const elapsed = lastTs
? Math.round((ts[0] - lastTs[0]) * 1e3 + (ts[1] - lastTs[1]) / 1e6)
: 0;
lastTs = ts;
return {
name: milestone.name,
elapsed
};
})
.slice(1);
perfTimestamps = undefined;
const startProfile = { totalTime, timings };
emitRemoteEvent('start-profile', startProfile);
}
function getAppMainRoot() {
if (isDev) {
return __dirname;
} else {
return process.mainModule.path;
}
}
function getAppContentRoot() {
return __dirname;
}
function reqNative(mod) {
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
const fullPath = path.join(getAppMainRoot(), modulePath);
return require(fullPath);
}
function loadSettingsEncryptionKey() {
return Promise.resolve().then(() => {
if (usingPortableUserDataDir) {
return null;
}
const keytar = reqNative('keytar');
return keytar.getPassword('KeeWeb', 'settings-key').then((key) => {
if (key) {
return Buffer.from(key, 'hex');
}
key = require('crypto').randomBytes(48);
return keytar.setPassword('KeeWeb', 'settings-key', key.toString('hex')).then(() => {
return migrateOldConfigs(key).then(() => key);
});
});
});
}
function loadConfig(name) {
const ext = configEncryptionKey ? 'dat' : 'json';
const configFilePath = path.join(main.getPath('userData'), `${name}.${ext}`);
return new Promise((resolve, reject) => {
fs.readFile(configFilePath, (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
resolve(null);
} else {
reject(`Error reading config ${name}: ${err}`);
}
return;
}
try {
if (configEncryptionKey) {
const key = configEncryptionKey.slice(0, 32);
const iv = configEncryptionKey.slice(32, 48);
const crypto = require('crypto');
const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
data = Buffer.concat([cipher.update(data), cipher.final()]);
}
resolve(data.toString('utf8'));
} catch (err) {
logStartupMessage(`Error reading config data (config ignored) ${name}: ${err}`);
resolve(null);
}
});
});
}
function saveConfig(name, data, key) {
if (!key) {
key = configEncryptionKey;
}
return new Promise((resolve, reject) => {
try {
data = Buffer.from(data);
if (key) {
const crypto = require('crypto');
const cipher = crypto.createCipheriv(
'aes-256-cbc',
key.slice(0, 32),
key.slice(32, 48)
);
data = Buffer.concat([cipher.update(data), cipher.final()]);
}
} catch (err) {
return reject(`Error writing config data ${name}: ${err}`);
}
const ext = key ? 'dat' : 'json';
const configFilePath = path.join(main.getPath('userData'), `${name}.${ext}`);
fs.writeFile(configFilePath, data, (err) => {
if (err) {
reject(`Error writing config ${name}: ${err}`);
} else {
resolve();
}
});
});
}
function loadLocale() {
return loadConfig('locale').then((localeValues) => {
if (localeValues) {
try {
localeValues = JSON.parse(localeValues);
if (appSettings?.locale === localeValues?.locale) {
setLocale(localeValues);
}
} catch (e) {
logStartupMessage(`Error loading locale: ${e}`);
}
}
locale.on('changed', () => {
setMenu();
saveConfig('locale', JSON.stringify(getLocaleValues()));
});
});
}
// TODO: delete in 2021
function migrateOldConfigs(key) {
const knownConfigs = [
'file-info',
'app-settings',
'runtime-data',
'update-info',
'plugin-gallery',
'plugins'
];
const promises = [];
for (const configName of knownConfigs) {
promises.push(
loadConfig(configName).then((data) => {
if (data) {
return saveConfig(configName, data, key).then(() => {
fs.unlinkSync(path.join(main.getPath('userData'), `${configName}.json`));
});
}
})
);
}
return Promise.all(promises);
}
function httpRequest(config, log, onLoad) {
// eslint-disable-next-line node/no-deprecated-api
const opts = url.parse(config.url);
opts.method = config.method || 'GET';
opts.headers = {
'User-Agent': mainWindow.webContents.userAgent,
...config.headers
};
opts.timeout = 60000;
let data;
if (config.data) {
if (config.dataIsMultipart) {
data = Buffer.concat(config.data.map((chunk) => Buffer.from(chunk)));
} else {
data = Buffer.from(config.data);
}
// Electron's API doesn't like that, while node.js needs it
// opts.headers['Content-Length'] = data.byteLength;
}
const req = electron.net.request(opts);
req.on('response', (res) => {
const chunks = [];
const onClose = () => {
log('info', 'HTTP response', opts.method, config.url, res.statusCode, res.headers);
onLoad({
status: res.statusCode,
response: Buffer.concat(chunks).toString('hex'),
headers: res.headers
});
};
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
onClose();
});
});
req.on('error', (e) => {
log('error', 'HTTP error', opts.method, config.url, e);
return config.error && config.error('network error', {});
});
req.on('timeout', () => {
req.abort();
return config.error && config.error('timeout', {});
});
if (data) {
req.write(data);
}
req.end();
}
function setupIpcHandlers() {
const { setupIpcHandlers } = require('./scripts/ipc');
setupIpcHandlers();
logProgress('setting ipc handlers');
}
function exitAndStartUpdate() {
if (pendingUpdateFilePath) {
const { installUpdate } = require('./scripts/update-installer');
installUpdate(pendingUpdateFilePath);
}
}