1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-30 08:09:15 +01:00
metamask-extension/app/scripts/contentscript.js
Erik Marks 0dfdd44ae7
Restore support for @metamask/inpage provider@"< 8.0.0" (#10179)
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>
2021-01-12 17:43:45 -08:00

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,
})}`
}