From f98edca315082d64e7ef08ea10beed19f57c1f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= <100827540+reneaaron@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:27:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20fast=20provider=20injection=20?= =?UTF-8?q?=F0=9F=94=A5=20(#2691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add webln * fix: all_frames property name * feat: use service worker instead of manifest * feat: inject content scripts on start * feat: inline and inject provider in mv2 via custom webpack plugin intercept build files add provider during build time and inject signed-off-by: pavan joshi * chore: comment/refactor * fix: use helper function * fix: move injection from onend to onstart * fix: remove injectScripts, separate WebLN from index * fix: add inpage script registration for other providers * fix: added todo * fix: remove web accessible resources * fix: separate index from webln * fix: rename scripts, merge manifest content script definition * feat: rewrite webpack plugin inject all the provider via single variable solve plugin error, while inlinining all the scripts in single variable signed-off-by: pavan joshi * feat: add webln * fix: all_frames property name * feat: use service worker instead of manifest * feat: inject content scripts on start * feat: inline and inject provider in mv2 via custom webpack plugin intercept build files add provider during build time and inject signed-off-by: pavan joshi * chore: comment/refactor * fix: use helper function * fix: move injection from onend to onstart * fix: remove injectScripts, separate WebLN from index * fix: add inpage script registration for other providers * fix: added todo * fix: remove web accessible resources * fix: separate index from webln * fix: rename scripts, merge manifest content script definition * feat: merge all the providers into single script * chore: move function into separate file * chore: place comment * feat: do brower side checks in the inpage script itself do browser side checks + extension side cheks such as blocklist in the provider's content script signed-off-by: pavan joshi * feat: refactor clickhandlers in separate function remove document check in inpage index signed-off-by: pavan joshi * chore: change plugin variable names * feat: be defensive about window and doctype are available or not * fix: add fallback injection for blocked inline scripts --------- Co-authored-by: pavanjoshi914 Co-authored-by: Pavan Joshi <55848322+pavanjoshi914@users.noreply.github.com> --- build-utils/ProviderInjectionPlugin.js | 36 +++++++++++++++ src/extension/background-script/index.ts | 4 ++ .../registerContentScript.ts | 15 +++++++ .../content-script/{onendalby.js => alby.js} | 0 src/extension/content-script/injectScript.ts | 29 +++++++++--- .../{onendliquid.js => liquid.js} | 0 .../{onendnostr.js => nostr.js} | 0 src/extension/content-script/onstart.ts | 40 +++++------------ src/extension/content-script/shouldInject.js | 40 +++-------------- .../{onendwebbtc.js => webbtc.js} | 0 .../{onendwebln.js => webln.js} | 3 -- src/extension/inpage-script/alby.js | 5 --- src/extension/inpage-script/index.js | 45 +++++++++++-------- src/extension/inpage-script/liquid.js | 5 --- src/extension/inpage-script/nostr.js | 5 --- src/extension/inpage-script/shouldInject.js | 13 ++++++ src/extension/inpage-script/webbtc.js | 5 --- src/extension/inpage-script/webln.js | 5 --- src/extension/shouldInjectBrowserChecks.js | 33 ++++++++++++++ src/manifest.json | 44 +++++------------- webpack.config.js | 17 +++---- 21 files changed, 183 insertions(+), 161 deletions(-) create mode 100644 build-utils/ProviderInjectionPlugin.js create mode 100644 src/extension/background-script/registerContentScript.ts rename src/extension/content-script/{onendalby.js => alby.js} (100%) rename src/extension/content-script/{onendliquid.js => liquid.js} (100%) rename src/extension/content-script/{onendnostr.js => nostr.js} (100%) rename src/extension/content-script/{onendwebbtc.js => webbtc.js} (100%) rename src/extension/content-script/{onendwebln.js => webln.js} (94%) delete mode 100644 src/extension/inpage-script/alby.js delete mode 100644 src/extension/inpage-script/liquid.js delete mode 100644 src/extension/inpage-script/nostr.js create mode 100644 src/extension/inpage-script/shouldInject.js delete mode 100644 src/extension/inpage-script/webbtc.js delete mode 100644 src/extension/inpage-script/webln.js create mode 100644 src/extension/shouldInjectBrowserChecks.js diff --git a/build-utils/ProviderInjectionPlugin.js b/build-utils/ProviderInjectionPlugin.js new file mode 100644 index 0000000000..ad41159d36 --- /dev/null +++ b/build-utils/ProviderInjectionPlugin.js @@ -0,0 +1,36 @@ +const { sources } = require("webpack"); + +const PLUGIN_NAME = "ProviderInjectionPlugin"; +const PROVIDER_SCRIPT_FILENAME = "js/inpageScript.bundle.js"; +const CONTENT_SCRIPT_FILENAME = "js/contentScriptOnStart.bundle.js"; + +// plugin to intercept webln bundle during built times and inline/attach provider to make it available immediately +// works only on MV2 and designed specifically for firefox as main world execution is not supported for firefox +class ProviderInjectionPlugin { + apply(compiler) { + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE, + }, + (assets) => { + let providerScriptSource = + assets[PROVIDER_SCRIPT_FILENAME].source().toString(); + providerScriptSource = JSON.stringify(providerScriptSource); + let contentScriptSource = + assets[CONTENT_SCRIPT_FILENAME].source().toString(); + + assets[CONTENT_SCRIPT_FILENAME] = new sources.RawSource( + contentScriptSource.replace( + '"@@@WINDOW_PROVIDER@@@"', + providerScriptSource + ) + ); + } + ); + }); + } +} + +module.exports = ProviderInjectionPlugin; diff --git a/src/extension/background-script/index.ts b/src/extension/background-script/index.ts index 6cc781c840..b6c60e9fea 100644 --- a/src/extension/background-script/index.ts +++ b/src/extension/background-script/index.ts @@ -1,6 +1,8 @@ import browser, { Runtime, Tabs } from "webextension-polyfill"; import utils from "~/common/lib/utils"; +import { isManifestV3 } from "~/common/utils/mv3"; +import { registerInPageContentScript } from "~/extension/background-script/registerContentScript"; import { ExtensionIcon, setIcon } from "./actions/setup/setIcon"; import { db, isIndexedDbAvailable } from "./db"; import * as events from "./events"; @@ -141,6 +143,8 @@ browser.runtime.onInstalled.addListener(handleInstalled); async function init() { console.info("Loading background script"); + if (isManifestV3) registerInPageContentScript(); + await state.getState().init(); console.info("State loaded"); diff --git a/src/extension/background-script/registerContentScript.ts b/src/extension/background-script/registerContentScript.ts new file mode 100644 index 0000000000..07c50a6bee --- /dev/null +++ b/src/extension/background-script/registerContentScript.ts @@ -0,0 +1,15 @@ +export const registerInPageContentScript = async () => { + try { + await chrome.scripting.registerContentScripts([ + { + id: "inpageScript", + matches: ["file://*/*", "http://*/*", "https://*/*"], + js: ["js/inpageScript.bundle.js"], + runAt: "document_start", + world: "MAIN", + }, + ]); + } catch (err) { + console.warn(`Dropped attempt to register inpage content script. ${err}`); + } +}; diff --git a/src/extension/content-script/onendalby.js b/src/extension/content-script/alby.js similarity index 100% rename from src/extension/content-script/onendalby.js rename to src/extension/content-script/alby.js diff --git a/src/extension/content-script/injectScript.ts b/src/extension/content-script/injectScript.ts index a5594d3b5b..c08bbfc9b5 100644 --- a/src/extension/content-script/injectScript.ts +++ b/src/extension/content-script/injectScript.ts @@ -1,7 +1,8 @@ // load the inpage scripts // only an inpage script gets access to the document + // and the document can interact with the extension through the inpage script -export default function injectScript(url: string) { +export function injectScript(script: string) { try { if (!document) throw new Error("No document"); const container = document.head || document.documentElement; @@ -9,12 +10,28 @@ export default function injectScript(url: string) { const scriptEl = document.createElement("script"); scriptEl.setAttribute("async", "false"); scriptEl.setAttribute("type", "text/javascript"); - scriptEl.setAttribute("src", url); + scriptEl.textContent = script; container.insertBefore(scriptEl, container.children[0]); - scriptEl.onload = () => { - container.removeChild(scriptEl); - }; + container.removeChild(scriptEl); } catch (err) { - console.error("injection failed", err); + console.error("Alby: provider injection failed", err); } } + +export function injectScriptByUrl(url: string) { + try { + if (!document) throw new Error("No document"); + const container = document.head || document.documentElement; + if (!container) throw new Error("No container element"); + const scriptEl = document.createElement("script"); + scriptEl.setAttribute("async", "false"); + scriptEl.setAttribute("type", "text/javascript"); + scriptEl.src = url; + container.insertBefore(scriptEl, container.children[0]); + container.removeChild(scriptEl); + } catch (err) { + console.error("Alby: provider injection failed", err); + } +} + +export default { injectScript, injectScriptByUrl }; diff --git a/src/extension/content-script/onendliquid.js b/src/extension/content-script/liquid.js similarity index 100% rename from src/extension/content-script/onendliquid.js rename to src/extension/content-script/liquid.js diff --git a/src/extension/content-script/onendnostr.js b/src/extension/content-script/nostr.js similarity index 100% rename from src/extension/content-script/onendnostr.js rename to src/extension/content-script/nostr.js diff --git a/src/extension/content-script/onstart.ts b/src/extension/content-script/onstart.ts index e82ba6c95a..6d81779ed6 100644 --- a/src/extension/content-script/onstart.ts +++ b/src/extension/content-script/onstart.ts @@ -1,36 +1,18 @@ import browser from "webextension-polyfill"; -import api from "~/common/lib/api"; -import injectScript from "./injectScript"; -import shouldInject from "./shouldInject"; +import { isManifestV3 } from "~/common/utils/mv3"; +import { + injectScript, + injectScriptByUrl, +} from "~/extension/content-script/injectScript"; async function onstart() { - // eslint-disable-next-line no-console - const inject = await shouldInject(); - if (!inject) { - return; - } - - const account = await api.getAccount(); - // window.webln - injectScript(browser.runtime.getURL("js/inpageScriptWebLN.bundle.js")); - - // window.alby - injectScript(browser.runtime.getURL("js/inpageScriptAlby.bundle.js")); - - // window.webbtc - if (account.hasMnemonic) { - injectScript(browser.runtime.getURL("js/inpageScriptWebBTC.bundle.js")); - } - - // window.nostr - if (account.nostrEnabled) { - injectScript(browser.runtime.getURL("js/inpageScriptNostr.bundle.js")); - } - - // window.liquid - if (account.liquidEnabled) { - injectScript(browser.runtime.getURL("js/inpageScriptLiquid.bundle.js")); + // Inject in-page scripts for MV2 + if (!isManifestV3) { + // Try to inject inline + injectScript("@@@WINDOW_PROVIDER@@@"); + // Fallback if inline script is blocked via CSP + injectScriptByUrl(browser.runtime.getURL("js/inpageScript.bundle.js")); } } diff --git a/src/extension/content-script/shouldInject.js b/src/extension/content-script/shouldInject.js index 7a27e366c6..aa818a2801 100644 --- a/src/extension/content-script/shouldInject.js +++ b/src/extension/content-script/shouldInject.js @@ -1,4 +1,9 @@ import msg from "../../common/lib/msg"; +import { + doctypeCheck, + documentElementCheck, + suffixCheck, +} from "../shouldInjectBrowserChecks"; // https://github.com/joule-labs/joule-extension/blob/develop/src/content_script/shouldInject.ts // Whether or not to inject the WebLN listeners @@ -11,41 +16,6 @@ export default async function shouldInject() { return notBlocked && isHTML && noProhibitedType && hasDocumentElement; } -// Checks the doctype of the current document if it exists -function doctypeCheck() { - const doctype = window.document.doctype; - if (doctype) { - return doctype.name === "html"; - } else { - return true; - } -} - -// Returns whether or not the extension (suffix) of the current document is prohibited -function suffixCheck() { - const prohibitedTypes = [/\.xml$/, /\.pdf$/]; - const currentUrl = window.location.pathname; - for (const type of prohibitedTypes) { - if (type.test(currentUrl)) { - return false; - } - } - return true; -} - -// Checks the documentElement of the current document -function documentElementCheck() { - // todo: correct? - if (!document || !document.documentElement) { - return false; - } - const docNode = document.documentElement.nodeName; - if (docNode) { - return docNode.toLowerCase() === "html"; - } - return true; -} - async function blocklistCheck() { try { const currentHost = window.location.host; diff --git a/src/extension/content-script/onendwebbtc.js b/src/extension/content-script/webbtc.js similarity index 100% rename from src/extension/content-script/onendwebbtc.js rename to src/extension/content-script/webbtc.js diff --git a/src/extension/content-script/onendwebln.js b/src/extension/content-script/webln.js similarity index 94% rename from src/extension/content-script/onendwebln.js rename to src/extension/content-script/webln.js index 977dd417bb..d119c06001 100644 --- a/src/extension/content-script/onendwebln.js +++ b/src/extension/content-script/webln.js @@ -1,7 +1,6 @@ import browser from "webextension-polyfill"; import extractLightningData from "./batteries"; -import injectScript from "./injectScript"; import getOriginData from "./originData"; import shouldInject from "./shouldInject"; @@ -33,8 +32,6 @@ async function init() { return; } - injectScript(browser.runtime.getURL("js/inpageScript.bundle.js")); // registers the DOM event listeners and checks webln again (which is also loaded onstart - browser.runtime.onMessage.addListener((request, sender, sendResponse) => { // extract LN data from websites if (request.action === "extractLightningData") { diff --git a/src/extension/inpage-script/alby.js b/src/extension/inpage-script/alby.js deleted file mode 100644 index e56f40eb63..0000000000 --- a/src/extension/inpage-script/alby.js +++ /dev/null @@ -1,5 +0,0 @@ -import AlbyProvider from "../providers/alby"; - -if (document) { - window.alby = new AlbyProvider(); -} diff --git a/src/extension/inpage-script/index.js b/src/extension/inpage-script/index.js index 49f9020bfb..e01578bdff 100644 --- a/src/extension/inpage-script/index.js +++ b/src/extension/inpage-script/index.js @@ -1,17 +1,33 @@ import { ABORT_PROMPT_ERROR, USER_REJECTED_ERROR } from "~/common/constants"; +import AlbyProvider from "~/extension/providers/alby"; +import LiquidProvider from "~/extension/providers/liquid"; +import NostrProvider from "~/extension/providers/nostr"; +import WebBTCProvider from "~/extension/providers/webbtc"; +import WebLNProvider from "~/extension/providers/webln"; +import shouldInjectInpage from "./shouldInject"; -import WebLNProvider from "../providers/webln"; - -if (document) { - // window.webln is normally loaded onstart (see onstart.js) - // this is just to make double sure we load it - if (!window.webln) { - window.webln = new WebLNProvider(); - } - +function init() { + const inject = shouldInjectInpage(); + if (!inject) return; + window.liquid = new LiquidProvider(); + window.alby = new AlbyProvider(); + window.nostr = new NostrProvider(); + window.webbtc = new WebBTCProvider(); + window.webln = new WebLNProvider(); const readyEvent = new Event("webln:ready"); window.dispatchEvent(readyEvent); + registerLightningLinkClickHandler(); + + // Listen for webln events from the extension + // emit events to the websites + window.addEventListener("message", (event) => { + if (event.source === window && event.data.action === "accountChanged") { + eventEmitter(event.data.action, event.data.scope); + } + }); +} +function registerLightningLinkClickHandler() { // Intercept any `lightning:` requests window.addEventListener( "click", @@ -112,19 +128,10 @@ if (document) { }, { capture: true } ); - // Listen for webln events from the extension - // emit events to the websites - window.addEventListener("message", (event) => { - if (event.source === window && event.data.action === "accountChanged") { - eventEmitter(event.data.action, event.data.scope); - } - }); -} else { - console.warn("Failed to inject WebLN provider"); } - function eventEmitter(action, scope) { if (window[scope] && window[scope].emit) { window[scope].emit(action); } } +init(); diff --git a/src/extension/inpage-script/liquid.js b/src/extension/inpage-script/liquid.js deleted file mode 100644 index d6455866c2..0000000000 --- a/src/extension/inpage-script/liquid.js +++ /dev/null @@ -1,5 +0,0 @@ -import LiquidProvider from "../providers/liquid"; - -if (document) { - window.liquid = new LiquidProvider(); -} diff --git a/src/extension/inpage-script/nostr.js b/src/extension/inpage-script/nostr.js deleted file mode 100644 index 3d8d5b0781..0000000000 --- a/src/extension/inpage-script/nostr.js +++ /dev/null @@ -1,5 +0,0 @@ -import NostrProvider from "../providers/nostr"; - -if (document) { - window.nostr = new NostrProvider(); -} diff --git a/src/extension/inpage-script/shouldInject.js b/src/extension/inpage-script/shouldInject.js new file mode 100644 index 0000000000..6003a420d8 --- /dev/null +++ b/src/extension/inpage-script/shouldInject.js @@ -0,0 +1,13 @@ +import { + doctypeCheck, + documentElementCheck, + suffixCheck, +} from "../shouldInjectBrowserChecks"; +export default function shouldInjectInpage() { + const isHTML = doctypeCheck(); + const noProhibitedType = suffixCheck(); + const hasDocumentElement = documentElementCheck(); + const injectedBefore = window.webln !== undefined; + + return isHTML && noProhibitedType && hasDocumentElement && !injectedBefore; +} diff --git a/src/extension/inpage-script/webbtc.js b/src/extension/inpage-script/webbtc.js deleted file mode 100644 index f1603d4272..0000000000 --- a/src/extension/inpage-script/webbtc.js +++ /dev/null @@ -1,5 +0,0 @@ -import WebBTCProvider from "../providers/webbtc"; - -if (document) { - window.webbtc = new WebBTCProvider(); -} diff --git a/src/extension/inpage-script/webln.js b/src/extension/inpage-script/webln.js deleted file mode 100644 index 116ecb88bc..0000000000 --- a/src/extension/inpage-script/webln.js +++ /dev/null @@ -1,5 +0,0 @@ -import WebLNProvider from "../providers/webln"; - -if (document) { - window.webln = new WebLNProvider(); -} diff --git a/src/extension/shouldInjectBrowserChecks.js b/src/extension/shouldInjectBrowserChecks.js new file mode 100644 index 0000000000..b8e8cfed9c --- /dev/null +++ b/src/extension/shouldInjectBrowserChecks.js @@ -0,0 +1,33 @@ +// Checks the doctype of the current document if it exists +export function doctypeCheck() { + if (window && window.document && window.document.doctype) { + return window.document.doctype.name === "html"; + } else { + return true; + } +} + +// Returns whether or not the extension (suffix) of the current document is prohibited +export function suffixCheck() { + const prohibitedTypes = [/\.xml$/, /\.pdf$/]; + const currentUrl = window.location.pathname; + for (const type of prohibitedTypes) { + if (type.test(currentUrl)) { + return false; + } + } + return true; +} + +// Checks the documentElement of the current document +export function documentElementCheck() { + // todo: correct? + if (!document || !document.documentElement) { + return false; + } + const docNode = document.documentElement.nodeName; + if (docNode) { + return docNode.toLowerCase() === "html"; + } + return true; +} diff --git a/src/manifest.json b/src/manifest.json index c34d07906f..855eca4a8c 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -13,27 +13,6 @@ }, "description": "The Bitcoin Lightning wallet for direct payments across the globe, Bitcoin Lightning applications and passwordless logins.", "homepage_url": "https://getAlby.com/", - "web_accessible_resources": [ - "js/inpageScript.bundle.js", - "js/inpageScriptWebLN.bundle.js", - "js/inpageScriptWebBTC.bundle.js", - "js/inpageScriptLiquid.bundle.js", - "js/inpageScriptNostr.bundle.js", - "js/inpageScriptAlby.bundle.js" - ], - "__chrome__web_accessible_resources": [ - { - "resources": [ - "js/inpageScript.bundle.js", - "js/inpageScriptWebLN.bundle.js", - "js/inpageScriptWebBTC.bundle.js", - "js/inpageScriptLiquid.bundle.js", - "js/inpageScriptNostr.bundle.js", - "js/inpageScriptAlby.bundle.js" - ], - "matches": ["file://*/*", "http://*/*", "https://*/*"] - } - ], "permissions": [ "activeTab", "nativeMessaging", @@ -48,6 +27,7 @@ "activeTab", "nativeMessaging", "notifications", + "scripting", "storage", "tabs", "unlimitedStorage", @@ -152,20 +132,16 @@ { "all_frames": true, "matches": ["*://*/*"], - "run_at": "document_end", + "run_at": "document_start", "js": [ - "js/contentScriptOnEndWebLN.bundle.js", - "js/contentScriptOnEndAlby.bundle.js", - "js/contentScriptOnEndLiquid.bundle.js", - "js/contentScriptOnEndNostr.bundle.js", - "js/contentScriptOnEndWebBTC.bundle.js" + "js/contentScriptOnStart.bundle.js", + "js/contentScriptWebLN.bundle.js", + "js/contentScriptAlby.bundle.js", + "js/contentScriptLiquid.bundle.js", + "js/contentScriptNostr.bundle.js", + "js/contentScriptWebBTC.bundle.js" ] - }, - { - "all_frames": true, - "matches": ["*://*/*"], - "run_at": "document_start", - "js": ["js/contentScriptOnStart.bundle.js"] } - ] + ], + "web_accessible_resources": ["js/inpageScript.bundle.js"] } diff --git a/webpack.config.js b/webpack.config.js index e498e77a7c..b7e4ad3f21 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const WextManifestWebpackPlugin = require("wext-manifest-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); +const ProviderInjectionPlugin = require("./build-utils/ProviderInjectionPlugin"); const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; @@ -153,18 +154,13 @@ var options = { entry: { manifest: "./src/manifest.json", background: "./src/extension/background-script/index.ts", - contentScriptOnEndWebLN: "./src/extension/content-script/onendwebln.js", - contentScriptOnEndAlby: "./src/extension/content-script/onendalby.js", - contentScriptOnEndLiquid: "./src/extension/content-script/onendliquid.js", - contentScriptOnEndNostr: "./src/extension/content-script/onendnostr.js", - contentScriptOnEndWebBTC: "./src/extension/content-script/onendwebbtc.js", + contentScriptWebLN: "./src/extension/content-script/webln.js", + contentScriptAlby: "./src/extension/content-script/alby.js", + contentScriptLiquid: "./src/extension/content-script/liquid.js", + contentScriptNostr: "./src/extension/content-script/nostr.js", + contentScriptWebBTC: "./src/extension/content-script/webbtc.js", contentScriptOnStart: "./src/extension/content-script/onstart.ts", inpageScript: "./src/extension/inpage-script/index.js", - inpageScriptWebLN: "./src/extension/inpage-script/webln.js", - inpageScriptWebBTC: "./src/extension/inpage-script/webbtc.js", - inpageScriptLiquid: "./src/extension/inpage-script/liquid.js", - inpageScriptNostr: "./src/extension/inpage-script/nostr.js", - inpageScriptAlby: "./src/extension/inpage-script/alby.js", popup: "./src/app/router/Popup/index.tsx", prompt: "./src/app/router/Prompt/index.tsx", options: "./src/app/router/Options/index.tsx", @@ -230,6 +226,7 @@ var options = { }, plugins: [ + new ProviderInjectionPlugin(), new webpack.ProvidePlugin({ Buffer: ["buffer", "Buffer"], process: ["process"],