TL;DR - Can I access Module Federation remotes from within a content script of a chrome extension?
I'm currently developing a Chrome extension and faced the problem that can be represented by the following.
Let's say I have 2 applications - extension and actionsLogger.
These applications are linked via Webpack Module Federation like that:
actionsLogger/webpack.js
{
...,
plugins: [
new ModuleFederationPlugin({
name: 'actionsLogger',
library: { type: 'var', name: 'actionsLogger' },
filename: 'remoteEntry.js',
exposes: {
'./logClick': './actions/logClick',
}
}),
],
devServer: {
port: 3000,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
},
static: {
directory: path.resolve(__dirname, 'dist'),
}
},
...
}
extension/webpack.js
{
...,
plugins: [
new webpack.ProvidePlugin({
browser: 'webextension-polyfill'
}),
new ModuleFederationPlugin({
name: 'extension',
remotes: {
actionsLogger: 'actionsLogger#http://localhost:3000/remoteEntry.js',
},
}),
],
...
}
So, as you can see, actionsLogger is running on port 3000 and extension is referring to it via Module Federation. actionsLogger contains a simple function to get position of a cursor in case of a click event.
actionsLogger/actions/logClick.js
function logClick(event) {
return { X: event.clientX, Y: event.clientY };
}
export default logClick;
Other application - extension, contains all the code for the chrome extension together with this particular script that imports logClick from actionsLogger/logClick and sends the position of a cursor to the background page whenever a click happens:
extension/tracker.js
import('actionsLogger/logClick').then((module) => {
const logClick = module.default;
document.addEventListener("click", (event) => {
const click = logClick(event);
chrome.runtime.sendMessage(click);
});
});
So manifest.json in extension looks like this:
extension/manifest.json
{
...
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["tracker.js"]
}],
...
}
And here comes the problem. If I try to open some web page with the extension installed and running, I get the following error:
Uncaught (in promise) ScriptExternalLoadError: Loading script failed.
(missing: http://localhost:3000/remoteEntry.js)
while loading "./logClick" from webpack/container/reference/actionsLogger
at webpack/container/reference/actionsLogger (remoteEntry.js":1:1)
at __webpack_require__ (bootstrap:18:1)
at handleFunction (remotes loading:33:1)
at remotes loading:52:1
at Array.forEach (<anonymous>)
at __webpack_require__.f.remotes (remotes loading:15:1)
at ensure chunk:6:1
at Array.reduce (<anonymous>)
at __webpack_require__.e (ensure chunk:5:1)
at iframe.js:44:1
First of all, I thought that I misconfigured something in my Module Federation settings, but then I tried the following - added inject.js:
extension/inject.js
const script = document.createElement('script');
script.src = "chrome-extension://ddgdsaidlksalmcgmphhechlkdlfocmd/tracker.js";
document.body.appendChild(script);
Modified manifest.json in extension:
extension/manifest.json
{
...
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["inject.js"]
}],
"web_accessible_resources": [
"tracker.js",
]
...
}
And now Module Federation works fine, but since extension/tracker.js imports import('actionsLogger/logClick') from a remote, some websites that have content security policy defined might block this request (e.g. IKEA). So this approach also won't always work.
And the initial problem with accessing MF module from content script probably happens because content scripts are running in their own isolated environment and module federation can't resolve there properly. But maybe there are some configuration flags/options or some other/better ways to make it work?
Would appreciate any advice.
Of course, minutes after I posted the question I came up with a working solution. So, basically, I decided to use the second approach where extension/tracker.js is injected via <script /> tag on a page by extension/inject.js and for the CSP issue I've added a utility that blocks a CSP header on a request for the page.
manifest.json
{
"permissions": [
...,
"webRequest",
"webRequestBlocking",
"browsingData",
...
]
}
extension/disableCSP.js
function disableCSP() {
chrome.browsingData.remove({}, { serviceWorkers: true }, function () {});
const onHeaderFilter = { urls: ['*://*/*'], types: ['main_frame', 'sub_frame'] };
browser.webRequest.onHeadersReceived.addListener(
onHeadersReceived, onHeaderFilter, ['blocking', 'responseHeaders']
);
};
function onHeadersReceived(details) {
for (let i = 0; i < details.responseHeaders.length; i++) {
if (details.responseHeaders[i].name.toLowerCase() === 'content-security-policy') {
details.responseHeaders[i].value = '';
}
}
return { responseHeaders: details.responseHeaders };
};
export default disableCSP;
And then just call disableCSP() on the background script.
I'm a bit unsure about security drawbacks of such an approach, so feel free to let me know if a potential high risk vulnerability is introduced by this solution.
Related
I've built a website with Nextjs (using version 12.1.4). For SEO purposes I would like to make a permanent redirect for my www version of the site to my non-www. Normally this could easily be done with nginx or an .htaccess file with apache. However, static websites hosted on Digitalocean are not running apache or nginx so an .htaccess file won’t do. I've read that this should be possible using Nextjs redirects.
I've tried the following 3 redirects:
redirects: async () => [
{
source: '/:path*',
has: [
{
type: 'host',
value: 'www',
},
],
destination: '/:path*',
permanent: true,
},
],
---------------------------------------------------
redirects: async () => [
{
source: '/:path*/',
has: [
{
type: 'host',
value: 'www',
},
],
destination: '/:path*/',
permanent: true,
},
],
------------------------------------------------------
redirects: async () => [
{
source: '/:path*',
has: [{ type: 'host', value: 'https://www.cvtips.nl/' }],
destination: 'https://cvtips.nl/:path*',
permanent: true,
},
],
All of them don't seem to redirect to the non-www version. I don't know if it is relevant, but I do use trailingSlash: true in the config.
Next thing I tried is adding a middleware file. I both tried adding it at the root and calling it middleware.js and inside the pages folder calling it _middleware.js.
This is the code I use for the redirect:
--> https://github.com/vercel/next.js/discussions/33554
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const host = req.headers.get('host');
const wwwRegex = /^www\./;
// This redirect will only take effect on a production website (on a non-localhost domain)
if (wwwRegex.test(host) && !req.headers.get('host').includes('localhost')) {
const newHost = host.replace(wwwRegex, '');
return NextResponse.redirect(`https://${newHost}${req.nextUrl.pathname}`, 301);
}
return NextResponse.next();
}
Also does not work at all... Doesn't do anything I believe.
How can I redirect a Nextjs website from www to non-www?
I've got problem in proxy authentication using chrome extension (due to problems with v3 my code is based on manifest v2). The connection works using the following curl command:
curl -v -x https://11.111.11.111:222 https://www.google.com/ --proxy-header "proxy-authorization: Basic abc" --proxy-insecure
And I tried to implement it in extension. Here's what I have in background.js file:
chrome.runtime.onStartup.addListener(() => {
const config = {
mode: 'fixed_servers',
rules: {
singleProxy: {
host: '11.111.11.111',
port: 222,
scheme: 'https',
},
},
}
chrome.proxy.settings.set({ value: config, scope: 'regular' })
})
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
const requestHeaders = details.requestHeaders || []
requestHeaders.push({
name: 'proxy-authorization',
value: 'Basic abc',
})
return { requestHeaders }
},
{ urls: ['<all_urls>'] },
['blocking', 'requestHeaders', 'extraHeaders']
)
chrome.webRequest.onSendHeaders.addListener(
(details) => {
console.log('sending', details)
return details
},
{ urls: ['<all_urls>'] },
['requestHeaders', 'extraHeaders']
)
I have the following permissions in manifest.json:
"permissions": [
"cookies",
"storage",
"tabs",
"activeTab",
"proxy",
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
And these are the headers that are printed in onSendHeaders function:
You see that proxy-authorization header is present. But I get ERR_PROXY_CERTIFICATE_INVALID while trying to browse any page. Proxy headers should be set in a different way? Because I use --proxy-header flag in the curl command, not --header.
PS. I tried using the method that is mentioned many times:
chrome.webRequest.onAuthRequired.addListener(function(details, callbackFn) {
callbackFn({
authCredentials: { username: username, password: password }
});
},{urls: ["<all_urls>"]},['asyncBlocking']);
too, but this function is never called (I put console.log inside).
Have you solved this problem?
If not try this:
chrome.webRequest.onAuthRequired.addListener(function(details, callbackFn) {
callbackFn({
authCredentials: { username: username, password: password }
});
},{urls: ["<all_urls>"]},['blocking']);
Main change - AsyncBlocking => Blocking
I'm attempting to build a browser extension that changes the CSS of a website (among other things).
In the manifest.json file, one is able to specify a "matches" key to limit which sites the extension has permission to modify via the content script. Right now, I'm trying to present multiple different theme options, and each has its own CSS file.
Let's say I have three themes: theme1.css, theme2.css, and theme3.css
This is my save function, simply for reference.
function save(theme) {
chrome.storage.sync.set({"theme": theme}, function() {
console.log('Set the theme to ' + theme)
main()
})
}
Currently, I have this in my manifest.json:
"content_scripts": [{
"css": ["style.css"],
"js": ["content.js"],
"matches": ["https://*.example.com/*"]
}]
How can I check the stored theme to determine which one has been selected by the user, and then apply the corresponding CSS file if the URL is, let's say, https://example.com/?
Edit: I've since tried Declarative Content, this is what I've gotten. The Javascript is being injected but the CSS does not appear to be working.
var theme1 = {
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: 'example.com', schemes: ['https'] }
})
],
actions: [
new chrome.declarativeContent.RequestContentScript({
js: [
"content.js"
],
css: [
"css/theme1.css"
]
})
]
};
if (theme === "theme1") {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([theme1])
console.log("Set theme to theme1")
})
}
I am requiring a JavaScript file which is located within my external resources within built electron package.
let rPath = eval(require(path.resolve(__dirname + `/../src/raffles/boltRaffle0${_this.rPassID}.js`)));
When I run the application I get:
components sync:2 Uncaught (in promise) Error: Cannot find module
'C:\Users\James\AppData\Local\Programs\bolt-beta\resources\src\raffles\boltRaffle04.js'
at n (webpack:/src/components sync:2)
at o.webpack_require (webpack:/src/components/Main.vue:686)
at passRaffle (webpack:/src/components/Main.vue?c5be:1)
at apply (webpack:/node_modules/vue/dist/vue.esm.js:2027)
at HTMLDivElement.apply (webpack:/node_modules/vue/dist/vue.esm.js:1826)
I'm not sure why it can't find the file as it is there. Furthermore if I run
let rPath = eval(require(path.resolve(__dirname + `/../src/raffles/boltRaffle0${_this.rPassID}.js`)));
within the console, while the app is running it finds the file.
I'm not sure if it has anything to do with Webpack? Just in case here my webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [{
test: /\.vue$/,
use: 'vue-loader'
}]
},
plugins: [
new VueLoaderPlugin(),
new browserPlugin({
openOptions: {
app: [
'chrome',
//'--incognito',
'--disable-web-security', // to enable CORS
'--user-data-dir=' + path.resolve(chromeUserDataDir) // to let Chrome create and store here developers plugins, settings, etc.
]
}
})
]
}
What I want to do.
Using a Chrome extension I need to inject a script into the page context in such a way that the injected script runs before any other javascript on the page.
Why do I need this?
I need to hijack all console commands for a specific page so my extension can listen to the messages.
My current issue
Currently I am catching some of the messages logged by the page but not all of them, specifically, all messages from web-directory-132a3f16cf1ea31e167fdf5294387073.js are not being caught. After some digging I discovered that the web-directory-132a3f16cf1ea31e167fdf5294387073.js is also hijacking console but doing so before my script has a chance to.
As a visual, if I look at the network tab after loading the page, I see this:
My injected script is consoleInterceptor.js. It correctly captures the output of the js files that are loaded here accept web-directory-132a3f16cf1ea31e167fdf5294387073.js
Inside of web-directory-132a3f16cf1ea31e167fdf5294387073.js is some code something like this:
....
_originalLogger: t.default.Logger,
...
// do stuff with logging ....
this._originalLogger[e].apply(this._originalLogger, s),
What I think the problem is
It seems to me that, web-directory-132a3f16cf1ea31e167fdf5294387073.js is grabbing the standard console functions and storing them internally before my script has had a chance to replace them with my own versions. So even though my script works, the web-directory-132a3f16cf1ea31e167fdf5294387073.js still uses the original standard console functions it saved.
Note that web-directory-132a3f16cf1ea31e167fdf5294387073.js is an ember application and I dont see any simple way to hook into that code to overwrite those functions too but Im open to that as a solution.
My current code:
manifest.js
...
"web_accessible_resources": [
"js/ajaxInterceptor.js",
"js/consoleInterceptor.js"
],
"version" : "5.2",
"manifest_version": 2,
"permissions": [
"<all_urls>",
"tabs",
"activeTab",
"storage",
"webNavigation",
"unlimitedStorage",
"notifications",
"clipboardWrite",
"downloads",
"tabCapture",
"cookies",
"browsingData",
"webRequest",
"*://*/*",
"gcm",
"contextMenus",
"management"
],
"externally_connectable": {
"matches": ["*://apps.mypurecloud.com/*","*://*.cloudfront.net/*"]
},
...
background.js
var options = {url: [{hostContains: 'apps.mypurecloud.com'}]};
chrome.webNavigation.onCommitted.addListener(function(details) {
// first inject the chrome extension's id
chrome.tabs.executeScript(details.tabId, {
code: "var chromeExtensionId = " + JSON.stringify(chrome.runtime.id)
});
// then inject the script which will use the dynamically added extension id
// to talk to the extension
chrome.tabs.executeScript(details.tabId, {
file: 'js/injectConsoleInterceptor.js'
});
},
options
);
chrome.runtime.onMessageExternal.addListener(
function(msg, sender, sendResponse) {
if(msg.action === 'console_intercepted'){
_this.processConsoleMessage(sender, msg.details.method, msg.details.arguments);
}
});
injectConsoleInterceptor.js
var interceptorScript = document.createElement('script');
interceptorScript.src = chrome.extension.getURL('js/consoleInterceptor.js');
interceptorScript.onload = function(){this.remove();};
(document.head || document.documentElement).prepend(interceptorScript);
consoleInterceptor.js
if(!window.hasConsoleInterceptor){
window.hasConsoleInterceptor = true;
console.log('overriding console functions');
var originals ={};
var console = window.console;
if (console){
function interceptConsoleMethod(method){
originals[method] = console[method];
console[method] = function(){
// send the data to the extension
// chromeExtensionId should be injected into the page separately and before this script
var data = {
action: 'console_intercepted',
details: {
method: method,
arguments: arguments
}
};
chrome.runtime.sendMessage(chromeExtensionId, data);
originals[method].apply(console, arguments)
}
}
// an array of the methods we want to observe
var methods = ['assert', 'count', 'debug', 'dir', 'dirxml', 'error', 'group','groupCollapsed','groupEnd','info','log', 'profile', 'profileEnd','time','timeEnd','timeStamp','trace','warn','table'];
for (var i = 0; i < methods.length; i++){
interceptConsoleMethod(methods[i])
}
console.log('Successfully overridden console functions: '+methods.join(','));
}
}
My question
What can I do to make consoleInterceptor.js run before web-directory-132a3f16cf1ea31e167fdf5294387073.js loads so that web-directory-132a3f16cf1ea31e167fdf5294387073.js uses my modified console functions rather than the default browser console funcitons?
You can try this.In manifest.json file:
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["script/inject.js"],
"run_at":"document_start"
}
]
In inject.js:
var ss = document.createElement("script");
ss.innerHTML= "xxx";
document.documentElement.appendChild(ss);