Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
219 lines (197 sloc) 6.83 KB
/*
Reasonably Secure Electron
Copyright (C) 2019 Bishop Fox
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
--------------------------------------------------------------------------
Maps IPC calls to RPC calls, and provides other local operations such as
listing/selecting configs to the sandboxed code.
*/
import { ipcMain, dialog, FileFilter, BrowserWindow, IpcMainEvent } from 'electron';
import { homedir } from 'os';
import * as base64 from 'base64-arraybuffer';
import * as fs from 'fs';
import * as path from 'path';
import * as Ajv from 'ajv';
export interface ReadFileReq {
title: string;
message: string;
openDirectory: boolean;
multiSelections: boolean;
filters: FileFilter[] | null; // { filters: [ { name: 'Custom File Type', extensions: ['as'] } ] }
}
export interface SaveFileReq {
title: string;
message: string;
filename: string;
data: string;
}
export interface IPCMessage {
id: number;
type: string;
method: string; // Identifies the target method and in the response if the method call was a success/error
data: string;
}
// jsonSchema - A JSON Schema decorator, somewhat redundant given we're using TypeScript
// but it provides a stricter method of validating incoming JSON messages than simply
// casting the result of JSON.parse() to an interface.
function jsonSchema(schema: object) {
const ajv = new Ajv({allErrors: true});
schema["additionalProperties"] = false;
const validate = ajv.compile(schema);
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = (arg: string) => {
const valid = validate(arg);
if (valid) {
return originalMethod(arg);
} else {
console.error(validate.errors);
return Promise.reject(`Invalid schema: ${ajv.errorsText(validate.errors)}`);
}
};
return descriptor;
};
}
// IPC Methods used to start/interact with the RPCClient
export class IPCHandlers {
@jsonSchema({
"properties": {
"title": {"type": "string", "minLength": 1, "maxLength": 100},
"message": {"type": "string", "minLength": 1, "maxLength": 100},
"openDirectory": {"type": "boolean"},
"multiSelections": {"type": "boolean"},
"filter": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"extensions": {
"type": "array",
"items": {"type": "string"}
}
}
}
}
},
"required": ["title", "message"]
})
static async fs_readFile(req: string): Promise<string> {
const readFileReq: ReadFileReq = JSON.parse(req);
const dialogOptions = {
title: readFileReq.title,
message: readFileReq.message,
openDirectory: readFileReq.openDirectory,
multiSelections: readFileReq.multiSelections
};
const files = [];
const open = await dialog.showOpenDialog(null, dialogOptions);
await Promise.all(open.filePaths.map((filePath) => {
return new Promise(async (resolve) => {
fs.readFile(filePath, (err, data) => {
files.push({
filePath: filePath,
error: err ? err.toString() : null,
data: data ? base64.encode(data) : null
});
resolve(); // Failures get stored in `files` array
});
});
}));
return JSON.stringify({ files: files });
}
@jsonSchema({
"properties": {
"title": {"type": "string", "minLength": 1, "maxLength": 100},
"message": {"type": "string", "minLength": 1, "maxLength": 100},
"filename": {"type": "string", "minLength": 1},
"data": {"type": "string"}
},
"required": ["title", "message", "filename", "data"]
})
static fs_saveFile(req: string): Promise<string> {
return new Promise(async (resolve, reject) => {
const saveFileReq: SaveFileReq = JSON.parse(req);
const dialogOptions = {
title: saveFileReq.title,
message: saveFileReq.message,
defaultPath: path.join(homedir(), 'Downloads', path.basename(saveFileReq.filename)),
};
const save = await dialog.showSaveDialog(dialogOptions);
console.log(`[save file] ${save.filePath}`);
if (save.canceled) {
return resolve(''); // Must return to stop execution
}
const fileOptions = {
mode: 0o644,
encoding: 'binary',
};
const data = Buffer.from(base64.decode(saveFileReq.data));
fs.writeFile(save.filePath, data, fileOptions, (err) => {
if (err) {
reject(err);
} else {
resolve(JSON.stringify({ filename: save.filePath }));
}
});
});
}
}
async function dispatchIPC(method: string, data: string): Promise<string | null> {
console.log(`IPC Dispatch: ${method}`);
// IPC handlers must start with "namespace_" this helps ensure we do not inadvertently
// expose methods that we don't want exposed to the sandboxed code.
if (['fs_'].some(prefix => method.startsWith(prefix))) {
if (typeof IPCHandlers[method] === 'function') {
const result: string = await IPCHandlers[method](data);
return result;
} else {
return Promise.reject(`No handler for method: ${method}`);
}
} else {
return Promise.reject(`Invalid method handler namespace for "${method}"`);
}
}
export function startIPCHandlers(window: BrowserWindow) {
ipcMain.on('ipc', async (event: IpcMainEvent, msg: IPCMessage) => {
dispatchIPC(msg.method, msg.data).then((result: string) => {
if (msg.id !== 0) {
event.sender.send('ipc', {
id: msg.id,
type: 'response',
method: 'success',
data: result
});
}
}).catch((err) => {
console.error(`[startIPCHandlers] ${err}`);
if (msg.id !== 0) {
event.sender.send('ipc', {
id: msg.id,
type: 'response',
method: 'error',
data: err.toString()
});
}
});
});
// This one doesn't have an event argument for some reason ...
ipcMain.on('push', async (_: IpcMainEvent, data: string) => {
window.webContents.send('ipc', {
id: 0,
type: 'push',
method: '',
data: data
});
});
}
You can’t perform that action at this time.