Tyler Smith
Tyler Smith

Reputation: 51

chrome.webRequest.onBeforeRequest acting unpredictably

I'm trying to make a web filtering chrome extension that will block certain sites and replace their html with a block page included in the extension.

let bans = ["*://*.amazon.com/*", "*://*.youtube.com/*", "*://*.netflix.com/*","*://*.facebook.com/*", "*://*.twitter.com/*"];

chrome.webRequest.onBeforeRequest.addListener(
    function(details){
        try{
            chrome.tabs.executeScript(null, {file: "content.js"});
        }
        catch(error){
            console.error(error);
        }
        return {cancel: true};
    },
    {urls: bans},
    ["blocking"]
);

This should mean that if I try to visit any site on that banned list the content script should replace the page with my own block page. However for some reason some sites never load the block page, other sites don't get blocked at all and some sites seem to work perfectly. Even stranger the listener seems to be triggered on sites not listed in the bans array at all. I can't figure out any sort of pattern between these behaviors.

I don't believe they are the source of the problem but here are the permissions in my manifest (manifest v2)

"web_accessible_resources": [
  "certBlockPage.html",
  "blockPageLight.html"
],

"incognito": "split",

"permissions": [
  "webNavigation",
  "webRequest",
  "webRequestBlocking",
  "tabs",
  "windows",
  "identity",
  "http://*/*",
  "https://*/*",
  "<all_urls>"
  ]

and here is the content.js file

window.onload = function(){
fetch(chrome.runtime.getURL('certBlockPage.html'))
    .then(r => r.text())
    .then(html => {
        document.open();
        document.write(html);
        document.close;
  });

}

Upvotes: 2

Views: 2311

Answers (1)

woxxom
woxxom

Reputation: 73616

There are several problems.

  1. You didn't specify runAt: 'document_start' in executeScript's options so it will wait in case you navigated to a banned site while the old page in this tab was still loading.
  2. executeScript is asynchronous so it can easily run after load event was already fired in the tab so your window.onload will never run. Don't use onload, just run the code immediately.
  3. You always run executeScript in the currently focused tab (the term is active) but the tab may be non-focused (inactive). You need to use details.tabId and details.frameId.
  4. The user may have opened a new tab and typed the blocked url or clicked its link, which is blocked by your {cancel: true}, but then executeScript will fail because the newtab page, which is currently shown in this tab, can't run content scripts. Same for any other chrome:// or chrome-extension:// tab or when a network error is displayed inside the tab.
  5. If you call onBeforeRequest.addListener another time without removing the previous registration, both will be active.
  6. document.write will fail on sites with strict CSP

Solution 1: webRequest + executeScript

background script:

updateListener(['amazon.com', 'youtube.com', 'netflix.com']);

function updateListener(hosts) {
  chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequest);
  chrome.webRequest.onBeforeRequest.addListener(
    onBeforeRequest, {
      types: ['main_frame', 'sub_frame'],
      urls: hosts.map(h => `*://*.${h}/*`),
    }, [
      'blocking',
    ]);
}

function onBeforeRequest(details) {
  const {tabId, frameId} = details;
  chrome.tabs.executeScript(tabId, {
    file: 'content.js',
    runAt: 'document_start',
    matchAboutBlank: true,
    frameId,
  }, () => chrome.runtime.lastError && redirectTab(tabId));
  // Cancel the navigation without showing that it was canceled
  return {redirectUrl: 'javascript:void 0'};
}

function redirectTab(tabId) {
  chrome.tabs.update(tabId, {url: 'certBlockPage.html'});
}

content.js:

fetch(chrome.runtime.getURL('certBlockPage.html'))
  .then(r => r.text())
  .then(html => {
    try {
      document.open();
      document.write(html);
      document.close();
    } catch (e) {
      location.href = chrome.runtime.getURL('certBlockPage.html');
    }
  });

Solution 2: webRequest + redirection

No need for content scripts.

const bannedHosts = ['amazon.com', 'youtube.com', 'netflix.com'];

chrome.webRequest.onBeforeRequest.addListener(
  details => ({
    redirectUrl: chrome.runtime.getURL('certBlockPage.html'),
  }), {
    types: ['main_frame', 'sub_frame'],
    urls: bannedHosts.map(h => `*://*.${h}/*`),
  }, [
    'blocking',
  ]);

Solution 3: declarativeNetRequest + redirection

This is the fastest method but it's limited to a very simple predefined set of actions. See the documentation and don't forget to add "declarativeNetRequest" to "permissions" in manifest.json, and "<all_urls>" to host_permissions (ManifestV3 still doesn't support optional permissions).

const bannedHosts = ['amazon.com', 'youtube.com', 'netflix.com'];

chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: bannedHosts.map((h, i) => i + 1),
  addRules: bannedHosts.map((h, i) => ({
    id: i + 1,
    action: {type: 'redirect', redirect: {extensionPath: '/certBlockPage.html'}},
    condition: {urlFilter: `||${h}/`, resourceTypes: ['main_frame', 'sub_frame']},
  })),
});

Upvotes: 4

Related Questions