Intercept and replace images in web app with Chrome extension

1k Views Asked by At

I am attempting to write a chrome extension (for personal use) to swap/replace images loaded by a webpage with alternate images. I'd had this working for some time using chrome.webRequest, but am attempting to bring it up-to-speed with manifest v3.

My general solution is that I am hosting my replacement images on my own server, including a script to retrieve as json a list of such images. I fetch that list and, for each image, create a dynamic redirect rule with chrome.declarativeNetRequest.updateDynamicRules.

This all works beautifully if I request an image to be replaced in a main frame. I can see the successful match with an onRuleMatchedDebug listener, and (of course) the path is dutifully redirected.

However, when I load the web app that in turn loads the image (with javascript, presumably with xmlhttprequest?), the redirect rule does not trigger. The initiator (a javascript source file) is on the same domain and similar path to the images being replaced.

//manifest.json
{
  "name": "Image replace",
  "description": "Replace images in web app",
  "version": "2.0",
  "manifest_version": 3,
  "background": {"service_worker": "background.js"},
  "permissions": [
  "declarativeNetRequestWithHostAccess",
//  "declarativeNetRequestFeedback" // Not necessary once tested
  ],
  "host_permissions" : [
//    "https://domain1.com/outerframe/*", // Not necessary
    "https://domain2.com/innerframe/*",
    "https://domain3.com/*",
    "https://myexample.com/*"
  ]
}
// background.js
//chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => console.log(info)); // Not necessary once tested

var rules = [];
var idx = 1;
fetch("https://myexample.com/list") // returns json list like: ["subdir1\/image1.png", "subdir1\/image2.png", "subdir2\/image1.png"]
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
    for (const path of data) {
      var src = "https://domain2.com/innerframe/v*/files/" + path; // wildcards a version number
      var dst = "https://myexample.com/files/" + path;
      rules.push({
        "id" : idx++,
        "action" : {
          "type": "redirect",
          "redirect": {
            "url": dst
          }
        },
        "condition" : {
          "urlFilter": src,
          // In the end I only needed main_frame, image, and not xmlhttprequest
          "resourceTypes": ["main_frame", "image"]
        }
      });
    }
    chrome.declarativeNetRequest.updateDynamicRules({"addRules": rules, "removeRuleIds" : rules.map(r => r.id)});
  });

Again, this DOES all work IF I load a source image directly in chrome, but fails when it's being loaded by the javascript app.

I also attempted to test the match by specifying the proper initiator with testMatchOutcome, but my browser seems to claim this API does not exist. Not at all sure what could be wrong here.

// snippet attempted after above updateDynamicRules call
    chrome.declarativeNetRequest.testMatchOutcome({
      "initiator": "https://domain2.com/innerframe/files/script.js",
      "type": "xmlhttprequest",
      "url": "https://domain2.com/innerframe/v001/files/subdir/image1.png"
    }, (outcome) => console.log(outcome)); 

I would expect a redirect to "https://myexample.com/files/subdir/image1.png" Instead, I get this error: Uncaught (in promise) TypeError: chrome.declarativeNetRequest.testMatchOutcome is not a function

Documentation https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#method-testMatchOutcome says it's supported in chrome 103+. I'm running chrome 108.0.5359.72

Thanks!

Edit: Example code updated to reflect my answer below.

2

There are 2 best solutions below

0
On BEST ANSWER

I've managed to work out why direct requests were redirected while script loaded ones were not. My problem was with the initiator and host permissions. I had been relying on Chrome developer tools to provide the initiator, which in the above example originated with domain2.com. However, the actual host permission I needed was from a third domain (call it domain3.com), which seems to be the source of the content that loaded scripts from domain2.com.

I discovered this when I recalled that host permissions allows "<all_urls>", which is not a good idea long term, but it did allow the redirects to complete. From there, my onRuleMatchedDebug listener could fire and log to the console the characteristics of the redirect, which showed me the proper initiator I was missing.

Having a concise view of the redirects I need, I can now truncate some of these options to only the ones actually needed (edited in original question).

Subsequent to that I thought to look back at the HTTP requests in developer tools and inspect the Referer header, which also had what I was needing.

So, silly oversights aside, I would like to leave this question open a little while longer in case anyone has any idea why chrome.declarativeNetRequest.testMatchOutcome seems unavailable in Chrome 108.0.5359.72 but is documented for 103+. I'd chalk it up to the documentation just being wrong, but it seems this function must have shipped at some point and somehow was erroneously removed? Barring any insights, I might just submit it as a bug.

0
On

How to use chrome.declarativeNetRequest.testMatchOutcom:

Documentation:

testMatchOutcome, getMatchedRules, and onRuleMatchedDebug are available to assist with testing rules and rulesets. These APIs require the "declarativeNetRequestFeedback" permissions.