在内容脚本之前注入 javascript 变量

IT技术 javascript google-chrome-extension
2021-02-04 03:51:10

使用我的后台脚本background.js,我需要在注入另一个文件inject.js 作为内容脚本之前注入一个动态变量作为内容脚本。Inject.js 需要访问这个变量并在页面上的任何脚本运行之前运行它的代码。我在从 inject.js 内容脚本访问动态变量时遇到困难。

清单文件.json

{
"name": "Shape Shifter",
"version": "1.0",
"description": "Anti browser fingerprinting web extension. Generates randomised values for HTTP request headers, javascript property values and javascript method return types.",
"manifest_version": 2,
"icons": {
    "32": "icons/person-32.png",
    "48": "icons/person-48.png"
},
"background": {
    "persistent": true,
    "scripts": ["js/ua.js", "js/words.js", "js/lib/seedrandom.min.js", "js/random.js", "js/background.js"]
},
"browser_action": {
    "default_title": "Shape Shifter",
    "default_icon": "icons/person-32.png",
    "default_popup": "html/popup.html"
},
"content_scripts": [
  {
    "run_at": "document_end",
    "matches": ["<all_urls>"],
    "js": ["js/inject.js"] 
  }
],
"permissions": [
    "webRequest",
    "webRequestBlocking",
    "webNavigation",
    "tabs",
    "activeTab",
    "storage",
    "<all_urls>"
],
"web_accessible_resources": [
    "js/ua.js",
    "js/words.js",
    "js/lib/seedrandom.min.js",
    "js/random.js",
    "js/api/document.js",
    "js/api/navigator.js",
    "js/api/canvas.js",
    "js/api/history.js",
    "js/api/battery.js",
    "js/api/audio.js",
    "js/api/element.js"
]

}

背景.js

"use strict";

console.log("Background Script Running ...");

function getSeed(origin) {
    // Get a Storage object
    var storage = window.localStorage;

    // Do we already have a seed in storage for this origin or not?
    var seed = storage.getItem(origin);

    if (seed === null) {
        // Initialise a 32 byte buffer
        seed = new Uint8Array(32);

        // Fill it with cryptographically random values
        window.crypto.getRandomValues(seed);

        // Save it to storage
        storage.setItem(origin, seed);
    }

    return seed;
}

// Methods to get HTTP headers
function getAcceptHeader(seed) {
    return "NotYetImplemented";
}
function getAcceptCharsetHeader(seed) {
    return "NotYetImplemented";
}
function getAcceptEncodingHeader(seed) {
    return "NotYetImplemented";
}
function getAcceptLanguageHeader() {
    // NOTE: TOR Browser uses American English
    return "en-US,en;q=0.5";
}
function getAuthorizationHeader(seed) {
    return "NotYetImplemented";
}
function getExpectHeader(seed) {
    return "NotYetImplemented";
}
function getFromHeader(seed) {
    return "NotYetImplemented";
}
function getHostHeader(seed) {
    return "NotYetImplemented";
}
function getIfMatchHeader(seed) {
    return "NotYetImplemented";
}
function getIfModifiedSinceHeader(seed) {
    return "NotYetImplemented";
}
function getIfNoneMatchHeader(seed) {
    return "NotYetImplemented";
}
function getIfRangeHeader(seed) {
    return "NotYetImplemented";
}
function getIfUnmodifiedSinceHeader(seed) {
    return "NotYetImplemented";
}
function getMaxForwardsHeader(seed) {
    return "NotYetImplemented";
}
function getProxyAuthorizationHeader(seed) {
    return "NotYetImplemented";
}
function getRangeHeader(seed) {
    return "NotYetImplemented";
}
function getRefererHeader() {
    // NOTE: From https://developer.mozilla.org/en-US/docs/Web/API/Document/referrer
    // NOTE: The value is an empty string if the user navigated to the page directly (not through a link, but, for example, via a bookmark).
    // NOTE: Since this property returns only a string, it does not give you DOM access to the referring page.

    // NOTE: Make websites think we always go to them directly rather than being referred.
    return "";
}
function getTEHeader(seed) {
    return "NotYetImplemented";
}
function getUserAgentHeader(seed) {
    Math.seedrandom(seed);

    return userAgents[randomNumber(0, userAgents.length)];
}

function rewriteHttpHeaders(e) {
    // Create URL object from url string
    var serverUrl = new URL(e.url);

    console.log(e);

    // Get the origin (hostname)
    var origin = serverUrl.hostname;

    var seed = getSeed(origin);

    console.log("Background - Seed for origin " + origin + ": " + seed);

    for (var header of e.requestHeaders) {
        if (header.name.toLowerCase() === "accept") {
        }
        else if (header.name.toLowerCase() === "accept-charset") {
        }
        else if (header.name.toLowerCase() === "accept-encoding") {
        }
        else if (header.name.toLowerCase() === "accept-language") {
            header.value = getAcceptLanguageHeader();
        }
        else if (header.name.toLowerCase() === "authorization") {
        }
        else if (header.name.toLowerCase() === "expect") {
        }
        else if (header.name.toLowerCase() === "from") {
        }
        else if (header.name.toLowerCase() === "host") {
        }
        else if (header.name.toLowerCase() === "if-match") {
        }
        else if (header.name.toLowerCase() === "if-modified-since") {
        }
        else if (header.name.toLowerCase() === "if-none-match") {
        }
        else if (header.name.toLowerCase() === "if-range") {
        }
        else if (header.name.toLowerCase() === "if-unmodified-since") {
        }
        else if (header.name.toLowerCase() === "max-forwards") {
        }
        else if (header.name.toLowerCase() === "proxy-authorization") {
        }
        else if (header.name.toLowerCase() === "range") {
        }
        else if (header.name.toLowerCase() === "referer") {
            header.value = getRefererHeader();
        }
        else if (header.name.toLowerCase() === "te") {
        }
        else if (header.name.toLowerCase() === "user-agent") {
            header.value = getUserAgentHeader(seed);
        }
    }

    return {requestHeaders: e.requestHeaders};
}

chrome.webRequest.onBeforeSendHeaders.addListener(rewriteHttpHeaders, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);

chrome.webNavigation.onBeforeNavigate.addListener(function(details) {
    // Create URL object from url string
    var serverUrl = new URL(details.url);

    // Get the origin (hostname)
    var origin = serverUrl.hostname;

    var seed = "Some dynamic value";

    console.log("Injecting Value");
    chrome.tabs.executeScript(details.tabId, {code: "var seed = '" + seed + "';console.log(seed);", runAt: "document_start"}, function() {
        console.log("Value Injected");
    });
});

注入.js

(function() {
  function inject(filePath) {
    var script = document.createElement('script');
    script.src = chrome.extension.getURL(filePath);
    script.onload = function() {
      this.remove();
    };
    (document.head || document.documentElement).appendChild(script);
  }

  function injectText(text) {
    var script = document.createElement('script');
    script.textContent = text;
    script.onload = function() {
      this.remove();
    };
    (document.head || document.documentElement).appendChild(script);
  }

  console.log("CONTENT SCRIPT RUNNING");

  console.log(seed); // SEED IS NOT DEFINED HERE ???

  injectText("var seed = 'hello';");
  console.log("[INFO] Injected Seed ...");
  inject("js/ua.js");
  console.log("[INFO] Injected UA ...");
  inject("js/words.js");
  console.log("[INFO] Injected Words ...");
  inject("js/lib/seedrandom.min.js");
  console.log("[INFO] Injected Seed Random ...");
  inject("js/random.js");
  console.log("[INFO] Injected Random ...");
  inject("js/api/document.js");
  console.log("[INFO] Injected Document API ...");
  inject("js/api/navigator.js");
  console.log("[INFO] Injected Navigator API ...");
  inject("js/api/canvas.js");
  console.log("[INFO] Injected Canvas API ...");
  inject("js/api/history.js");
  console.log("[INFO] Injected History API ...");
  inject("js/api/battery.js");
  console.log("[INFO] Injected Battery API ...");
  inject("js/api/audio.js");
  console.log("[INFO] Injected Audio API ...");
  inject("js/api/element.js");
  console.log("[INFO] Injected Element API ...");
})();

尝试将种子记录到控制台时出现错误:

inject.js:26 Uncaught ReferenceError: seed is not defined
    at inject.js:26
    at inject.js:52

有任何想法吗?

1个回答

这将是非常棘手的。

让我们看看你的要求。

Inject.js 需要访问这个变量并在页面上的任何脚本运行之前运行它的代码。

这不是您的代码当前的工作方式。inject.js的执行时间为document_end- 这发生整个 DOM 树被解析之后,这意味着在所有页面脚本都运行之后(除了异步部分和async脚本加载)。

Chrome 有一个解决方案 - 您可以将执行设置为document_start. 然后你的代码将真正在其他一切之前运行,而 DOM 仍然没有被解析(所以document基本上是空的)。使用您的代码所做的事情,它不应该产生问题(它只依赖于document.documentElement,它将存在)。

问题是,您的所有代码都必须同步才能仍然享受“在其他一切之前运行”的特性。只要您的代码的同步部分运行,Chrome 就会暂停 DOM 解析,但随后所有的赌注都将停止,因为它会继续愉快地解析(并从中运行代码)文档。

例如,这会取消资格chrome.storage和消息传递,因为对它的访问必然是异步的。

我需要注入一个动态变量 [在页面加载时]

这意味着您不能提前将其存储在某些同步可用的存储中(例如localStorage网站的 cookie 中),考虑到您事先不知道域,这无论如何都会有问题。

请注意,特别是对于您的代码,这可能不是那么重要;您的“动态”值实际上是每个域固定的。您仍然不知道将访问哪个域,但您至少可以保证在第二次加载时它会在那里。

使用我的后台脚本 background.js,我需要在注入另一个文件之前注入一个动态变量作为内容脚本[仍然需要在页面上的其他所有内容之前运行]

这就是棘手的部分。事实上,如前所述,这根本不可能。您试图从后台捕获导航提交之间的确切时刻,以便 Chrome 将页面切换到新域,以及执行您的document_start脚本。

那里没有可检测的间隙,也无法告诉 Chrome 等待。这是一个您无法解决的竞争条件。

您正在尝试使用webNavigation.onBeforeNavigate- 甚至在提交导航之前。因此,您injectScript甚至可能会转到上一页,使其变得无用。如果您尝试其他一些事件,例如 . onCommitted,目前还不清楚具体什么时候injectScript会得到治疗。可能你的脚本之后。

那么,如何解决所有这些问题?

幸运的是,有一些同步存储可用于内容脚本,您可以在最早的脚本执行之前将一些信息推送到正确的位置。

饼干。

但是,使用chrome.cookiesAPI 无济于事。您需要主动将 cookie 值注入到 上的请求中webRequest.onHeadersReceived

您必须同步准备好该值,以便使用阻塞处理程序 to 处理它onHeadersReceived,但随后您可以简单地添加一个Set-Cookie标头并立即document.cookies在您的inject.js.

  • 背景.js

    function addSeedCookie(details) {
      seed = SomethingSynchronous();
      details.responseHeaders.push({
        name: "Set-Cookie",
        value: `seed_goes_here=${seed};`
      });
      return {
        responseHeaders: details.responseHeaders
      };
    }
    
    chrome.webRequest.onHeadersReceived.addListener(
      addSeedCookie, {urls: ["<all_urls>"]}, [
        "blocking",
        "responseHeaders",
        // Chrome 72+ requires 'extraHeaders' to handle Set-Cookie header
        chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
      ].filter(Boolean)
    );
    
  • 注入.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 seed = getCookie("seed_goes_here");
    

如果需要异步函数来生成数据,请在 onBeforeRequest 事件中发送请求之前准备数据,然后在 onHeadersReceived 侦听器中使用它。

const preparedSeed = {};
chrome.webRequest.onBeforeRequest.addListener(
  details => {
    chrome.storage.local.get('seed', data => {
      preparedSeed[details.requestId] = data.seed;
    });
  }, {
    urls: ['<all_urls>'],
    types: ['main_frame', 'sub_frame'],
  });

注意:以上代码未经测试,仅用于说明该想法。

我确实喜欢这个概念,但是 a.) 我们最多只能通过 cookie 获得 4 KB 的数据,并且 b.) 除非非常仔细地构建,否则网站可以检测到这一点。也许这对 OP 来说可能就足够了,但在我的情况下,我可能有 MB。真的没有别的办法了吗?:-(
2021-03-15 03:51:10
看起来这是一个棘手的问题。我最终找到了完全不同的解决方案来解决我的问题。谢谢你的帮助。
2021-03-16 03:51:10
哇,你到底是怎么想出这个 Xan 的?天才。
2021-03-21 03:51:10