mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-23 02:10:12 +01:00
42c8703f3e
Two comments have been added to reference a longer explanation of what the legacy provider streams are, why we still have them, and why we want to remove them.
375 lines
11 KiB
JavaScript
375 lines
11 KiB
JavaScript
import pump from 'pump';
|
|
import { WindowPostMessageStream } from '@metamask/post-message-stream';
|
|
import ObjectMultiplex from 'obj-multiplex';
|
|
import browser from 'webextension-polyfill';
|
|
import PortStream from 'extension-port-stream';
|
|
import { obj as createThoughStream } from 'through2';
|
|
|
|
import { isManifestV3 } from '../../shared/modules/mv3.utils';
|
|
|
|
// 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=${browser.runtime.getURL('inpage.js')}\n`;
|
|
const inpageBundle = inpageContent + inpageSuffix;
|
|
|
|
// contexts
|
|
const CONTENT_SCRIPT = 'metamask-contentscript';
|
|
const INPAGE = 'metamask-inpage';
|
|
const PHISHING_WARNING_PAGE = 'metamask-phishing-warning-page';
|
|
|
|
// stream channels
|
|
const PHISHING_SAFELIST = 'metamask-phishing-safelist';
|
|
const PROVIDER = 'metamask-provider';
|
|
|
|
// For more information about these legacy streams, see here:
|
|
// https://github.com/MetaMask/metamask-extension/issues/15491
|
|
// TODO:LegacyProvider: Delete
|
|
const LEGACY_CONTENT_SCRIPT = 'contentscript';
|
|
const LEGACY_INPAGE = 'inpage';
|
|
const LEGACY_PROVIDER = 'provider';
|
|
const LEGACY_PUBLIC_CONFIG = 'publicConfig';
|
|
|
|
const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
|
|
|
|
if (
|
|
window.location.origin === phishingPageUrl.origin &&
|
|
window.location.pathname === phishingPageUrl.pathname
|
|
) {
|
|
setupPhishingStream();
|
|
} else 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');
|
|
// Inline scripts do not work in MV3 due to more strict security policy
|
|
if (isManifestV3()) {
|
|
scriptTag.setAttribute('src', browser.runtime.getURL('inpage.js'));
|
|
} else {
|
|
scriptTag.textContent = content;
|
|
}
|
|
container.insertBefore(scriptTag, container.children[0]);
|
|
container.removeChild(scriptTag);
|
|
} catch (error) {
|
|
console.error('MetaMask: Provider injection failed.', error);
|
|
}
|
|
}
|
|
|
|
async function setupPhishingStream() {
|
|
// the transport-specific streams for communication between inpage and background
|
|
const pageStream = new WindowPostMessageStream({
|
|
name: CONTENT_SCRIPT,
|
|
target: PHISHING_WARNING_PAGE,
|
|
});
|
|
const extensionPort = browser.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);
|
|
|
|
pump(pageMux, pageStream, pageMux, (err) =>
|
|
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
|
|
);
|
|
pump(extensionMux, extensionStream, extensionMux, (err) => {
|
|
logStreamDisconnectWarning('MetaMask Background Multiplex', err);
|
|
window.postMessage(
|
|
{
|
|
target: PHISHING_WARNING_PAGE, // the post-message-stream "target"
|
|
data: {
|
|
// this object gets passed to obj-multiplex
|
|
name: PHISHING_SAFELIST, // the obj-multiplex channel name
|
|
data: {
|
|
jsonrpc: '2.0',
|
|
method: 'METAMASK_STREAM_FAILURE',
|
|
},
|
|
},
|
|
},
|
|
window.location.origin,
|
|
);
|
|
});
|
|
|
|
// forward communication across inpage-background for these channels only
|
|
forwardTrafficBetweenMuxes(PHISHING_SAFELIST, pageMux, extensionMux);
|
|
}
|
|
|
|
/**
|
|
* 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 WindowPostMessageStream({
|
|
name: CONTENT_SCRIPT,
|
|
target: INPAGE,
|
|
});
|
|
const extensionPort = browser.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 WindowPostMessageStream({
|
|
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 = [
|
|
'adyen.com',
|
|
'ani.gamer.com.tw',
|
|
'blueskybooking.com',
|
|
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
|
|
'docs.google.com',
|
|
'dropbox.com',
|
|
'gravityforms.com',
|
|
'harbourair.com',
|
|
'sharefile.com',
|
|
'uscourts.gov',
|
|
'webbyawards.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
|
|
*
|
|
* @param data
|
|
*/
|
|
function redirectToPhishingWarning(data = {}) {
|
|
console.debug('MetaMask: Routing to Phishing Warning page.');
|
|
const { hostname, href } = window.location;
|
|
const { newIssueUrl } = data;
|
|
const baseUrl = process.env.PHISHING_WARNING_PAGE_URL;
|
|
|
|
const querystring = new URLSearchParams({ hostname, href, newIssueUrl });
|
|
window.location.href = `${baseUrl}#${querystring}`;
|
|
}
|