Wait for an answer from Electron synchronously - security

I'm trying to make a desktop bybit trading app... and I can't figure it out how to make the code wait for the response from the main script..... I need to wait for a response with the needed info for example like wallet balance. Instead the code runs asynchronously and I the var I need it to render is undefined.... I found the article about the vulnerability of nodeIntegration: true from the author of electron security or smth. So I did everything like he said... but now I can't "pause" the code for the data to receive and render.... here's the code
main.js
const { app, BrowserWindow, ipcMain, Menu } = require('electron');
const path = require('path');
const CryptoJS = require('crypto-js');
const axios = require('axios');
let win;
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 385,
height: 200,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
preload: path.join(app.getAppPath(), 'preload.js'), // use a preload script
backgroundThrottling: false,
},
});
// Load app
win.loadFile(path.join(__dirname, './index.html'));
win.setAlwaysOnTop(true, 'screen');
// win.removeMenu();
// rest of code..
}
app.on('ready', createWindow);
ipcMain.on('giveBalance', async e => {
const apiKey = '';
const secret = '';
const timestamp = Date.now().toString();
const params = {
api_key: apiKey,
timestamp: timestamp,
};
let orderedParams = '';
Object.keys(params)
.sort()
.forEach(function (key) {
orderedParams += key + '=' + params[key] + '&';
});
orderedParams = orderedParams.substring(0, orderedParams.length - 1);
var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, secret);
hmac.update(orderedParams);
const sign = hmac.finalize().toString(CryptoJS.enc.Hex);
const res = await axios.get(`https://api.bybit.com/v2/private/wallet/balance?&api_key=${apiKey}&sign=${sign}&timestamp=${timestamp}`);
let responseObj = res.data.result.USDT.equity.toFixed(2);
console.log(res.data.result.USDT.equity.toFixed(2));
win.webContents.send('balance', { responseObj });
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => {
// whitelist channels
let validChannels = ['giveBalance', 'givePosition'];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ['balance', 'position'];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
script.js
const getBalance = () => {
window.api.send('giveBalance');
const res = window.api.receive('balance', data => {
console.log('hi');
return data.responseObj;
});
const myBalance = '$' + res;
document.querySelector('.myBalance').textContent = `My Balance: ${myBalance}`;
console.log('not in time');
};
getBalance();
I need to get the balance in the scipt.js, to stop the code untill the res var has received the data... but all of the code gets executed right away and only then it receives a message... thank you..
if I change the part in preload.js to this
changing .on to .sendSync
receive: (channel, func) => {
let validChannels = ['balance', 'position'];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.sendSync(channel, (event, ...args) => func(...args));
}
},
then I get
Uncaught Error: An object could not be cloned.

I just needed to put the rest logic inside of the receive message scope...

Related

Electron security (is my code is enough?)

I know about #reZach's answer about the electron security, and I know about his electron secured template.. I tried to use it, but it's just too much for today's me, I don't know anything about webpack, I know react not completely etc, the overall complexity of the template is too much for the current me. So I wanted to ask (maybe my code is good enough to be secured and safe?). I made a trading app, so I cannot give it to people not being sure that it is safe to use. I used just vanilla JS, express, localtunnel for receiving webhooks, and several modules. In short here's the code:
main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs');
const path = require('path');
const Store = require('electron-store');
const store = new Store();
const CryptoJS = require('crypto-js');
const axios = require('axios');
const WebSocket = require('ws');
const express = require('express');
const bodyParser = require('body-parser');
const localtunnel = require('localtunnel');
(async () => {
const tunnel = await localtunnel({ port: 3000, subdomain: 'mysubdomain' });
// the assigned public url for your tunnel
// i.e. https://abcdefgjhij.localtunnel.me
console.log(tunnel.url);
tunnel.on('close', () => {
// tunnels are closed
console.log('connection closed');
});
})();
const server = express();
const PORT = 3000;
let webhookMsg;
server.use(bodyParser.text());
server.listen(PORT, () => console.log(`Server running on port ${PORT}`));
server.get('/', function (req, res) {
res.send('Server is ready!');
});
server.post('/', (req, res) => {
webhookMsg = req.body;
res.sendStatus(200);
});
let win;
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 500,
height: 550,
titleBarStyle: 'hidden',
webPreferences: {
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
preload: path.join(app.getAppPath(), 'preload.js'), // use a preload script
backgroundThrottling: false,
},
});
// Load app
win.loadFile(path.join(__dirname, './index.html'));
win.setAlwaysOnTop(true, 'screen');
// win.removeMenu();
// rest of code..
}
app.on('ready', createWindow);
// NEXT IS MY APP MAIN LOGICS ON RECEIVING MESSAGES AND SENDING API REQUEST TO AN EXCHANGE PLATFORM AND GIVING BACK THE ANSWERS TO THE 'front-end script js file'
//for example
ipcMain.on('loadPrefs', () => {
const allPrefs = store.get('prefs');
win.webContents.send('prefs', allPrefs);
});
ipcMain.on('giveBalance', async e => {
const [apiKey, secret] = fs.readFileSync('api.txt').toString('UTF8').replace(/\r/g, '').split('\n');
const timestamp = Date.now().toString();
const params = {
api_key: apiKey,
timestamp: timestamp,
};
let orderedParams = '';
Object.keys(params)
.sort()
.forEach(function (key) {
orderedParams += key + '=' + params[key] + '&';
});
orderedParams = orderedParams.substring(0, orderedParams.length - 1);
var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, secret);
hmac.update(orderedParams);
const sign = hmac.finalize().toString(CryptoJS.enc.Hex);
try {
const res = await axios.get(`https://api.bybit.com/v2/private/wallet/balance?&api_key=${apiKey}&sign=${sign}&timestamp=${timestamp}`);
// console.log(res.data);
const responseObj = res.data.result.USDT.equity;
win.webContents.send('balance', { responseObj });
} catch (error) {
// console.log(error);
}
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => {
ipcRenderer.removeAllListeners(channel);
let validChannels = [
// myChannelsList
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
ipcRenderer.removeAllListeners(channel);
let validChannels = [
// myResponseChannelsList
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
In short, I just copied this reZach's example from this answer and added my JS logics to it.
Could you please tell is it secured and safe to use, I just worry that somebody can get the user's api keys for example and take their money away:) Thank you

Serviceworker subscription working in firefox but not in Chrome. Throws error: "Registration failed - no Service Worker""

I'm trying to setup a service worker for recieving push notifications from a nodejs backend into react. The following code works in Firefox, but Chrome throws the error "Registration failed - no Service Worker". The serviceworker itself seems to be working, but the inclusion of the subscription call on the pushManager throws the error. Does anyone know what is wrong, or do you need more information?
const checkSupport = () => {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
return false;
}
return true;
};
const registerServiceWorker = async () => {
const registration = await navigator.serviceWorker.register('/service.js', { scope: '/' });
await navigator.serviceWorker.ready; // Waits for the serviceworker to be ready
return registration;
};
const requestNotificationPermission = async () => {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
return false;
}
return true;
};
// urlB64ToUint8Array is a magic function that will encode the base64 public key
// to Array buffer which is needed by the subscription option
const urlBase64ToUint8Array = (base64String: string) => {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
const installServiceWorker = async (team: number, semester: number) => {
try {
// Check browser compatability and notification permission
if (!checkSupport() || !(await requestNotificationPermission())) return;
// Register serviceworker
const registration = await registerServiceWorker();
registration.showNotification('Hello notification!');
// Subscribe the user to notifications
const publicVAPID =
'<String>';
const applicationServerKey = urlBase64ToUint8Array(publicVAPID);
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey
});
} catch (error) {
console.error(new Error(error));
}
};
export default installServiceWorker;
The serviceworker looks like this (and seems to be working):
console.log('Hello from serviceworker');
self.addEventListener('activate', async () => {
console.log('activated');
});
self.addEventListener('push', (event) => {
const data = event.data.json();
self.registration.showNotification(data.title, {
body: 'Yay it works!'
});
});
This is the error
Thanks.
So I figured out that the reason the serviceworker wasn't working, was that create-react-app (which I bootstrapped my app with) includes its own serviceworker. When I remove the call to that, my own custom serviceworker started working.

Send data to renderer process

I am using "electron": "^4.1.4" and I am getting my data via knex from an sqlite3 db.
I tried to send the data via an ipcMain call. Find below my main.js
const {
app,
BrowserWindow,
ipcMain
} = require('electron')
// require configuration file
require('dotenv').config()
// database
const knex = require('./config/database')
// services
const {
ScheduledContent
} = require('./service/ScheduledContent')
require('electron-reload')(__dirname);
let mainWindow
const contentService = new ScheduledContent(knex)
ipcMain.on('content-edit', async (e, args) => {
console.log("content-edit - Backend");
let res = await contentService.getAllContent()
event.sender.send('content-edit-res', res)
})
function createWindow() {
mainWindow = new BrowserWindow({
width: 1000,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
// and load the index.html of the app.
mainWindow.loadFile('./public/index.html')
mainWindow.on('closed', function () {
mainWindow = null
})
}
app.on('ready', createWindow)
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', function () {
if (mainWindow === null) createWindow()
})
My renderer.js looks like the following:
const {
ipcRenderer
} = require('electron')
console.log("loaded renderer.js 123")
document.addEventListener('DOMContentLoaded', pageLoaded);
function pageLoaded() {
console.log('The page is loaded');
ipcRenderer.send('content-edit', 'x')
}
ipcRenderer.on('content-edit-res', (event, arg) => {
console.log(arg)
})
I tried to get the data in the frontend when the page is loaded, then I wanted to append it to my <table id="table1">-tag within my index.html
However, I do not get any data in the frontend.
Any suggestions what I am doing wrong?
I appreciate your replies!
As for now, the problem with code...
// event in receive, not e...
ipcMain.on('content-edit', async (event, args) => {
console.log("content-edit - Backend");
let res = await contentService.getAllContent()
event.sender.send('content-edit-res', res)
})
But, as additional information, you also can send data to renderer process with webContents of window.
const BrowserWindow = electron.BrowserWindow;
let win = new BrowserWindow(<your configs>);
win.webContents.send('message', 'Hello from main!');
You can find useful information here

How to detect the end of this stream in NodeJS

Trying to understand how to work with this async stream for a Firebase loader. I'm finding:
when I enable the Firebase Promise, the process stays open even though the last record is output to stdout.
With Firebase code commented as such, the process closes, but the final event is not written to console.
So I'm guessing Firebase is leaving some connection open that needs to be closed before the process exits, and that if I could figure out how to detect the end of the pipe, I can probably do that.
I've commented the Firebase stuff for now to remove it from the problem, but given this code, how do I run some code after the pipe completes? As it is the done function is not executing.
const csv = require('csv');
const fs = require('fs');
const firebase = require('firebase-admin');
const config = require('../../app-config');
const serviceAccount = require('../../service-account');
const jobConf = require('../../data/category.jobfile');
const _ = require('lodash');
// configure firebase
firebase.initializeApp({
credential: firebase.credential.cert(serviceAccount),
databaseURL: config.firebase.databaseURL,
});
console.log('Loading config', jobConf);
const counts = {
total: 0,
new: 0,
updated: 0,
}
const transformer = (record, done) => {
const newObj = {};
const fieldMaps = jobConf.fieldMaps;
Object.keys(record).forEach((key) => {
const destination = fieldMaps[key];
const value = _.get(record, key);
destination ? _.set(newObj, destination, value) : _.set(newObj, key, value);
});
Promise.resolve()
.then(() => {
done(null, JSON.stringify(newObj, null, 2));
});
// // now we look for existing row
// firebase.database().ref(`/categories`).orderByChild('identifier').equalTo(newObj.identifier)
// .once('value')
// .then((snap) => snap.val())
// .then((val) => {
// if (val) {
// console.log('Got value', val);
// newObj.lastUpdated = firebase.database.ServerValue.TIMESTAMP;
// } else {
// newObj.created = firebase.database.ServerValue.TIMESTAMP;
// newObj.lastUpdated = firebase.database.ServerValue.TIMESTAMP;
// counts.total++;
// counts.new++;
// done(null, JSON.stringify(newObj, null, 2));
// }
// })
// .catch((err) => {
// console.log(err);
// done(err);
// })
};
const stream = fs.createReadStream(`../../data/${jobConf.sourceFile}`);
const pipe = stream.pipe(csv.parse({columns: true})).pipe(csv.transform(transformer)).pipe(process.stdout);
pipe.on('finish', function () { // not executed
console.log('Done.');
console.log('Stats: ', counts);
process.exit(0);
});

koa2+koa-router+mysql keep returning 'Not Found'

Background
I am using koa2 with some middlewares to build a basic api framework. But when I use "ctx.body" to send response in my router, the client side always receive "Not Found"
My code
./app.js
const Koa = require('koa');
const app = new Koa();
const config = require('./config');
//Middlewares
const loggerAsync = require('./middleware/logger-async')
const bodyParser = require('koa-bodyparser')
const jsonp = require('koa-jsonp')
app.use(loggerAsync())
app.use(bodyParser())
app.use(jsonp());
//Router
const gateway = require('./router/gateway')
app.use(gateway.routes(), gateway.allowedMethods());
app.use(async(ctx, next) => {
await next();
ctx.response.body = {
success: false,
code: config.code_system,
message: 'wrong path'
}
});
app.listen(3000);
./router/gateway.js
/**
* Created by Administrator on 2017/4/11.
*/
const Router = require('koa-router');
const gateway = new Router();
const df = require('../db/data-fetcher');
const config = require('../config');
const moment = require('moment');
const log4js = require('log4js');
// log4js.configure({
// appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
// categories: { default: { appenders: ['cheese'], level: 'error' } }
// });
const logger = log4js.getLogger('cheese');
logger.setLevel('ERROR');
gateway.get('/gateway', async(ctx, next) => {
let time = ctx.query.time;
if (!time) {
ctx.body = {
success: false,
code: config.code_system,
message: 'Please input running times'
}
} else {
try {
let r = await df(`insert into gateway (g_time, g_result, g_date) values (${time}, '',now())`);
return ctx.body = {
success: true,
code: config.code_success
}
} catch (error) {
logger.error(error.message);
}
}
});
module.exports = gateway;
Then a db wrapper(mysql)
./db/async-db.js
const mysql = require('mysql');
const config = require('../config');
const pool = mysql.createPool({
host: config.database.HOST,
user: config.database.USERNAME,
password: config.database.PASSWORD,
database: config.database.DATABASE
})
let query = (sql, values) => {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, connection) {
if (err) {
reject(err)
} else {
connection.query(sql, values, (err, rows) => {
if (err) {
reject(err)
} else {
resolve(rows)
}
connection.release()
})
}
})
})
}
module.exports = query
./db/data-fetcher.js
const query = require('./async-db')
async function performQuery(sql) {
let dataList = await query(sql)
return dataList
}
module.exports = performQuery;
My running result
When I launch server on port 3000 then accesss via http://localhost:3000/gateway?time=5, it always returns "Not found". But as I can see I have already used
return ctx.body = {
success: true,
code: config.code_success
}
to send response. I debugged and found that the database processing was done well, the new data was inserted well.
when I remove that db inserting line, it works well and returns success info.
let r = await df(`insert into gateway (g_time, g_result, g_date) values (${time}, '',now())`);
Is there anything wrong?
Thanks a lot!
Update 2017/04/27
Now I have found the problem. It's due to my custom middleware
const loggerAsync = require('./middleware/logger-async')
Code are like following -
function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}
module.exports = function () {
return function ( ctx, next ) {
return new Promise( ( resolve, reject ) => {
// 执行中间件的操作
log( ctx )
resolve()
return next()
}).catch(( err ) => {
return next()
})
}
}
I changed it to async/await way then everything is working well.
Could anyone please tell me what's wrong with this middleware?
I guess, your problem is the ./db/data-fetcher.js function. When you are calling
let r = await df(`insert ....`)
your df - function should return a promise.
So try to rewrite your ./db/data-fetcher.js like this (not tested):
const query = require('./async-db')
function performQuery(sql) {
return new Promise((resolve, reject) => {
query(sql).then(
result => {
resolve(result)
}
)
}
}
module.exports = performQuery;
Hope that helps.
correct middleware:
function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}
module.exports = function () {
return function ( ctx, next ) {
log( ctx );
return next()
}
}
reason: when resolve involved; promise chain was completed; response has been sent to client. although middleware remained will involved, but response has gone!
try to understand It seems that if you want to use a common function as middleware, you have to return the next function
nodejs(koa):Can't set headers after they are sent

Resources