Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login per site onboarding #7602

Merged
merged 7 commits into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,6 @@ function setupController (initState, initLangCode) {
//
extension.runtime.onConnect.addListener(connectRemote)
extension.runtime.onConnectExternal.addListener(connectExternal)
extension.runtime.onMessage.addListener(controller.onMessage.bind(controller))

const metamaskInternalProcessHash = {
[ENVIRONMENT_TYPE_POPUP]: true,
Expand Down Expand Up @@ -356,10 +355,7 @@ function setupController (initState, initLangCode) {
const portStream = new PortStream(remotePort)
// communication with popup
controller.isClientOpen = true
// construct fake URL for identifying internal messages
const metamaskUrl = new URL(window.location)
metamaskUrl.hostname = 'metamask'
controller.setupTrustedCommunication(portStream, metamaskUrl)
controller.setupTrustedCommunication(portStream, remotePort.sender)

if (processName === ENVIRONMENT_TYPE_POPUP) {
popupIsOpen = true
Expand Down Expand Up @@ -406,13 +402,8 @@ function setupController (initState, initLangCode) {

// communication with page or other extension
function connectExternal (remotePort) {
const senderUrl = new URL(remotePort.sender.url)
let extensionId
if (remotePort.sender.id !== extension.runtime.id) {
extensionId = remotePort.sender.id
}
const portStream = new PortStream(remotePort)
controller.setupUntrustedCommunication(portStream, senderUrl, extensionId)
controller.setupUntrustedCommunication(portStream, remotePort.sender)
}

//
Expand Down
40 changes: 0 additions & 40 deletions app/scripts/contentscript.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
const fs = require('fs')
const path = require('path')
const pump = require('pump')
const log = require('loglevel')
const querystring = require('querystring')
const { Writable } = require('readable-stream')
const LocalMessageDuplexStream = require('post-message-stream')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
Expand Down Expand Up @@ -86,44 +84,6 @@ async function setupStreams () {
(err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err)
)

const onboardingStream = pageMux.createStream('onboarding')
const addCurrentTab = new Writable({
objectMode: true,
write: (chunk, _, callback) => {
if (!chunk) {
return callback(new Error('Malformed onboarding message'))
}

const handleSendMessageResponse = (error, success) => {
if (!error && !success) {
error = extension.runtime.lastError
}
if (error) {
log.error(`Failed to send ${chunk.type} message`, error)
return callback(error)
}
callback(null)
}

try {
if (chunk.type === 'registerOnboarding') {
extension.runtime.sendMessage({ type: 'metamask:registerOnboarding', location: window.location.href }, handleSendMessageResponse)
} else {
throw new Error(`Unrecognized onboarding message type: '${chunk.type}'`)
}
} catch (error) {
log.error(error)
return callback(error)
}
},
})

pump(
onboardingStream,
addCurrentTab,
error => console.error('MetaMask onboarding channel traffic failed', error),
)

// forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxers('provider', pageMux, extensionMux)
forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux)
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/controllers/onboarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class OnboardingController {
* @param {string} location - The location of the site registering
* @param {string} tabId - The id of the tab registering
*/
async registerOnboarding (location, tabId) {
registerOnboarding = async (location, tabId) => {
if (this.completedOnboarding) {
log.debug('Ignoring registerOnboarding; user already onboarded')
return
Expand Down
29 changes: 29 additions & 0 deletions app/scripts/lib/createOnboardingMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import log from 'loglevel'
import extension from 'extensionizer'

/**
* Returns a middleware that intercepts `wallet_registerOnboarding` messages
* @param {{ location: string, tabId: number, registerOnboarding: Function }} opts - The middleware options
* @returns {(req: any, res: any, next: Function, end: Function) => void}
*/
function createOnboardingMiddleware ({ location, tabId, registerOnboarding }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could pull the tabId from the request in a world where #7695 is merged first

Copy link
Member Author

@Gudahtt Gudahtt Dec 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That PR relies upon many things in this PR, but yes I agree that it might be cleaner to do it that way. That can be a follow-up PR.

return async function originMiddleware (req, res, next, end) {
try {
if (req.method !== 'wallet_registerOnboarding') {
next()
return
}
if (tabId && tabId !== extension.tabs.TAB_ID_NONE) {
await registerOnboarding(location, tabId)
} else {
log.debug(`'wallet_registerOnboarding' message from ${location} ignored due to missing tabId`)
}
res.result = true
end()
} catch (error) {
end(error)
}
}
}

export default createOnboardingMiddleware
109 changes: 45 additions & 64 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* @license MIT
*/

const assert = require('assert').strict
const EventEmitter = require('events')
const pump = require('pump')
const Dnode = require('dnode')
Expand All @@ -20,6 +19,7 @@ const createFilterMiddleware = require('eth-json-rpc-filters')
const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager')
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const createOriginMiddleware = require('./lib/createOriginMiddleware')
import createOnboardingMiddleware from './lib/createOnboardingMiddleware'
const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware')
const { setupMultiplex } = require('./lib/stream-utils.js')
const KeyringController = require('eth-keyring-controller')
Expand Down Expand Up @@ -1285,28 +1285,32 @@ module.exports = class MetamaskController extends EventEmitter {
// SETUP
//=============================================================================

/**
* A runtime.MessageSender object, as provided by the browser:
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender
* @typedef {Object} MessageSender
*/

/**
* Used to create a multiplexed stream for connecting to an untrusted context
* like a Dapp or other extension.
* @param {*} connectionStream - The Duplex stream to connect to.
* @param {URL} senderUrl - The URL of the resource requesting the stream,
* which may trigger a blacklist reload.
* @param {string} extensionId - The extension id of the sender, if the sender
* is an extension
* @param {MessageSender} sender - The sender of the messages on this stream
*/
setupUntrustedCommunication (connectionStream, senderUrl, extensionId) {
setupUntrustedCommunication (connectionStream, sender) {
const hostname = (new URL(sender.url)).hostname
// Check if new connection is blacklisted
if (this.phishingController.test(senderUrl.hostname)) {
log.debug('MetaMask - sending phishing warning for', senderUrl.hostname)
this.sendPhishingWarning(connectionStream, senderUrl.hostname)
if (this.phishingController.test(hostname)) {
log.debug('MetaMask - sending phishing warning for', hostname)
this.sendPhishingWarning(connectionStream, hostname)
return
}

// setup multiplexing
const mux = setupMultiplex(connectionStream)

// messages between inpage and background
this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId)
this.setupProviderConnection(mux.createStream('provider'), sender)
this.setupPublicConfig(mux.createStream('publicConfig'))
}

Expand All @@ -1317,15 +1321,14 @@ module.exports = class MetamaskController extends EventEmitter {
* functions, like the ability to approve transactions or sign messages.
*
* @param {*} connectionStream - The duplex stream to connect to.
* @param {URL} senderUrl - The URL requesting the connection,
* used in logging and error reporting.
* @param {MessageSender} sender - The sender of the messages on this stream
*/
setupTrustedCommunication (connectionStream, senderUrl) {
setupTrustedCommunication (connectionStream, sender) {
// setup multiplexing
const mux = setupMultiplex(connectionStream)
// connect features
this.setupControllerConnection(mux.createStream('controller'))
this.setupProviderConnection(mux.createStream('provider'), senderUrl)
this.setupProviderConnection(mux.createStream('provider'), sender, true)
}

/**
Expand Down Expand Up @@ -1380,14 +1383,23 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A method for serving our ethereum provider over a given stream.
* @param {*} outStream - The stream to provide over.
* @param {URL} senderUrl - The URI of the requesting resource.
* @param {string} extensionId - The id of the extension, if the requesting
* resource is an extension.
* @param {object} publicApi - The public API
*/
setupProviderConnection (outStream, senderUrl, extensionId) {
const origin = senderUrl.hostname
const engine = this.setupProviderEngine(senderUrl, extensionId)
* @param {MessageSender} sender - The sender of the messages on this stream
* @param {boolean} isInternal - True if this is a connection with an internal process
*/
setupProviderConnection (outStream, sender, isInternal) {
const origin = isInternal
? 'metamask'
: (new URL(sender.url)).hostname
let extensionId
if (sender.id !== extension.runtime.id) {
extensionId = sender.id
}
let tabId
if (sender.tab && sender.tab.id) {
tabId = sender.tab.id
}

const engine = this.setupProviderEngine({ origin, location: sender.url, extensionId, tabId })

// setup connection
const providerStream = createEngineStream({ engine })
Expand Down Expand Up @@ -1415,10 +1427,13 @@ module.exports = class MetamaskController extends EventEmitter {

/**
* A method for creating a provider that is safely restricted for the requesting domain.
* @param {Object} options - Provider engine options
* @param {string} options.origin - The hostname of the sender
* @param {string} options.location - The full URL of the sender
* @param {extensionId} [options.extensionId] - The extension ID of the sender, if the sender is an external extension
* @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab
**/
setupProviderEngine (senderUrl, extensionId) {

const origin = senderUrl.hostname
setupProviderEngine ({ origin, location, extensionId, tabId }) {
// setup json rpc engine stack
const engine = new RpcEngine()
const provider = this.provider
Expand All @@ -1435,6 +1450,11 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(createOriginMiddleware({ origin }))
// logging
engine.push(createLoggerMiddleware({ origin }))
engine.push(createOnboardingMiddleware({
location,
tabId,
registerOnboarding: this.onboardingController.registerOnboarding,
}))
// filter and subscription polyfills
engine.push(filterMiddleware)
engine.push(subscriptionManager.middleware)
Expand Down Expand Up @@ -1474,45 +1494,6 @@ module.exports = class MetamaskController extends EventEmitter {
)
}

// manage external connections

onMessage (message, sender, sendResponse) {
if (!message || !message.type) {
log.debug(`Ignoring invalid message: '${JSON.stringify(message)}'`)
return
}

let handleMessage

try {
if (message.type === 'metamask:registerOnboarding') {
assert(sender.tab, 'Missing tab from sender')
assert(sender.tab.id && sender.tab.id !== extension.tabs.TAB_ID_NONE, 'Missing tab ID from sender')
assert(message.location, 'Missing location from message')

handleMessage = this.onboardingController.registerOnboarding(message.location, sender.tab.id)
} else {
throw new Error(`Unrecognized message type: '${message.type}'`)
}
} catch (error) {
console.error(error)
sendResponse(error)
return true
}

if (handleMessage) {
handleMessage
.then(() => {
sendResponse(null, true)
})
.catch((error) => {
console.error(error)
sendResponse(error)
})
return true
}
}

/**
* Adds a reference to a connection by origin. Ignores the 'metamask' origin.
* Caller must ensure that the returned id is stored such that the reference
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.5.5",
"@metamask/forwarder": "^1.0.0",
"@metamask/onboarding": "^0.1.2",
"@metamask/forwarder": "^1.1.0",
"@metamask/onboarding": "^0.2.0",
"@sentry/cli": "^1.30.3",
"@storybook/addon-actions": "^5.2.6",
"@storybook/addon-info": "^5.1.1",
Expand Down
22 changes: 18 additions & 4 deletions test/unit/app/controllers/metamask-controller-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ class ThreeBoxControllerMock {
}
}

const ExtensionizerMock = {
runtime: {
id: 'fake-extension-id',
},
}

const MetaMaskController = proxyquire('../../../../app/scripts/metamask-controller', {
'./controllers/threebox': ThreeBoxControllerMock,
'extensionizer': ExtensionizerMock,
})

const currentNetworkId = 42
Expand Down Expand Up @@ -805,7 +812,10 @@ describe('MetaMaskController', function () {
describe('#setupUntrustedCommunication', function () {
let streamTest

const phishingUrl = new URL('http://myethereumwalletntw.com')
const phishingMessageSender = {
url: 'http://myethereumwalletntw.com',
tab: {},
}

afterEach(function () {
streamTest.end()
Expand All @@ -820,11 +830,11 @@ describe('MetaMaskController', function () {
if (chunk.name !== 'phishing') {
return cb()
}
assert.equal(chunk.data.hostname, phishingUrl.hostname)
assert.equal(chunk.data.hostname, (new URL(phishingMessageSender.url)).hostname)
resolve()
cb()
})
metamaskController.setupUntrustedCommunication(streamTest, phishingUrl)
metamaskController.setupUntrustedCommunication(streamTest, phishingMessageSender)

await promise
})
Expand All @@ -838,13 +848,17 @@ describe('MetaMaskController', function () {
})

it('sets up controller dnode api for trusted communication', function (done) {
const messageSender = {
url: 'http://mycrypto.com',
tab: {},
}
streamTest = createThoughStream((chunk, _, cb) => {
assert.equal(chunk.name, 'controller')
cb()
done()
})

metamaskController.setupTrustedCommunication(streamTest, 'mycrypto.com')
metamaskController.setupTrustedCommunication(streamTest, messageSender)
})
})

Expand Down
Loading