Receive response of http request in chrome extension - javascript

So I am trying to get a way to display the content of a http response made by clicking a button on a webapplication and display it in a chrome extension.
The http request is to an API and responds with 1 line of data, so i'm trying to capture this, whenever a request is sent off to a specific url and store the response of the request as a variable of some kind .
So far I've followed this guide to get an understanding:
https://betterprogramming.pub/chrome-extension-intercepting-and-reading-the-body-of-http-requests-dd9ebdf2348b
This is nearly what I want to achieve but I Cant get it to work
Im hit with the error "Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval'"."
Not really sure how I can just listen for the response of a certain request, any help?
Source code:
Manifest.json:
{
"name": "SafePaste",
"description": "Build an Extension!",
"version": "1.0",
"manifest_version": 3,
"devtools_page": "devtools.html",
"permissions": [
"clipboardRead",
"clipboardWrite",
"storage",
"activeTab"
],
"host_permissions": ["https://demogamesfree.pragmaticplay.net/gs2c/v3/gameService/"],
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["contentScript.js"],
"run_at": "document_start"
}]
}
contentScript.js
function interceptData() {
var xhrOverrideScript = document.createElement('script');
xhrOverrideScript.type = 'text/javascript';
xhrOverrideScript.innerHTML = '
(function() {
var XHR = XMLHttpRequest.prototype;
var send = XHR.send;
var open = XHR.open;
XHR.open = function(method, url) {
this.url = url; // the request url
return open.apply(this, arguments);
}
XHR.send = function() {
this.addEventListener('load', function() {
if (this.url.includes('<url-you-want-to-intercept>')) {
var dataDOMElement = document.createElement('div');
dataDOMElement.id = '__interceptedData';
dataDOMElement.innerText = this.response;
dataDOMElement.style.height = 0;
dataDOMElement.style.overflow = 'hidden';
document.body.appendChild(dataDOMElement);
}
});
return send.apply(this, arguments);
};
})();
`
document.head.prepend(xhrOverrideScript);
}
function checkForDOM() {
if (document.body && document.head) {
interceptData();
} else {
requestIdleCallback(checkForDOM);
}
}
requestIdleCallback(checkForDOM);
function scrapeData() {
var responseContainingEle = document.getElementById('__interceptedData');
if (responseContainingEle) {
var response = JSON.parse(responseContainingEle.innerHTML);
} else {
requestIdleCallback(scrapeData);
}
}
requestIdleCallback(scrapeData);
devtools.html
<script src="devtools.js"></script>
devtools.js
chrome.devtools.panels.create("MyPanel", null, 'panel.html'
panel.html
<html>
<body>
<script src="panel.js"></script>
</body>
</html>
panel.js
chrome.devtools.network.onRequestFinished.addListener(request => {
request.getContent((body) => {
if (request.request && request.request.url) {
if (request.request.url.includes('<url-to-intercept>')) {
chrome.runtime.sendMessage({
response: body
});
}
}
});
});

Put xhrOverrideScript.innerHTML inside a separate .js file, and instead use
xhrOverrideScript.src = chrome.runtime.getURL('./script.js');
Add this to the manifest to allow the file to be executed and commujnicate with background page.
"web_accessible_resources": [
{
"resources": ["your_script.js"],
"matches": ["https://*/*", "http://*/*"]
}
],
"externally_connectable": {
"matches": [
"https://www.crunchbase.com/*"
]
},
Then add a message listener in background page like this
chrome.runtime.onMessageExternal.addListener(async (request, sender,
sendResponse) => {
console.log("🔰 Message From Injected Script 🔰")
console.log(request)
if (request.message == "Intercepted Request Response") {
console.log(request.Intercepted_response)
sendResponse("done")
}
})
And finally in the script injected:
function sendResponsetoBackground(response) {
chrome.runtime.sendMessage(
CE_id,
{ message: 'Intercepted Request Response', intercepted_response: response },
(response) => {
console.log('Response sent to Background page', response);
}
);
}

Related

Why does my Chrome extension only load on refresh? But attempts to run in background

My Chrome Extension is only working when I refresh the website I have it matched to.
If I navigate the website, it will not successfully load, however, I do see the content-script being re-run in the console. It seems to fail because it's not finding the HTML elements I'm looking for. On refresh, it can find those HTML elements, and works fine.
I've been trying a few things like chrome.tabs.onUpdated.addListener and MutationObserver but couldn't figure it out. Most likely because my JS skills are fairly limited.
Link to the extension documents:
https://www.dropbox.com/s/x31uvkdpdcnhchz/chrome-ext-stack-example.zip?dl=0
How can I get the content-script to find the HTML elements as I navigate without always having to refresh?
Any thoughts on what I'm screwing up?
Thank you!
manifest.json
{
"manifest_version": 3,
"name": "Test",
"description": "Example for StackOverflow",
"version": "0.0.1",
"host_permissions": ["<all_urls>"],
"permissions": ["storage", "activeTab", "scripting", "tabs", "webNavigation"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://www.zillow.com/homedetails/*"],
"js": ["ballpark.js"],
"css": ["main.css"]
}
],
"web_accessible_resources": [
{
"resources": ["/images/*"],
"matches": ["<all_urls>"]
}
]
}
background.js
function injectScript(tabId) {
chrome.scripting.executeScript(
{
target: {tabId: tabId},
files: ['ballpark.js'],
}
);
}
// adds a listener to tab change
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// check for a URL in the changeInfo parameter (url is only added when it is changed)
if (changeInfo.url) {
// calls the inject function
injectScript(tabId);
}
});
ballpark.js
var offerPrice;
// setTimeout(startWidget, 2000);
startWidget()
function startWidget() {
if(location.href.match(/homedetails/g)) {
console.log("YES Zillow Home Details URL");
getAskingPrice();
insertWidget();
} else {
console.log("NO Zillow Home Details URL");
}
}
// Get Price from Zillow
function getAskingPrice() {
var askingPrice = document.querySelector('[data-testid="price"] span');
if(askingPrice !== null) {
offerPrice = parseFloat(askingPrice.innerText.replace(/\$|,/g, ''));
console.log(offerPrice + " Offer Price");
} else {
console.log("Null Asking Price");
}
}
// Find Zillow widget to insert the extension widget
function insertWidget() {
const select_div_for_bpd = document.querySelector('div.Spacer-c11n-8-65-2__sc-17suqs2-0');
if(select_div_for_bpd !== null) {
const ballpark_container = document.createElement("div");
ballpark_container.setAttribute("id", "ballpark-container");
select_div_for_bpd.appendChild(ballpark_container);
ballpark_container.innerHTML = `
<div class="ballpark-roi-container">
<div><h1>${offerPrice}</h1> Offer Price</div>
</div>
`;
} else {
console.log("Cannot insert ballpark widget");
}
}
for getAskingPrice and insertWidget you need to wait from the DOM to be ready first.
try this
function startWidget() {
if(location.href.match(/homedetails/g)) {
console.log("YES Zillow Home Details URL");
const fn = () => {
getAskingPrice();
insertWidget();
}
document.addEventListener('DOMContentLoaded', fn, false);
} else {
console.log("NO Zillow Home Details URL");
}
}
You should wait until the document completely loaded before you interact with the page.
You could implement a function which will return a promise if the document has loaded. Your function could look something like this.
function getCompletedReadyState()
{
return new Promise((resolve) =>
{
if (document.readyState === 'complete')
{
resolve('completed');
}
document.onreadystatechange = () =>
{
if (document.readyState === 'complete')
{
resolve('completed');
}
};
});
}
Then you could call the method at the end of your content script like so
...
(async () => {
await getCompletedReadyState();
startWidget()
})();
You can read more about document.readyState here
I added the following to the top of my content-script file and now my extension will keep looking for a specific class as I navigate the SPA website.
const observer = new MutationObserver(function() {
if (document.getElementsByClassName('name-of-class-you-want')[0]) {
// add what you want to do here...
}
})
const target = document.querySelector('body');
const config = { childList: true };
observer.observe(target, config);

Javascript Browser WebExtension API Manifest v3 - Service Worker and chrome.storage.local API issue

I'm implementing a small extension for Copy as cURL feature (as done by the Network tab of DevTools) and I would like to use Manifest v3. According to the documentation and to the contribution of the community, Service Worker at a certain time stops to live so some variables cannot retrieve the needed information from the active tab.
For managing this, I'm using chrome.storage.local.set and .get functions in order to keep the needed information also after the Service Worker stops to live. When I run the extension test, I don't receive any error, but, despite I retrieve the stored variables by the chrome.storage API, sometimes I continue to not retrieve the values anymore also when the Service Worker should be alive. For example:
when I connect to a website, I can retrieve and copy the correct data also in 1 min, then, if I continue to Copy (without refreshing the page), I don't get the parameters (i.e., GET headers).
sometimes, if I open a new tab, insert an address and quickly press Copy as cURL, of my extension, headers are not copied, and I need to refresh the page (not by clicking refresh button of browser but click on URL then ENTER) for getting them.
Maybe the issue is not related to the Time-to-live of the Service Worker because I can keep a page opened for a lot of minutes and it gives me the right parameters. I don't know where my approach is failing. The code of this small implementation is the following:
background.js
"use strict";
/*
Called when the item has been created, or when creation failed due to an error.
We'll just log success/failure here.
*/
function onCreated() {
if (chrome.runtime.lastError) {
console.log(`Error: ${chrome.runtime.lastError}`);
} else {
console.log("Item created successfully");
}
}
/*
Called when the item has been removed.
We'll just log success here.
*/
function onRemoved() {
console.log("Item removed successfully");
}
/*
Called when there was an error.
We'll just log the error here.
*/
function onError(error) {
console.log(`Error: ${error}`);
}
/*
Create all the context menu items.
*/
chrome.contextMenus.create({
id: "tools-copy",
//title: chrome.i18n.getMessage("menuItemToolsCopy"),
title: "Copy",
contexts: ["all"],
}, onCreated);
chrome.contextMenus.create({
id: "tools-copy-curl",
parentId: "tools-copy",
//title: chrome.i18n.getMessage("menuItemToolsCopyAsFFUF"),
title: "Copy as cURL",
contexts: ["all"],
}, onCreated);
const tabData = {};
const getProp = (obj, key) => (obj[key] || (obj[key] = {}));
const encodeBody = body => {
var data = '';
// Read key
for (var key in body.formData) { //body is a JSON object
data += `${key}=${body.formData[key]}&`;
}
data = data.replace(/.$/,"");
var body_data = `'${data}'`;
return body_data;
}
const FILTER = {
types: ['main_frame', 'sub_frame'],
urls: ['<all_urls>'],
};
const TOOLS = {
CURL: 'tools-copy-curl',
};
chrome.webRequest.onBeforeRequest.addListener(e => {
getProp(getProp(tabData, e.tabId), e.frameId).body = e.requestBody;
chrome.storage.local.set({tabData: tabData}, function() {
console.log('HTTP request saved');
});
}, FILTER, ['requestBody']);
chrome.webRequest.onBeforeSendHeaders.addListener(e => {
getProp(getProp(tabData, e.tabId), e.frameId).headers = e.requestHeaders;
chrome.storage.local.set({tabData: tabData}, function() {
console.log('HTTP request saved');
});
}, FILTER, ['requestHeaders']);
chrome.tabs.onRemoved.addListener(tabId => delete tabData[tabId]);
chrome.tabs.onReplaced.addListener((addId, delId) => delete tabData[delId]);
chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.storage.local.get(["tabData"], function(items) {
const data = items.tabData[tab.id]?.[info.frameId || 0] || {};
if (info.menuItemId === TOOLS.CURL) {
var txt_clip = `curl -u '${info.frameUrl || tab.url}'` +
(data.headers?.map(h => ` -H '${h.name}: ${h.value}'`).join('') || '') +
(data.body? ' --data_raw ' + encodeBody(data.body) : '');
}
chrome.tabs.sendMessage(tab.id,
{
message: "copyText",
textToCopy: txt_clip
}, function(response) {})
});
});
content.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.message === "copyText") {
navigator.clipboard.writeText(request.textToCopy);
sendResponse({status: true});
}
}
);
manifest.json
{
"manifest_version": 3,
"name": "CopyAsCURL",
"description": "Copy as cURL test example.",
"version": "1.0",
"default_locale": "en",
"background": {
"service_worker": "background.js"
},
"permissions": [
"contextMenus",
"activeTab",
"cookies",
"webRequest",
"tabs",
"clipboardWrite",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": ["content.js"]
}
],
"icons": {
"16": "icons/menu-16.png",
"32": "icons/menu-32.png",
"48": "icons/menu-48.png"
}
}
I want also to thank #wOxxOm for the support on similar topic.

getting `DOMException: Failed due to shutdown` when trying to record stream from screen+microphone

I am building an extension that records my screen and microphone audio as well.
Overview:
I have content.js which tries to get the access of navigator.mediaDevices.getUserMedia({... in the succession I send message to the background.js which again tries to access navigator.mediaDevices.getUserMedia({... over here I am recording the audio stream. In addition to this, I have popup.html which have a start button on clicking the button I am recording the screen. But in the full process I am getting the error DOMException: Failed due to shutdown.
Also, I am aware there have been questions on the above error (the famous one Chrome Extension - getUserMedia throws "NotAllowedError: Failed due to shutdown") but didn’t help me much.
content.js
navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true }
})
.then((e) => { chrome.runtime.sendMessage({ from: 'success' })})
.catch((e) => {console.log(e);});
Background.js
var recorder = null;
chrome.runtime.onMessage.addListener(gotMessage);
function gotMessage(message) {
if(message.from === 'success') {
navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true }
})
.then((e) => {
var chunks = [];
var options = {
mimeType: 'audio/webm',
};
recorder = new MediaRecorder(e, options);
recorder.ondataavailable = function (event) {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = function () {
var superBuffer = new Blob(chunks, {
type: 'audio/webm',
});
var url = URL.createObjectURL(superBuffer);
};
recorder.start();
})
.catch((e) => {console.log(e);});
}
popup.js:
let btnStartTab = document.getElementById('start');
let btnStartTab1 = document.getElementById('stop');
btnStartTab.addEventListener('click', function (event) {
chrome.runtime.sendMessage({ from: 'start' });
});
btnStartTab1.addEventListener('click', function (event) {
chrome.runtime.sendMessage({ from: 'stop' });
});
manifest.json
{
"name": "My Recorer",
"version": "1.0",
"manifest_version": 2,
"background": {
"scripts": ["background.js"],
"persistent": true
},
"browser_action": {
"default_icon": "test.png"
},
"commands": {
"run-foo": {
"suggested_key": {
"default": "Ctrl + Shift + Y",
"mac": "Command+Shift+Y"
},
"description": "Run \"foo\" on the current page."
},
"_execute_action": {
"suggested_key": {
"default": "Ctrl + Shift + Y",
"mac": "Command+Shift+Y"
}
}
},
"permissions": [
"activeTab",
"clipboardWrite",
"declarativeContent",
"storage",
"tabs",
"tabCapture",
"desktopCapture",
"alarms",
"activeTab",
"downloads",
"<all_urls>"
]
}
After deep searching and help from SO foundation found a way to reach the solution thought of sharing.
So, we need to understand that background doesn’t have control over DOM that's why we need to take help from contentscript.js and below architecture is important :
In my case, I am injecting contentscript.js from manifest.json but one can use executescript() to inject the script.
contentscript.js will create an iframe with display none with iframe.src = chrome.extension.getURL('audiosources.html'); above is important!!
Also, audiosources.html is under web_accessible_resources in manifest.json
The body of above HTML will have <script src="audiosources.js"></script> only.
audiosources.js is the oe which will help to get rid of the error here I am getting access to navigator.mediaDevices.getUserMedia(...) and all the mic devices. here on success it will send message to background.js.
Now, background.js will do its magic of tabCapture with navigator.mediaDevices.getUserMedia and here we need to apply logic for merging.

How can an extension listen to a WebSocket? (and what if the WebSocket is within an iframe?)

I am writing a Firefox extension that needs to listen to communication between browser and server. For HTTP communication, I use the webRequest library in a background script. But according to this Mozilla bug report, I cannot use webRequest to intercept WebSocket messages. How can my extension listen in on WebSocket communication?
Update:
I've injected my WebSocket wrapper, but it's never called! The WebSocket I am trying to listen to is inside an iframe. Does that matter?
The only way to listen to WebSocket communication is to inject a WebSocket wrapper into the site's code and have it relay the messages to you. You're code should look something like this:
manifest.json
{
...
"content_scripts": [
{
"matches": [
"<website-url>",
],
"js": ["content/syringe.js"]
}
],
"web_accessible_resources": ["lib/socket-sniffer.js"]
}
content/syringe.js
var s = document.createElement('script');
s.src = browser.runtime.getURL('lib/socket-sniffer.js');
s.onload = function() {
this.remove();
};
lib/socket-sniffer.js
(function() {
var OrigWebSocket = window.WebSocket;
var callWebSocket = OrigWebSocket.apply.bind(OrigWebSocket);
var wsAddListener = OrigWebSocket.prototype.addEventListener;
wsAddListener = wsAddListener.call.bind(wsAddListener);
window.WebSocket = function WebSocket(url, protocols) {
var ws;
if (!(this instanceof WebSocket)) {
// Called without 'new' (browsers will throw an error).
ws = callWebSocket(this, arguments);
} else if (arguments.length === 1) {
ws = new OrigWebSocket(url);
} else if (arguments.length >= 2) {
ws = new OrigWebSocket(url, protocols);
} else { // No arguments (browsers will throw an error)
ws = new OrigWebSocket();
}
wsAddListener(ws, 'message', function(event) {
console.log("Received:", event);
});
return ws;
}.bind();
window.WebSocket.prototype = OrigWebSocket.prototype;
window.WebSocket.prototype.constructor = window.WebSocket;
var wsSend = OrigWebSocket.prototype.send;
wsSend = wsSend.apply.bind(wsSend);
OrigWebSocket.prototype.send = function(data) {
console.log("Sent:", data);
return wsSend(this, arguments);
};
})();
(document.head || document.documentElement).appendChild(s);
All credit for the above code obviously goes to the posters linked above. I have only copied tt here for convenience' sake.
Update:
Yes, it does matter that the WebSocket is within an iframe! By default, extensions are only loaded in the top-most frame. For it to load into the iframe, you must add "all_frames": true to your manifest.json:
manifest.json
{
...
"content_scripts": [
{
"matches": [
"<website-url>",
],
"all_frames": true,
"js": ["content/syringe.js"]
}
],
"web_accessible_resources": ["lib/socket-sniffer.js"]
}
If you'd like to read more, here's the documentation for all_frames.
I don't know if you still need this, but I got it to work by editing something from trexinf14s's answer.
1, Add syringe.js to your manifest.json, "run_at": "document_end" if you want this extension run after webpage is fully loaded, change "web_accessible_resources" to new manifest format.
"content_scripts": [{
"css": ["styles.css"],
"run_at": "document_end",
"all_frames": true,
"js": ["lib/jquery-3.6.0.min.js","content/jquerycontent.js","content/syringe.js"],
"matches": ["https://example.com/*"]
}],
"web_accessible_resources": [{
"resources": ["lib/socket-sniffer.js"],
"matches": ["<all_urls>"]
}]
2, inject "s" variable at syringe.js (not at socket-sniffer.js), the result is:
var s = document.createElement('script');
s.src = chrome.runtime.getURL('lib/socket-sniffer.js');
s.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
Remove appendChilds(s) from socket-sniffer.js, the result is:
(function () {
var OrigWebSocket = window.WebSocket;
var callWebSocket = OrigWebSocket.apply.bind(OrigWebSocket);
var wsAddListener = OrigWebSocket.prototype.addEventListener;
wsAddListener = wsAddListener.call.bind(wsAddListener);
window.WebSocket = function WebSocket(url, protocols) {
var ws;
if (!(this instanceof WebSocket)) {
// Called without 'new' (browsers will throw an error).
ws = callWebSocket(this, arguments);
} else if (arguments.length === 1) {
ws = new OrigWebSocket(url);
} else if (arguments.length >= 2) {
ws = new OrigWebSocket(url, protocols);
} else { // No arguments (browsers will throw an error)
ws = new OrigWebSocket();
}
wsAddListener(ws, 'message', function (event) {
console.log("Received:", event);
});
return ws;
}.bind();
window.WebSocket.prototype = OrigWebSocket.prototype;
window.WebSocket.prototype.constructor = window.WebSocket;
var wsSend = OrigWebSocket.prototype.send;
wsSend = wsSend.apply.bind(wsSend);
OrigWebSocket.prototype.send = function (data) {
//console.log("Sent:", data);
return wsSend(this, arguments);
};
})();
console.log("nothing to show");

How to share a unique tab ID between content and background scripts without an asynchronous delay?

I have built a chrome extension and I'm getting hit by a race condition that I need help with.
If you see the answer chrome extension: sharing an object between content scripts and background script it tells us that you cannot share a variable between content and background scripts.
My goal is to generate a unique ID per-browser tab and then share it in between the content.js and the background.js. Then I need to use this value in a content injected javascript as explained in this answer: In Chrome extensions, can you force some javascript to be injected before everything?
The only way I have been able to figure out how to do this is by doing the following async code then I just use the tab ID as the unique ID:
content.js
await pingBackground();
async function pingBackground() {
var info;
await new Promise(function (resolve, reject) {
chrome.runtime.sendMessage({ type: 1 }, function (response) {
if (response !== undefined) {
info = response;
resolve();
}
else {
reject();
}
});
});
console.log("Id is " + info.id);
}
background.js
chrome.runtime.onMessage.addListener(messageHandler);
function messageHandler(message, sender, reply) {
switch (message.type) {
case 1:
reply({ 'id': sender['tab'].id, 'active': true });
break;
}
}
manifest.json
{
"name": "oogi",
"version": "0.1",
"manifest_version": 2,
"background": {
"scripts": [
"common.js",
"background.js"
],
"persistent": true
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"permissions": [
"contentSettings",
"webRequest",
"webRequestBlocking",
"*://*/*"
]
}
But the problem with this is by the time I get the tab ID from background js, the script's content has already been loaded.
Is there some way to make it so this variable can be asynchronously shared between background.js and content.js? Or is this simply impossible?
Can I switch it around and have background.js load a variable from content.js asynchronously?
UPDATE:
A terrible hack which works is to do this in the foreground of the content.js:
var sleepScript = document.createElement('script');
var sleepCode = `function sleep (ms) {
var start = new Date().getTime();
while (new Date() < start + ms) {}
return 0;
}
sleep(500);`;
sleepScript.textContent = sleepCode;
(document.head || document.documentElement).appendChild(sleepScript);
This will force the page to wait for a bit giving the time to query the background before running the inline dom.
It works but that's awful.
This question was already answered previously, although it is hard to tell that this is the same issue at first glance.
https://stackoverflow.com/a/45105934
The answer is pretty descriptive so give it a read.
Here is the script changes that make it work:
// background.js
function addSeedCookie(details) {
details.responseHeaders.push({
name: "Set-Cookie",
value: `tab_id=${details.tabId}; Max-Age=2`
});
return {
responseHeaders: details.responseHeaders
};
}
chrome.webRequest.onHeadersReceived.addListener(
addSeedCookie, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]
);
// inject.js
function getCookie(cookie) { // https://stackoverflow.com/a/19971550/934239
return document.cookie.split(';').reduce(function(prev, c) {
var arr = c.split('=');
return (arr[0].trim() === cookie) ? arr[1] : prev;
}, undefined);
}
var tabId = getCookie("tab_id");

Categories