Merge pull request #2070 from MetaMask/filter-leak-fix3

Memory leak fixes - stream and filter life cycles
feature/default_network_editable
Dan Finlay 7 years ago committed by GitHub
commit 693655e2da
  1. 1
      CHANGELOG.md
  2. 8
      app/scripts/background.js
  3. 45
      app/scripts/contentscript.js
  4. 15
      app/scripts/lib/createLoggerMiddleware.js
  5. 9
      app/scripts/lib/createOriginMiddleware.js
  6. 13
      app/scripts/lib/createProviderMiddleware.js
  7. 69
      app/scripts/lib/inpage-provider.js
  8. 48
      app/scripts/lib/obj-multiplex.js
  9. 16
      app/scripts/lib/port-stream.js
  10. 22
      app/scripts/lib/stream-utils.js
  11. 91
      app/scripts/metamask-controller.js
  12. 6
      package.json

@ -5,6 +5,7 @@
- Add ability to export private keys as a file. - Add ability to export private keys as a file.
- Add ability to export seed words as a file. - Add ability to export seed words as a file.
- Changed state logs to a file download than a clipboard copy. - Changed state logs to a file download than a clipboard copy.
- Fixed a long standing memory leak associated with filters installed by dapps
- Fix link to support center. - Fix link to support center.
## 3.10.0 2017-9-11 ## 3.10.0 2017-9-11

@ -1,6 +1,8 @@
const urlUtil = require('url') const urlUtil = require('url')
const endOfStream = require('end-of-stream') const endOfStream = require('end-of-stream')
const pipe = require('pump') const pipe = require('pump')
const log = require('loglevel')
const extension = require('extensionizer')
const LocalStorageStore = require('obs-store/lib/localStorage') const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform') const storeTransform = require('obs-store/lib/transform')
const ExtensionPlatform = require('./platforms/extension') const ExtensionPlatform = require('./platforms/extension')
@ -9,13 +11,11 @@ const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const NotificationManager = require('./lib/notification-manager.js') const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller') const MetamaskController = require('./metamask-controller')
const extension = require('extensionizer')
const firstTimeState = require('./first-time-state') const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const log = require('loglevel')
window.log = log window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
@ -29,12 +29,12 @@ let popupIsOpen = false
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY }) const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
// initialization flow // initialization flow
initialize().catch(console.error) initialize().catch(log.error)
async function initialize () { async function initialize () {
const initState = await loadStateFromPersistence() const initState = await loadStateFromPersistence()
await setupController(initState) await setupController(initState)
console.log('MetaMask initialization complete.') log.debug('MetaMask initialization complete.')
} }
// //

@ -1,11 +1,12 @@
const fs = require('fs')
const path = require('path')
const pump = require('pump')
const LocalMessageDuplexStream = require('post-message-stream') const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong') const PongStream = require('ping-pong-stream/pong')
const PortStream = require('./lib/port-stream.js') const ObjectMultiplex = require('obj-multiplex')
const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('extensionizer') const extension = require('extensionizer')
const PortStream = require('./lib/port-stream.js')
const fs = require('fs')
const path = require('path')
const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString() const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
// Eventually this streaming injection could be replaced with: // Eventually this streaming injection could be replaced with:
@ -50,22 +51,42 @@ function setupStreams () {
pageStream.pipe(pluginStream).pipe(pageStream) pageStream.pipe(pluginStream).pipe(pageStream)
// setup local multistream channels // setup local multistream channels
const mx = ObjectMultiplex() const mux = new ObjectMultiplex()
mx.on('error', console.error) pump(
mx.pipe(pageStream).pipe(mx) mux,
mx.pipe(pluginStream).pipe(mx) pageStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask Inpage', err)
)
pump(
mux,
pluginStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask Background', err)
)
// connect ping stream // connect ping stream
const pongStream = new PongStream({ objectMode: true }) const pongStream = new PongStream({ objectMode: true })
pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream) pump(
mux,
pongStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask PingPongStream', err)
)
// connect phishing warning stream // connect phishing warning stream
const phishingStream = mx.createStream('phishing') const phishingStream = mux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning) phishingStream.once('data', redirectToPhishingWarning)
// ignore unused channels (handled by background, inpage) // ignore unused channels (handled by background, inpage)
mx.ignoreStream('provider') mux.ignoreStream('provider')
mx.ignoreStream('publicConfig') mux.ignoreStream('publicConfig')
}
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
} }
function shouldInjectWeb3 () { function shouldInjectWeb3 () {

@ -0,0 +1,15 @@
// log rpc activity
module.exports = createLoggerMiddleware
function createLoggerMiddleware({ origin }) {
return function loggerMiddleware (req, res, next, end) {
next((cb) => {
if (res.error) {
log.error('Error in RPC response:\n', res)
}
if (req.isMetamaskInternal) return
log.info(`RPC (${origin}):`, req, '->', res)
cb()
})
}
}

@ -0,0 +1,9 @@
// append dapp origin domain to request
module.exports = createOriginMiddleware
function createOriginMiddleware({ origin }) {
return function originMiddleware (req, res, next, end) {
req.origin = origin
next()
}
}

@ -0,0 +1,13 @@
module.exports = createProviderMiddleware
// forward requests to provider
function createProviderMiddleware({ provider }) {
return (req, res, next, end) => {
provider.sendAsync(req, (err, _res) => {
if (err) return end(err)
res.result = _res.result
end()
})
}
}

@ -1,8 +1,9 @@
const pipe = require('pump') const pump = require('pump')
const StreamProvider = require('web3-stream-provider') const RpcEngine = require('json-rpc-engine')
const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
const createStreamMiddleware = require('json-rpc-middleware-stream')
const LocalStorageStore = require('obs-store') const LocalStorageStore = require('obs-store')
const ObjectMultiplex = require('./obj-multiplex') const ObjectMultiplex = require('obj-multiplex')
const createRandomId = require('./random-id')
module.exports = MetamaskInpageProvider module.exports = MetamaskInpageProvider
@ -10,64 +11,46 @@ function MetamaskInpageProvider (connectionStream) {
const self = this const self = this
// setup connectionStream multiplexing // setup connectionStream multiplexing
var multiStream = self.multiStream = ObjectMultiplex() const mux = self.mux = new ObjectMultiplex()
pipe( pump(
connectionStream, connectionStream,
multiStream, mux,
connectionStream, connectionStream,
(err) => logStreamDisconnectWarning('MetaMask', err) (err) => logStreamDisconnectWarning('MetaMask', err)
) )
// subscribe to metamask public config (one-way) // subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
pipe( pump(
multiStream.createStream('publicConfig'), mux.createStream('publicConfig'),
self.publicConfigStore, self.publicConfigStore,
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
) )
// ignore phishing warning message (handled elsewhere) // ignore phishing warning message (handled elsewhere)
multiStream.ignoreStream('phishing') mux.ignoreStream('phishing')
// connect to async provider // connect to async provider
const asyncProvider = self.asyncProvider = new StreamProvider() const streamMiddleware = createStreamMiddleware()
pipe( pump(
asyncProvider, streamMiddleware.stream,
multiStream.createStream('provider'), mux.createStream('provider'),
asyncProvider, streamMiddleware.stream,
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
) )
// start and stop polling to unblock first block lock
self.idMap = {} // handle sendAsync requests via dapp-side rpc engine
const rpcEngine = new RpcEngine()
rpcEngine.push(createIdRemapMiddleware())
rpcEngine.push(streamMiddleware)
self.rpcEngine = rpcEngine
} }
// handle sendAsync requests via asyncProvider // handle sendAsync requests via asyncProvider
// also remap ids inbound and outbound // also remap ids inbound and outbound
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
const self = this const self = this
self.rpcEngine.handle(payload, cb)
// rewrite request ids
const request = eachJsonMessage(payload, (_message) => {
const message = Object.assign({}, _message)
const newId = createRandomId()
self.idMap[newId] = message.id
message.id = newId
return message
})
// forward to asyncProvider
self.asyncProvider.sendAsync(request, (err, _res) => {
if (err) return cb(err)
// transform messages to original ids
const res = eachJsonMessage(_res, (message) => {
const oldId = self.idMap[message.id]
delete self.idMap[message.id]
message.id = oldId
return message
})
cb(null, res)
})
} }
@ -124,14 +107,6 @@ MetamaskInpageProvider.prototype.isMetaMask = true
// util // util
function eachJsonMessage (payload, transformFn) {
if (Array.isArray(payload)) {
return payload.map(transformFn)
} else {
return transformFn(payload)
}
}
function logStreamDisconnectWarning (remoteLabel, err) { function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack if (err) warningMsg += '\n' + err.stack

@ -1,48 +0,0 @@
const through = require('through2')
module.exports = ObjectMultiplex
function ObjectMultiplex (opts) {
opts = opts || {}
// create multiplexer
const mx = through.obj(function (chunk, enc, cb) {
const name = chunk.name
const data = chunk.data
if (!name) {
console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`)
return cb()
}
const substream = mx.streams[name]
if (!substream) {
console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`)
} else {
if (substream.push) substream.push(data)
}
return cb()
})
mx.streams = {}
// create substreams
mx.createStream = function (name) {
const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) {
mx.push({
name: name,
data: chunk,
})
return cb()
})
mx.on('end', function () {
return substream.emit('end')
})
if (opts.error) {
mx.on('error', function () {
return substream.emit('error')
})
}
return substream
}
// ignore streams (dont display orphaned data warning)
mx.ignoreStream = function (name) {
mx.streams[name] = true
}
return mx
}

@ -1,5 +1,6 @@
const Duplex = require('readable-stream').Duplex const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits const inherits = require('util').inherits
const noop = function(){}
module.exports = PortDuplexStream module.exports = PortDuplexStream
@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) {
if (Buffer.isBuffer(msg)) { if (Buffer.isBuffer(msg)) {
delete msg._isBuffer delete msg._isBuffer
var data = new Buffer(msg) var data = new Buffer(msg)
// console.log('PortDuplexStream - saw message as buffer', data)
this.push(data) this.push(data)
} else { } else {
// console.log('PortDuplexStream - saw message', msg)
this.push(msg) this.push(msg)
} }
} }
PortDuplexStream.prototype._onDisconnect = function () { PortDuplexStream.prototype._onDisconnect = function () {
try { this.destroy()
this.push(null)
} catch (err) {
this.emit('error', err)
}
} }
// stream plumbing // stream plumbing
@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
if (Buffer.isBuffer(msg)) { if (Buffer.isBuffer(msg)) {
var data = msg.toJSON() var data = msg.toJSON()
data._isBuffer = true data._isBuffer = true
// console.log('PortDuplexStream - sent message as buffer', data)
this._port.postMessage(data) this._port.postMessage(data)
} else { } else {
// console.log('PortDuplexStream - sent message', msg)
this._port.postMessage(msg) this._port.postMessage(msg)
} }
} catch (err) { } catch (err) {
// console.error(err)
return cb(new Error('PortDuplexStream - disconnected')) return cb(new Error('PortDuplexStream - disconnected'))
} }
cb() cb()
} }
// util
function noop () {}

@ -1,6 +1,6 @@
const Through = require('through2') const Through = require('through2')
const endOfStream = require('end-of-stream') const ObjectMultiplex = require('obj-multiplex')
const ObjectMultiplex = require('./obj-multiplex') const pump = require('pump')
module.exports = { module.exports = {
jsonParseStream: jsonParseStream, jsonParseStream: jsonParseStream,
@ -23,14 +23,14 @@ function jsonStringifyStream () {
} }
function setupMultiplex (connectionStream) { function setupMultiplex (connectionStream) {
var mx = ObjectMultiplex() const mux = new ObjectMultiplex()
connectionStream.pipe(mx).pipe(connectionStream) pump(
endOfStream(mx, function (err) { connectionStream,
mux,
connectionStream,
(err) => {
if (err) console.error(err) if (err) console.error(err)
}) }
endOfStream(connectionStream, function (err) { )
if (err) console.error(err) return mux
mx.destroy()
})
return mx
} }

@ -1,12 +1,18 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const extend = require('xtend') const extend = require('xtend')
const promiseToCallback = require('promise-to-callback') const promiseToCallback = require('promise-to-callback')
const pipe = require('pump') const pump = require('pump')
const Dnode = require('dnode') const Dnode = require('dnode')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const EthStore = require('./lib/eth-store') const EthStore = require('./lib/eth-store')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const streamIntoProvider = require('web3-stream-provider/handler') const RpcEngine = require('json-rpc-engine')
const debounce = require('debounce')
const createEngineStream = require('json-rpc-middleware-stream/engineStream')
const createFilterMiddleware = require('eth-json-rpc-filters')
const createOriginMiddleware = require('./lib/createOriginMiddleware')
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const createProviderMiddleware = require('./lib/createProviderMiddleware')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const KeyringController = require('./keyring-controller') const KeyringController = require('./keyring-controller')
const NetworkController = require('./controllers/network') const NetworkController = require('./controllers/network')
@ -24,8 +30,6 @@ const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify') const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies') const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url') const getBuyEthUrl = require('./lib/buy-eth-url')
const debounce = require('debounce')
const version = require('../manifest.json').version const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
@ -77,12 +81,13 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
this.blockTracker = this.provider
// eth data query tools // eth data query tools
this.ethQuery = new EthQuery(this.provider) this.ethQuery = new EthQuery(this.provider)
this.ethStore = new EthStore({ this.ethStore = new EthStore({
provider: this.provider, provider: this.provider,
blockTracker: this.provider, blockTracker: this.blockTracker,
}) })
// key mgmt // key mgmt
@ -109,7 +114,7 @@ module.exports = class MetamaskController extends EventEmitter {
getNetwork: this.networkController.getNetworkState.bind(this), getNetwork: this.networkController.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController), signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider, provider: this.provider,
blockTracker: this.provider, blockTracker: this.blockTracker,
ethQuery: this.ethQuery, ethQuery: this.ethQuery,
ethStore: this.ethStore, ethStore: this.ethStore,
}) })
@ -337,36 +342,43 @@ module.exports = class MetamaskController extends EventEmitter {
setupUntrustedCommunication (connectionStream, originDomain) { setupUntrustedCommunication (connectionStream, originDomain) {
// Check if new connection is blacklisted // Check if new connection is blacklisted
if (this.blacklistController.checkForPhishing(originDomain)) { if (this.blacklistController.checkForPhishing(originDomain)) {
console.log('MetaMask - sending phishing warning for', originDomain) log.debug('MetaMask - sending phishing warning for', originDomain)
this.sendPhishingWarning(connectionStream, originDomain) this.sendPhishingWarning(connectionStream, originDomain)
return return
} }
// setup multiplexing // setup multiplexing
const mx = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
// connect features // connect features
this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain)
this.setupPublicConfig(mx.createStream('publicConfig')) this.setupPublicConfig(mux.createStream('publicConfig'))
} }
setupTrustedCommunication (connectionStream, originDomain) { setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing // setup multiplexing
const mx = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
// connect features // connect features
this.setupControllerConnection(mx.createStream('controller')) this.setupControllerConnection(mux.createStream('controller'))
this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupProviderConnection(mux.createStream('provider'), originDomain)
} }
sendPhishingWarning (connectionStream, hostname) { sendPhishingWarning (connectionStream, hostname) {
const mx = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
const phishingStream = mx.createStream('phishing') const phishingStream = mux.createStream('phishing')
phishingStream.write({ hostname }) phishingStream.write({ hostname })
} }
setupControllerConnection (outStream) { setupControllerConnection (outStream) {
const api = this.getApi() const api = this.getApi()
const dnode = Dnode(api) const dnode = Dnode(api)
outStream.pipe(dnode).pipe(outStream) pump(
outStream,
dnode,
outStream,
(err) => {
if (err) log.error(err)
}
)
dnode.on('remote', (remote) => { dnode.on('remote', (remote) => {
// push updates to popup // push updates to popup
const sendUpdate = remote.sendUpdate.bind(remote) const sendUpdate = remote.sendUpdate.bind(remote)
@ -374,27 +386,42 @@ module.exports = class MetamaskController extends EventEmitter {
}) })
} }
setupProviderConnection (outStream, originDomain) { setupProviderConnection (outStream, origin) {
streamIntoProvider(outStream, this.provider, onRequest, onResponse) // setup json rpc engine stack
// append dapp origin domain to request const engine = new RpcEngine()
function onRequest (request) {
request.origin = originDomain // create filter polyfill middleware
} const filterMiddleware = createFilterMiddleware({
// log rpc activity provider: this.provider,
function onResponse (err, request, response) { blockTracker: this.blockTracker,
if (err) return console.error(err) })
if (response.error) {
console.error('Error in RPC response:\n', response) engine.push(createOriginMiddleware({ origin }))
} engine.push(createLoggerMiddleware({ origin }))
if (request.isMetamaskInternal) return engine.push(filterMiddleware)
log.info(`RPC (${originDomain}):`, request, '->', response) engine.push(createProviderMiddleware({ provider: this.provider }))
// setup connection
const providerStream = createEngineStream({ engine })
pump(
outStream,
providerStream,
outStream,
(err) => {
// cleanup filter polyfill middleware
filterMiddleware.destroy()
if (err) log.error(err)
} }
)
} }
setupPublicConfig (outStream) { setupPublicConfig (outStream) {
pipe( pump(
this.publicConfigStore, this.publicConfigStore,
outStream outStream,
(err) => {
if (err) log.error(err)
}
) )
} }

@ -68,6 +68,7 @@
"eth-bin-to-ops": "^1.0.1", "eth-bin-to-ops": "^1.0.1",
"eth-contract-metadata": "^1.1.4", "eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.1.1", "eth-hd-keyring": "^1.1.1",
"eth-json-rpc-filters": "^1.1.0",
"eth-phishing-detect": "^1.1.4", "eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2", "eth-query": "^2.1.2",
"eth-sig-util": "^1.2.2", "eth-sig-util": "^1.2.2",
@ -92,12 +93,15 @@
"iframe-stream": "^3.0.0", "iframe-stream": "^3.0.0",
"inject-css": "^0.1.1", "inject-css": "^0.1.1",
"jazzicon": "^1.2.0", "jazzicon": "^1.2.0",
"json-rpc-engine": "^3.1.0",
"json-rpc-middleware-stream": "^1.0.0",
"loglevel": "^1.4.1", "loglevel": "^1.4.1",
"metamask-logo": "^2.1.2", "metamask-logo": "^2.1.2",
"mississippi": "^1.2.0", "mississippi": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"multiplex": "^6.7.0", "multiplex": "^6.7.0",
"number-to-bn": "^1.7.0", "number-to-bn": "^1.7.0",
"obj-multiplex": "^1.0.0",
"obs-store": "^2.3.1", "obs-store": "^2.3.1",
"once": "^1.3.3", "once": "^1.3.3",
"ping-pong-stream": "^1.0.0", "ping-pong-stream": "^1.0.0",
@ -118,7 +122,7 @@
"react-select": "^1.0.0-rc.2", "react-select": "^1.0.0-rc.2",
"react-simple-file-input": "^1.0.0", "react-simple-file-input": "^1.0.0",
"react-tooltip-component": "^0.3.0", "react-tooltip-component": "^0.3.0",
"readable-stream": "^2.1.2", "readable-stream": "^2.3.3",
"redux": "^3.0.5", "redux": "^3.0.5",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",

Loading…
Cancel
Save