mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-30 08:09:15 +01:00
0dfdd44ae7
This restores support for versions of the inpage provider prior to v8. This is intended to support dapps and extensions that directly instantiated their own provider rather than using the injected provider. * Forward traffic between old and new provider streams * Ignore publicConfig stream for non-legacy muxes * Transform accountsChanged notification for legacy streams * Convert publicConfigStore to singleton Co-authored-by: Mark Stacey <markjstacey@gmail.com>
310 lines
8.7 KiB
JavaScript
310 lines
8.7 KiB
JavaScript
import querystring from 'querystring'
|
|
import pump from 'pump'
|
|
import LocalMessageDuplexStream from 'post-message-stream'
|
|
import ObjectMultiplex from 'obj-multiplex'
|
|
import extension from 'extensionizer'
|
|
import PortStream from 'extension-port-stream'
|
|
import { obj as createThoughStream } from 'through2'
|
|
|
|
// These require calls need to use require to be statically recognized by browserify
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const inpageContent = fs.readFileSync(
|
|
path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js'),
|
|
'utf8',
|
|
)
|
|
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
|
|
const inpageBundle = inpageContent + inpageSuffix
|
|
|
|
const CONTENT_SCRIPT = 'metamask-contentscript'
|
|
const INPAGE = 'metamask-inpage'
|
|
const PROVIDER = 'metamask-provider'
|
|
|
|
// TODO:LegacyProvider: Delete
|
|
const LEGACY_CONTENT_SCRIPT = 'contentscript'
|
|
const LEGACY_INPAGE = 'inpage'
|
|
const LEGACY_PROVIDER = 'provider'
|
|
const LEGACY_PUBLIC_CONFIG = 'publicConfig'
|
|
|
|
if (shouldInjectProvider()) {
|
|
injectScript(inpageBundle)
|
|
setupStreams()
|
|
}
|
|
|
|
/**
|
|
* Injects a script tag into the current document
|
|
*
|
|
* @param {string} content - Code to be executed in the current document
|
|
*/
|
|
function injectScript(content) {
|
|
try {
|
|
const container = document.head || document.documentElement
|
|
const scriptTag = document.createElement('script')
|
|
scriptTag.setAttribute('async', 'false')
|
|
scriptTag.textContent = content
|
|
container.insertBefore(scriptTag, container.children[0])
|
|
container.removeChild(scriptTag)
|
|
} catch (error) {
|
|
console.error('MetaMask: Provider injection failed.', error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up two-way communication streams between the
|
|
* browser extension and local per-page browser context.
|
|
*
|
|
*/
|
|
async function setupStreams() {
|
|
// the transport-specific streams for communication between inpage and background
|
|
const pageStream = new LocalMessageDuplexStream({
|
|
name: CONTENT_SCRIPT,
|
|
target: INPAGE,
|
|
})
|
|
const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT })
|
|
const extensionStream = new PortStream(extensionPort)
|
|
|
|
// create and connect channel muxers
|
|
// so we can handle the channels individually
|
|
const pageMux = new ObjectMultiplex()
|
|
pageMux.setMaxListeners(25)
|
|
const extensionMux = new ObjectMultiplex()
|
|
extensionMux.setMaxListeners(25)
|
|
extensionMux.ignoreStream(LEGACY_PUBLIC_CONFIG) // TODO:LegacyProvider: Delete
|
|
|
|
pump(pageMux, pageStream, pageMux, (err) =>
|
|
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
|
|
)
|
|
pump(extensionMux, extensionStream, extensionMux, (err) => {
|
|
logStreamDisconnectWarning('MetaMask Background Multiplex', err)
|
|
notifyInpageOfStreamFailure()
|
|
})
|
|
|
|
// forward communication across inpage-background for these channels only
|
|
forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux)
|
|
|
|
// connect "phishing" channel to warning system
|
|
const phishingStream = extensionMux.createStream('phishing')
|
|
phishingStream.once('data', redirectToPhishingWarning)
|
|
|
|
// TODO:LegacyProvider: Delete
|
|
// handle legacy provider
|
|
const legacyPageStream = new LocalMessageDuplexStream({
|
|
name: LEGACY_CONTENT_SCRIPT,
|
|
target: LEGACY_INPAGE,
|
|
})
|
|
|
|
const legacyPageMux = new ObjectMultiplex()
|
|
legacyPageMux.setMaxListeners(25)
|
|
const legacyExtensionMux = new ObjectMultiplex()
|
|
legacyExtensionMux.setMaxListeners(25)
|
|
|
|
pump(legacyPageMux, legacyPageStream, legacyPageMux, (err) =>
|
|
logStreamDisconnectWarning('MetaMask Legacy Inpage Multiplex', err),
|
|
)
|
|
pump(
|
|
legacyExtensionMux,
|
|
extensionStream,
|
|
getNotificationTransformStream(),
|
|
legacyExtensionMux,
|
|
(err) => {
|
|
logStreamDisconnectWarning('MetaMask Background Legacy Multiplex', err)
|
|
notifyInpageOfStreamFailure()
|
|
},
|
|
)
|
|
|
|
forwardNamedTrafficBetweenMuxes(
|
|
LEGACY_PROVIDER,
|
|
PROVIDER,
|
|
legacyPageMux,
|
|
legacyExtensionMux,
|
|
)
|
|
forwardTrafficBetweenMuxes(
|
|
LEGACY_PUBLIC_CONFIG,
|
|
legacyPageMux,
|
|
legacyExtensionMux,
|
|
)
|
|
}
|
|
|
|
function forwardTrafficBetweenMuxes(channelName, muxA, muxB) {
|
|
const channelA = muxA.createStream(channelName)
|
|
const channelB = muxB.createStream(channelName)
|
|
pump(channelA, channelB, channelA, (error) =>
|
|
console.debug(
|
|
`MetaMask: Muxed traffic for channel "${channelName}" failed.`,
|
|
error,
|
|
),
|
|
)
|
|
}
|
|
|
|
// TODO:LegacyProvider: Delete
|
|
function forwardNamedTrafficBetweenMuxes(
|
|
channelAName,
|
|
channelBName,
|
|
muxA,
|
|
muxB,
|
|
) {
|
|
const channelA = muxA.createStream(channelAName)
|
|
const channelB = muxB.createStream(channelBName)
|
|
pump(channelA, channelB, channelA, (error) =>
|
|
console.debug(
|
|
`MetaMask: Muxed traffic between channels "${channelAName}" and "${channelBName}" failed.`,
|
|
error,
|
|
),
|
|
)
|
|
}
|
|
|
|
// TODO:LegacyProvider: Delete
|
|
function getNotificationTransformStream() {
|
|
return createThoughStream((chunk, _, cb) => {
|
|
if (chunk?.name === PROVIDER) {
|
|
if (chunk.data?.method === 'metamask_accountsChanged') {
|
|
chunk.data.method = 'wallet_accountsChanged'
|
|
chunk.data.result = chunk.data.params
|
|
delete chunk.data.params
|
|
}
|
|
}
|
|
cb(null, chunk)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Error handler for page to extension stream disconnections
|
|
*
|
|
* @param {string} remoteLabel - Remote stream name
|
|
* @param {Error} error - Stream connection error
|
|
*/
|
|
function logStreamDisconnectWarning(remoteLabel, error) {
|
|
console.debug(
|
|
`MetaMask: Content script lost connection to "${remoteLabel}".`,
|
|
error,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* This function must ONLY be called in pump destruction/close callbacks.
|
|
* Notifies the inpage context that streams have failed, via window.postMessage.
|
|
* Relies on obj-multiplex and post-message-stream implementation details.
|
|
*/
|
|
function notifyInpageOfStreamFailure() {
|
|
window.postMessage(
|
|
{
|
|
target: INPAGE, // the post-message-stream "target"
|
|
data: {
|
|
// this object gets passed to obj-multiplex
|
|
name: PROVIDER, // the obj-multiplex channel name
|
|
data: {
|
|
jsonrpc: '2.0',
|
|
method: 'METAMASK_STREAM_FAILURE',
|
|
},
|
|
},
|
|
},
|
|
window.location.origin,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Determines if the provider should be injected
|
|
*
|
|
* @returns {boolean} {@code true} Whether the provider should be injected
|
|
*/
|
|
function shouldInjectProvider() {
|
|
return (
|
|
doctypeCheck() &&
|
|
suffixCheck() &&
|
|
documentElementCheck() &&
|
|
!blockedDomainCheck()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Checks the doctype of the current document if it exists
|
|
*
|
|
* @returns {boolean} {@code true} if the doctype is html or if none exists
|
|
*/
|
|
function doctypeCheck() {
|
|
const { doctype } = window.document
|
|
if (doctype) {
|
|
return doctype.name === 'html'
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the extension (suffix) of the current document is prohibited
|
|
*
|
|
* This checks {@code window.location.pathname} against a set of file extensions
|
|
* that we should not inject the provider into. This check is indifferent of
|
|
* query parameters in the location.
|
|
*
|
|
* @returns {boolean} whether or not the extension of the current document is prohibited
|
|
*/
|
|
function suffixCheck() {
|
|
const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]
|
|
const currentUrl = window.location.pathname
|
|
for (let i = 0; i < prohibitedTypes.length; i++) {
|
|
if (prohibitedTypes[i].test(currentUrl)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Checks the documentElement of the current document
|
|
*
|
|
* @returns {boolean} {@code true} if the documentElement is an html node or if none exists
|
|
*/
|
|
function documentElementCheck() {
|
|
const documentElement = document.documentElement.nodeName
|
|
if (documentElement) {
|
|
return documentElement.toLowerCase() === 'html'
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Checks if the current domain is blocked
|
|
*
|
|
* @returns {boolean} {@code true} if the current domain is blocked
|
|
*/
|
|
function blockedDomainCheck() {
|
|
const blockedDomains = [
|
|
'uscourts.gov',
|
|
'dropbox.com',
|
|
'webbyawards.com',
|
|
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
|
|
'adyen.com',
|
|
'gravityforms.com',
|
|
'harbourair.com',
|
|
'ani.gamer.com.tw',
|
|
'blueskybooking.com',
|
|
'sharefile.com',
|
|
]
|
|
const currentUrl = window.location.href
|
|
let currentRegex
|
|
for (let i = 0; i < blockedDomains.length; i++) {
|
|
const blockedDomain = blockedDomains[i].replace('.', '\\.')
|
|
currentRegex = new RegExp(
|
|
`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`,
|
|
'u',
|
|
)
|
|
if (!currentRegex.test(currentUrl)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Redirects the current page to a phishing information page
|
|
*/
|
|
function redirectToPhishingWarning() {
|
|
console.debug('MetaMask: Routing to Phishing Warning component.')
|
|
const extensionURL = extension.runtime.getURL('phishing.html')
|
|
window.location.href = `${extensionURL}#${querystring.stringify({
|
|
hostname: window.location.hostname,
|
|
href: window.location.href,
|
|
})}`
|
|
}
|