fix(@embark/profiler): Fix profile output and update messaging

The profiler was not formatted correctly in the console as `util.inspect` was being applied to the ASCII table before being output to the console REPL.

In addition, functions containing solidity assertions (require, revert, assert) that cause the function to fail when estimating gas would print an error to embark’s console log, and would show nothing as their gas estimate in the table.

Do not `util.inspect` command output if the result is a string. For API commands being run, allow the command to specify whether or not the output of the command should be HTML escaped. This could pose security risks!

For functions that have errors during gas estimation, add a message in the embark console explaining that the error may be due to solidity assertions in the function that prevent the gas from being estimated correctly. For functions that error, show `-ERROR-` in the gas estimation column. Additionally, show a description in the table footer explaining that the error may be due to solidity assertions in the function.

For events with no gas estimate, show `-EVENT-` in the gas estimate column of the profile table, and a description in the table footer explaining that there is no gas estimate for events.

### Warnings
This PR allows the console command to specify whether or not it should allow for a string result of the command to be HTML-escaped before being sent in the API response. Combining this with Cockpit’s `dangerouslySetInnerHTML`, this could allow a plugin to register a console command that injects XSS in to Cockpit.

![Imgur](https://i.imgur.com/1Rqkjyx.png)
![Imgur](https://i.imgur.com/s6Y1Ecy.png)
![Imgur](https://i.imgur.com/BhsjkBs.png)
This commit is contained in:
emizzle 2020-02-13 13:14:36 +11:00 committed by Iuri Matias
parent 1e9ed81ff3
commit 74e2935846
3 changed files with 35 additions and 14 deletions

View File

@ -50,7 +50,7 @@ export default class Console {
const error = { name: "Console error", message: err, stack: err.stack };
return cb(error);
}
cb(null, util.inspect(result));
cb(null, typeof result !== "string" ? util.inspect(result) : result);
});
});
this.ipc.on("console:executePartial", (cmd: string, cb: any) => {
@ -93,18 +93,18 @@ export default class Console {
private registerApi() {
const plugin = this.plugins.createPlugin("consoleApi", {});
plugin.registerAPICall("post", "/embark-api/command", (req: any, res: any) => {
this.executeCmd(req.body.command, (err: any, result: any) => {
this.executeCmd(req.body.command, (err: any, result: any, shouldEscapeHtml = true) => {
if (err) {
return res.send({ result: err.message || err });
}
let response = result;
if (typeof result !== "string") {
response = stringify(result, jsonFunctionReplacer, 2);
} else {
} else if (shouldEscapeHtml) {
// Avoid HTML injection in the Cockpit
response = escapeHtml(response);
}
const jsonResponse = {result: response};
const jsonResponse = { result: response };
if (res.headersSent) {
return res.end(jsonResponse);
}

View File

@ -2,7 +2,10 @@ const async = require('async');
const ContractFuzzer = require('./fuzzer.js');
const Web3 = require('web3');
class GasEstimator {
export const GAS_ERROR = ' -ERROR- ';
export const EVENT_NO_GAS = ' -EVENT- ';
export class GasEstimator {
constructor(embark) {
this.embark = embark;
this.logger = embark.logger;
@ -10,6 +13,14 @@ class GasEstimator {
this.fuzzer = new ContractFuzzer(embark);
}
printError(message, name, values = []) {
this.logger.error(`Error getting gas estimate for "${name}(${Object.values(values).join(",")})"`, message);
if (message.includes('always failing transaction')) {
this.logger.error(`This may mean function assertions (revert, assert, require) are preventing the estimate from completing. Gas will be listed as "${GAS_ERROR}" in the profile.`);
}
this.logger.error(''); // new line to separate likely many lines
}
estimateGas(contractName, cb) {
const self = this;
let gasMap = {};
@ -19,13 +30,16 @@ class GasEstimator {
if (err) return cb(err);
let fuzzMap = self.fuzzer.generateFuzz(3, contract);
let contractObj = new web3.eth.Contract(contract.abiDefinition, contract.deployedAddress);
async.each(contract.abiDefinition.filter((x) => x.type !== "event"),
async.each(contract.abiDefinition,
(abiMethod, gasCb) => {
let name = abiMethod.name;
if (abiMethod.type === "constructor") {
// already provided for us
gasMap['constructor'] = parseFloat(contract.gasEstimates.creation.totalCost.toString());
return gasCb(null, name, abiMethod.type);
} else if (abiMethod.type === "event") {
gasMap[name] = EVENT_NO_GAS;
return gasCb(null, name, abiMethod.type);
} else if (abiMethod.type === "fallback") {
gasMap['fallback'] = parseFloat(contract.gasEstimates.external[""].toString());
return gasCb(null, name, abiMethod.type);
@ -35,9 +49,9 @@ class GasEstimator {
// just run it and register it
contractObj.methods[name]
.apply(contractObj.methods[name], [])
.estimateGas((err, gasAmount) => {
.estimateGas({ from: web3.eth.defaultAccount }, (err, gasAmount) => {
if (err) {
self.logger.error(`Error getting gas estimate for "${name}"`, err.message || err);
self.printError(err.message || err, name);
return gasCb(null, name, abiMethod.type);
}
gasMap[name] = gasAmount;
@ -49,13 +63,13 @@ class GasEstimator {
contractObj.methods[name].apply(contractObj.methods[name], values)
.estimateGas((err, gasAmount) => {
if (err) {
self.logger.error(`Error getting gas estimate for "${name}"`, err.message || err);
self.printError(err.message || err, name, values);
}
getVarianceCb(null, gasAmount);
});
}, (err, variance) => {
if (variance.every(v => v === variance[0])) {
gasMap[name] = variance[0];
gasMap[name] = variance[0] ?? GAS_ERROR;
} else {
// get average
let sum = variance.reduce(function(memo, num) { return memo + num; });
@ -77,5 +91,3 @@ class GasEstimator {
});
}
}
module.exports = GasEstimator;

View File

@ -1,7 +1,7 @@
import { warnIfPackageNotDefinedLocally } from 'embark-utils';
const asciiTable = require('ascii-table');
const GasEstimator = require('./gasEstimator.js');
import { GasEstimator, GAS_ERROR, EVENT_NO_GAS } from './gasEstimator';
class Profiler {
constructor(embark, _options) {
@ -60,10 +60,19 @@ class Profiler {
let table = new asciiTable(contractName);
table.setHeading('Function', 'Payable', 'Mutability', 'Inputs', 'Outputs', 'Gas Estimates');
table.setAlign(5, asciiTable.RIGHT);
profileObj.methods.forEach((method) => {
table.addRow(method.name, method.payable, method.mutability, self.formatParams(method.inputs), self.formatParams(method.outputs), method.gasEstimates);
});
return returnCb(null, table.toString());
const strTable = table.toString();
let result = [strTable];
if (strTable.includes(GAS_ERROR)) {
result.push(`${GAS_ERROR} indicates there was an error during gas estimation (see console for details).`);
}
if (strTable.includes(EVENT_NO_GAS)) {
result.push(`${EVENT_NO_GAS} indicates the method is an event, and therefore no gas was estimated.`);
}
return returnCb(null, result.join('\n'), false);
});
}