modules/ipc/methods/base.js
const _ = require('../../utils/underscore.js');
const Q = require('bluebird');
const log = require('../../utils/logger').create('method');
const Windows = require('../../windows');
const db = require('../../db');
import ethereumNodeRemote from '../../ethereumNodeRemote';
/**
* Process a request.
*
* This is the base class for all specialized request processors.
*/
module.exports = class BaseProcessor {
constructor(name, ipcProviderBackend) {
this._log = log.create(name);
this._ipcProviderBackend = ipcProviderBackend;
this.ERRORS = this._ipcProviderBackend.ERRORS;
this.remoteIgnoreMethods = [
'eth_mining',
'eth_accounts',
'eth_coinbase',
'personal_newAccount',
'personal_signTransaction',
'eth_subscribe',
'eth_unsubscribe'
];
}
/**
* Execute given request.
* @param {Object} conn IPCProviderBackend connection data.
* @param {Object|Array} payload payload
* @return {Promise}
*/
async exec(conn, payload) {
this._log.trace('Execute request', payload);
if (Array.isArray(payload)) {
return await this._handleArrayExec(payload, conn);
} else {
return await this._handleObjectExec(payload, conn);
}
}
_handleObjectExec(payload, conn) {
return this._sendPayload(payload, conn);
}
async _handleArrayExec(payload, conn) {
// If on local node, send batch transaction.
// Otherwise, iterate through the batch to send over remote node since infura does not have batch support yet.
const isRemote = store.getState().nodes.active === 'remote';
if (!isRemote) {
this._log.trace(
`Sending batch request to local node: ${payload
.map(req => {
return req.payload;
})
.join(', ')}`
);
const ret = await conn.socket.send(payload, { fullResult: true });
this._log.trace(
`Result from batch request: ${payload
.map(req => {
return req.payload;
})
.join(', ')}`
);
return ret.result;
}
const result = [];
payload.forEach(value => {
result.push(this._handleObjectExec(value, conn));
});
return new Promise(async resolve => {
resolve(await Promise.all(result));
});
}
async _sendPayload(payload, conn) {
if (this._shouldSendToRemote(payload, conn)) {
this._log.trace(`Sending request to remote node: ${payload.method}`);
const result = await this._sendToRemote(payload);
this._log.trace(
`Result from remote node: ${payload.method} (id: ${payload.id})`
);
return result;
} else {
this._log.trace(`Sending request to local node: ${payload.method}`);
const result = await conn.socket.send(payload, { fullResult: true });
this._log.trace(
`Result from local node: ${payload.method} (id: ${payload.id})`
);
return result.result;
}
}
_shouldSendToRemote(payload, conn) {
// Do NOT send to the remote node if: (all conditions must be satisfied)
// 1. the local node is synced
const isRemote = store.getState().nodes.active === 'remote';
if (!isRemote) {
return false;
}
// 2. method is on the ignore list
const method = payload.method;
if (this.remoteIgnoreMethods.includes(method)) {
return false;
}
// 3. the method is
// net_peerCount | eth_syncing | eth_subscribe[syncing]
// and is originating from the mist interface
// dev: localhost:3000, production: app.asar/interface/index.html
if (
conn &&
conn.owner &&
conn.owner.history &&
(conn.owner.history[0].startsWith('http://localhost:3000') ||
conn.owner.history[0].indexOf('app.asar/interface/index.html') > -1)
) {
if (
method === 'net_peerCount' ||
method === 'eth_syncing' ||
(method === 'eth_subscribe' && payload.params[0] === 'syncing')
) {
return false;
}
}
return true;
}
_sendToRemote(payload, retry = false) {
return new Promise(async (resolve, reject) => {
const requestId = await ethereumNodeRemote.send(
payload.method,
payload.params
);
if (!requestId) {
const errorMessage = `No request id for method ${payload.method} (${
payload.params
})`;
reject(errorMessage);
this._log.error(errorMessage);
return;
}
const callback = data => {
if (!data) {
return;
}
try {
data = JSON.parse(data);
} catch (error) {
const errorMessage = `Error parsing data: ${data}`;
this._log.trace(errorMessage);
reject(errorMessage);
}
if (data.id === requestId) {
resolve(data);
// TODO: remove listener
// ethereumNodeRemote.ws.removeListener('message', callback);
}
};
ethereumNodeRemote.ws.on('message', callback);
});
}
_isAdminConnection(conn) {
// main window or popupwindows - always allow requests
const wnd = Windows.getById(conn.id);
const tab = db.getCollection('UI_tabs').findOne({ webviewId: conn.id });
return (
(wnd && (wnd.type === 'main' || wnd.isPopup)) ||
(tab && _.get(tab, 'permissions.admin') === true)
);
}
/**
Sanitize a request payload.
This may modify the input payload object.
@param {Object} conn The connection.
@param {Object} payload The request payload.
@param {Boolean} isPartOfABatch Whether it's part of a batch payload.
*/
sanitizeRequestPayload(conn, payload, isPartOfABatch) {
this._log.trace('Sanitize request payload', payload);
this._sanitizeRequestResponsePayload(conn, payload, isPartOfABatch);
}
/**
Sanitize a response payload.
This may modify the input payload object.
@param {Object} conn The connection.
@param {Object} payload The request payload.
@param {Boolean} isPartOfABatch Whether it's part of a batch payload.
*/
sanitizeResponsePayload(conn, payload, isPartOfABatch) {
this._log.trace('Sanitize response payload', payload);
this._sanitizeRequestResponsePayload(conn, payload, isPartOfABatch);
}
/**
Sanitize a request or response payload.
This may modify the input payload object.
@param {Object} conn The connection.
@param {Object} payload The request payload.
@param {Boolean} isPartOfABatch Whether it's part of a batch payload.
*/
_sanitizeRequestResponsePayload(conn, payload, isPartOfABatch) {
if (!_.isObject(payload)) {
throw this.ERRORS.INVALID_PAYLOAD;
}
if (this._isAdminConnection(conn)) {
return;
}
// prevent dapps from acccesing admin endpoints
if (!/^eth_|^bzz_|^shh_|^net_|^web3_|^db_/.test(payload.method)) {
delete payload.result;
const err = _.clone(this.ERRORS.METHOD_DENIED);
err.message = err.message.replace('__method__', `"${payload.method}"`);
payload.error = err;
}
}
};