1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 09:23:21 +01:00

fix: replace dnode background with JSON-RPC (#10627)

fixes #10090
This commit is contained in:
Shane 2021-03-18 11:23:46 -07:00 committed by GitHub
parent 480512d14f
commit b50fe3184a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 339 additions and 123 deletions

View File

@ -1,7 +1,7 @@
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import BN from 'bn.js';
import createId from '../lib/random-id';
import createId from '../../../shared/modules/random-id';
import { bnToHex } from '../lib/util';
import getFetchWithTimeout from '../../../shared/modules/fetch-with-timeout';

View File

@ -22,7 +22,7 @@ import {
import { NETWORK_EVENTS } from './network';
const IncomingTransactionsController = proxyquire('./incoming-transactions', {
'../lib/random-id': { default: () => 54321 },
'../../../shared/modules/random-id': { default: () => 54321 },
}).default;
const FAKE_CHAIN_ID = '0x1338';

View File

@ -1,7 +1,7 @@
import EventEmitter from 'safe-event-emitter';
import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel';
import createId from '../../lib/random-id';
import createId from '../../../../shared/modules/random-id';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { transactionMatchesNetwork } from '../../../../shared/modules/transaction.utils';

View File

@ -0,0 +1,33 @@
import { ethErrors, serializeError } from 'eth-rpc-errors';
const createMetaRPCHandler = (api, outStream) => {
return (data) => {
if (!api[data.method]) {
outStream.write({
jsonrpc: '2.0',
error: ethErrors.rpc.methodNotFound({
message: `${data.method} not found`,
}),
id: data.id,
});
return;
}
api[data.method](...data.params, (err, result) => {
if (err) {
outStream.write({
jsonrpc: '2.0',
error: serializeError(err, { shouldIncludeStack: true }),
id: data.id,
});
} else {
outStream.write({
jsonrpc: '2.0',
result,
id: data.id,
});
}
});
};
};
export default createMetaRPCHandler;

View File

@ -0,0 +1,61 @@
import assert from 'assert';
import { obj as createThoughStream } from 'through2';
import createMetaRPCHandler from './createMetaRPCHandler';
describe('createMetaRPCHandler', function () {
it('can call the api when handler receives a JSON-RPC request', function (done) {
const api = {
foo: (param1) => {
assert.strictEqual(param1, 'bar');
done();
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
});
it('can write the response to the outstream when api callback is called', function (done) {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(null, 'foobarbaz');
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
streamTest.on('data', (data) => {
assert.strictEqual(data.result, 'foobarbaz');
streamTest.end();
done();
});
});
it('can write the error to the outstream when api callback is called with an error', function (done) {
const api = {
foo: (param1, cb) => {
assert.strictEqual(param1, 'bar');
cb(new Error('foo-error'));
},
};
const streamTest = createThoughStream();
const handler = createMetaRPCHandler(api, streamTest);
handler({
id: 1,
method: 'foo',
params: ['bar'],
});
streamTest.on('data', (data) => {
assert.strictEqual(data.error.message, 'foo-error');
streamTest.end();
done();
});
});
});

View File

@ -5,8 +5,8 @@ import { ethErrors } from 'eth-rpc-errors';
import log from 'loglevel';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from '../../../shared/modules/random-id';
import { addHexPrefix } from './util';
import createId from './random-id';
const hexRe = /^[0-9A-Fa-f]+$/gu;

View File

@ -4,7 +4,7 @@ import { ethErrors } from 'eth-rpc-errors';
import log from 'loglevel';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from './random-id';
import createId from '../../../shared/modules/random-id';
/**
* Represents, and contains data about, an 'eth_getEncryptionPublicKey' type request. These are created when

View File

@ -4,7 +4,7 @@ import ethUtil from 'ethereumjs-util';
import { ethErrors } from 'eth-rpc-errors';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from './random-id';
import createId from '../../../shared/modules/random-id';
/**
* Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for

View File

@ -0,0 +1,81 @@
import { EthereumRpcError } from 'eth-rpc-errors';
import SafeEventEmitter from 'safe-event-emitter';
import createRandomId from '../../../shared/modules/random-id';
class MetaRPCClient {
constructor(connectionStream) {
this.connectionStream = connectionStream;
this.notificationChannel = new SafeEventEmitter();
this.requests = new Map();
this.connectionStream.on('data', this.handleResponse.bind(this));
this.connectionStream.on('end', this.close.bind(this));
}
onNotification(handler) {
this.notificationChannel.addListener('notification', (data) => {
handler(data);
});
}
close() {
this.notificationChannel.removeAllListeners();
}
handleResponse(data) {
const { id, result, error, method, params } = data;
const cb = this.requests.get(id);
if (method && params && id) {
// dont handle server-side to client-side requests
return;
}
if (method && params && !id) {
// handle servier-side to client-side notification
this.notificationChannel.emit('notification', data);
return;
}
if (!cb) {
// not found in request list
return;
}
if (error) {
const e = new EthereumRpcError(error.code, error.message, error.data);
// preserve the stack from serializeError
e.stack = error.stack;
this.requests.delete(id);
cb(e);
return;
}
this.requests.delete(id);
cb(null, result);
}
}
const metaRPCClientFactory = (connectionStream) => {
const metaRPCClient = new MetaRPCClient(connectionStream);
return new Proxy(metaRPCClient, {
get: (object, property) => {
if (object[property]) {
return object[property];
}
return (...p) => {
const cb = p[p.length - 1];
const params = p.slice(0, -1);
const id = createRandomId();
object.requests.set(id, cb);
object.connectionStream.write({
jsonrpc: '2.0',
method: property,
params,
id,
});
};
},
});
};
export default metaRPCClientFactory;

View File

@ -0,0 +1,88 @@
import assert from 'assert';
import { obj as createThoughStream } from 'through2';
import metaRPCClientFactory from './metaRPCClientFactory';
describe('metaRPCClientFactory', function () {
it('should be able to make an rpc request with the method', function (done) {
const streamTest = createThoughStream((chunk) => {
assert.strictEqual(chunk.method, 'foo');
done();
});
const metaRPCClient = metaRPCClientFactory(streamTest);
metaRPCClient.foo();
});
it('should be able to make an rpc request/response with the method and params and node-style callback', function (done) {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
// make a "foo" method call
metaRPCClient.foo('bar', (_, result) => {
assert.strictEqual(result, 'foobarbaz');
done();
});
// fake a response
metaRPCClient.requests.forEach((_, key) => {
streamTest.write({
jsonrpc: '2.0',
id: key,
result: 'foobarbaz',
});
});
});
it('should be able to make an rpc request/error with the method and params and node-style callback', function (done) {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
// make a "foo" method call
metaRPCClient.foo('bar', (err) => {
assert.strictEqual(err.message, 'foo-message');
assert.strictEqual(err.code, 1);
done();
});
metaRPCClient.requests.forEach((_, key) => {
streamTest.write({
jsonrpc: '2.0',
id: key,
error: {
code: 1,
message: 'foo-message',
},
});
});
});
it('should be able to make an rpc request/response with the method and params and node-style callback with multiple instances of metaRPCClientFactory and the same connectionStream', function (done) {
const streamTest = createThoughStream();
const metaRPCClient = metaRPCClientFactory(streamTest);
const metaRPCClient2 = metaRPCClientFactory(streamTest);
// make a "foo" method call, followed by "baz" call on metaRPCClient2
metaRPCClient.foo('bar', (_, result) => {
assert.strictEqual(result, 'foobarbaz');
metaRPCClient2.baz('bar', (err) => {
assert.strictEqual(err, null);
done();
});
});
// fake a response
metaRPCClient.requests.forEach((_, key) => {
streamTest.write({
jsonrpc: '2.0',
id: key,
result: 'foobarbaz',
});
});
// fake client2's response
metaRPCClient2.requests.forEach((_, key) => {
streamTest.write({
jsonrpc: '2.0',
id: key,
result: 'foobarbaz',
});
});
});
});

View File

@ -5,8 +5,8 @@ import { ethErrors } from 'eth-rpc-errors';
import log from 'loglevel';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from '../../../shared/modules/random-id';
import { addHexPrefix } from './util';
import createId from './random-id';
const hexRe = /^[0-9A-Fa-f]+$/gu;

View File

@ -8,7 +8,7 @@ import log from 'loglevel';
import jsonschema from 'jsonschema';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import { METAMASK_CONTROLLER_EVENTS } from '../metamask-controller';
import createId from './random-id';
import createId from '../../../shared/modules/random-id';
/**
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a

View File

@ -1,6 +1,5 @@
import EventEmitter from 'events';
import pump from 'pump';
import Dnode from 'dnode';
import { ObservableStore } from '@metamask/obs-store';
import { storeAsStream } from '@metamask/obs-store/dist/asStream';
import { JsonRpcEngine } from 'json-rpc-engine';
@ -60,6 +59,7 @@ import accountImporter from './account-import-strategies';
import seedPhraseVerifier from './lib/seed-phrase-verifier';
import MetaMetricsController from './controllers/metametrics';
import { segment, segmentLegacy } from './lib/segment';
import createMetaRPCHandler from './lib/createMetaRPCHandler';
export const METAMASK_CONTROLLER_EVENTS = {
// Fired after state changes that impact the extension badge (unapproved msg count)
@ -598,7 +598,7 @@ export default class MetamaskController extends EventEmitter {
/**
* Returns an Object containing API Callback Functions.
* These functions are the interface for the UI.
* The API object can be transmitted over a stream with dnode.
* The API object can be transmitted over a stream via JSON-RPC.
*
* @returns {Object} Object containing API functions.
*/
@ -1996,36 +1996,34 @@ export default class MetamaskController extends EventEmitter {
}
/**
* A method for providing our API over a stream using Dnode.
* A method for providing our API over a stream using JSON-RPC.
* @param {*} outStream - The stream to provide our API over.
*/
setupControllerConnection(outStream) {
const api = this.getApi();
// the "weak: false" option is for nodejs only (eg unit tests)
// it is a workaround for node v12 support
const dnode = Dnode(api, { weak: false });
// report new active controller connection
this.activeControllerConnections += 1;
this.emit('controllerConnectionChanged', this.activeControllerConnections);
// connect dnode api to remote connection
pump(outStream, dnode, outStream, (err) => {
// report new active controller connection
// set up postStream transport
outStream.on('data', createMetaRPCHandler(api, outStream));
const handleUpdate = (update) => {
// send notification to client-side
outStream.write({
jsonrpc: '2.0',
method: 'sendUpdate',
params: [update],
});
};
this.on('update', handleUpdate);
outStream.on('end', () => {
this.activeControllerConnections -= 1;
this.emit(
'controllerConnectionChanged',
this.activeControllerConnections,
);
// report any error
if (err) {
log.error(err);
}
});
dnode.on('remote', (remote) => {
// push updates to popup
const sendUpdate = (update) => remote.sendUpdate(update);
this.on('update', sendUpdate);
// remove update listener once the connection ends
dnode.on('end', () => this.removeListener('update', sendUpdate));
this.removeListener('update', handleUpdate);
});
}

View File

@ -1116,7 +1116,7 @@ describe('MetaMaskController', function () {
});
describe('#setupTrustedCommunication', function () {
it('sets up controller dnode api for trusted communication', async function () {
it('sets up controller JSON-RPC api for trusted communication', async function () {
const messageSender = {
url: 'http://mycrypto.com',
tab: {},

View File

@ -1,8 +1,7 @@
import querystring from 'querystring';
import { EventEmitter } from 'events';
import dnode from 'dnode';
import PortStream from 'extension-port-stream';
import extension from 'extensionizer';
import createRandomId from '../../shared/modules/random-id';
import { setupMultiplex } from './lib/stream-utils';
import { getEnvironmentType } from './lib/util';
import ExtensionPlatform from './platforms/extension';
@ -22,36 +21,15 @@ function start() {
});
const connectionStream = new PortStream(extensionPort);
const mx = setupMultiplex(connectionStream);
setupControllerConnection(
mx.createStream('controller'),
(err, metaMaskController) => {
if (err) {
return;
}
const continueLink = document.getElementById('unsafe-continue');
continueLink.addEventListener('click', () => {
metaMaskController.safelistPhishingDomain(suspect.hostname);
window.location.href = suspect.href;
});
},
);
}
function setupControllerConnection(connectionStream, cb) {
const eventEmitter = new EventEmitter();
// the "weak: false" option is for nodejs only (eg unit tests)
// it is a workaround for node v12 support
const metaMaskControllerDnode = dnode(
{
sendUpdate(state) {
eventEmitter.emit('update', state);
},
},
{ weak: false },
);
connectionStream.pipe(metaMaskControllerDnode).pipe(connectionStream);
metaMaskControllerDnode.once('remote', (backgroundConnection) =>
cb(null, backgroundConnection),
);
const backgroundConnection = mx.createStream('controller');
const continueLink = document.getElementById('unsafe-continue');
continueLink.addEventListener('click', () => {
backgroundConnection.write({
jsonrpc: '2.0',
method: 'safelistPhishingDomain',
params: [suspect.hostname],
id: createRandomId(),
});
window.location.href = suspect.href;
});
}

View File

@ -2,11 +2,9 @@
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
import '@formatjs/intl-relativetimeformat/polyfill';
import { EventEmitter } from 'events';
import PortStream from 'extension-port-stream';
import extension from 'extensionizer';
import Dnode from 'dnode';
import Eth from 'ethjs';
import EthQuery from 'eth-query';
import StreamProvider from 'web3-stream-provider';
@ -19,6 +17,7 @@ import {
import ExtensionPlatform from './platforms/extension';
import { setupMultiplex } from './lib/stream-utils';
import { getEnvironmentType } from './lib/util';
import metaRPCClientFactory from './lib/metaRPCClientFactory';
start().catch(log.error);
@ -138,20 +137,6 @@ function setupWeb3Connection(connectionStream) {
* @param {Function} cb - Called when the remote account manager connection is established
*/
function setupControllerConnection(connectionStream, cb) {
const eventEmitter = new EventEmitter();
// the "weak: false" option is for nodejs only (eg unit tests)
// it is a workaround for node v12 support
const backgroundDnode = Dnode(
{
sendUpdate(state) {
eventEmitter.emit('update', state);
},
},
{ weak: false },
);
connectionStream.pipe(backgroundDnode).pipe(connectionStream);
backgroundDnode.once('remote', function (backgroundConnection) {
backgroundConnection.on = eventEmitter.on.bind(eventEmitter);
cb(null, backgroundConnection);
});
const backgroundRPC = metaRPCClientFactory(connectionStream);
cb(null, backgroundRPC);
}

View File

@ -107,7 +107,6 @@
"currency-formatter": "^1.4.2",
"debounce-stream": "^2.0.0",
"deep-freeze-strict": "1.1.1",
"dnode": "^1.2.2",
"end-of-stream": "^1.4.4",
"eth-block-tracker": "^4.4.2",
"eth-ens-namehash": "^2.0.8",

View File

@ -2427,8 +2427,12 @@ export function requestAccountsPermissionWithId(origin) {
* @param {string[]} accounts - The accounts to expose, if any.
*/
export function approvePermissionsRequest(request, accounts) {
return () => {
background.approvePermissionsRequest(request, accounts);
return (dispatch) => {
background.approvePermissionsRequest(request, accounts, (err) => {
if (err) {
dispatch(displayWarning(err.message));
}
});
};
}
@ -2455,8 +2459,12 @@ export function rejectPermissionsRequest(requestId) {
* Clears the given permissions for the given origin.
*/
export function removePermissionsFor(domains) {
return () => {
background.removePermissionsFor(domains);
return (dispatch) => {
background.removePermissionsFor(domains, (err) => {
if (err) {
dispatch(displayWarning(err.message));
}
});
};
}
@ -2464,8 +2472,12 @@ export function removePermissionsFor(domains) {
* Clears all permissions for all domains.
*/
export function clearPermissions() {
return () => {
background.clearPermissions();
return (dispatch) => {
background.clearPermissions((err) => {
if (err) {
dispatch(displayWarning(err.message));
}
});
};
}
@ -2610,7 +2622,11 @@ export function getContractMethodData(data = '') {
return getMethodDataAsync(fourBytePrefix).then(({ name, params }) => {
dispatch(loadingMethodDataFinished());
background.addKnownMethodData(fourBytePrefix, { name, params });
background.addKnownMethodData(fourBytePrefix, { name, params }, (err) => {
if (err) {
dispatch(displayWarning(err.message));
}
});
return { name, params };
});
};

View File

@ -128,8 +128,16 @@ async function startApp(metamaskState, backgroundConnection, opts) {
);
}
backgroundConnection.on('update', function (state) {
store.dispatch(actions.updateMetamaskState(state));
backgroundConnection.onNotification((data) => {
if (data.method === 'sendUpdate') {
store.dispatch(actions.updateMetamaskState(data.params[0]));
} else {
throw new Error(
`Internal JSON-RPC Notification Not Handled:\n\n ${JSON.stringify(
data,
)}`,
);
}
});
// global metamask api - used by tooling

View File

@ -8565,24 +8565,6 @@ dnd-core@^7.4.4:
invariant "^2.2.4"
redux "^4.0.1"
dnode-protocol@~0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dnode-protocol/-/dnode-protocol-0.2.2.tgz#51151d16fc3b5f84815ee0b9497a1061d0d1949d"
integrity sha1-URUdFvw7X4SBXuC5SXoQYdDRlJ0=
dependencies:
jsonify "~0.0.0"
traverse "~0.6.3"
dnode@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/dnode/-/dnode-1.2.2.tgz#4ac3cfe26e292b3b39b8258ae7d94edc58132efa"
integrity sha1-SsPP4m4pKzs5uCWK59lO3FgTLvo=
dependencies:
dnode-protocol "~0.2.2"
jsonify "~0.0.0"
optionalDependencies:
weak "^1.0.0"
dns-packet@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-4.2.0.tgz#3fd6f5ff5a4ec3194ed0b15312693ffe8776b343"
@ -17608,7 +17590,7 @@ nan@2.13.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
nan@^2.0.5, nan@^2.11.1, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.2.1:
nan@^2.11.1, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.2.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
@ -24362,11 +24344,6 @@ tr46@^1.0.0, tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
traverse@~0.6.3:
version "0.6.6"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
@ -25417,14 +25394,6 @@ wcwidth@^1.0.0:
dependencies:
defaults "^1.0.3"
weak@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/weak/-/weak-1.0.1.tgz#ab99aab30706959aa0200cb8cf545bb9cb33b99e"
integrity sha1-q5mqswcGlZqgIAy4z1RbucszuZ4=
dependencies:
bindings "^1.2.1"
nan "^2.0.5"
web3-bzz@1.2.11:
version "1.2.11"
resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.2.11.tgz#41bc19a77444bd5365744596d778b811880f707f"