网站可以调用浏览器扩展吗?

IT技术 javascript google-chrome google-chrome-extension firefox-addon safari-extension
2021-01-16 14:22:34

我是浏览器扩展开发的新手,我了解浏览器扩展更改页面并向其中注入代码的概念。

有没有办法扭转这个方向?我编写了一个提供一组 API 的扩展,想要使用我的扩展的网站可以检测到它的存在,如果它存在,网站可以调用我的 API 方法,如var extension = Extenion(foo, bar). 这在 Chrome、Firefox 和 Safari 中可能吗?

例子:

  1. Google 创建了一个名为 BeautifierExtension 的新扩展。它有一组 API 作为 JS 对象。

  2. 用户访问 reddit.com。Reddit.com 检测 BeautifierExtension 并通过调用调用 APIbeautifer = Beautifier();

参见#2 - 通常它是检测匹配站点并更改页面的扩展程序。我有兴趣知道#2 是否可行。

1个回答

由于 Chrome 引入了externally_connectable,这在 Chrome 中很容易做到。首先,在您的manifest.json文件中指定允许的域

"externally_connectable": {
  "matches": ["*://*.example.com/*"]
}

用于chrome.runtime.sendMessage从页面发送消息:

chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    // ...
  });

最后,在您的背景页面中使用chrome.runtime.onMessageExternal以下内容收听

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    // verify `sender.url`, read `request` object, reply with `sednResponse(...)`...
  });

如果您无法获得externally_connectable支持,原始答案如下:

我将从以 Chrome 为中心的角度来回答,尽管这里描述的原则(网页脚本注入、长时间运行的后台脚本、消息传递)几乎适用于所有浏览器扩展框架。

从高层次来看,您想要做的是将内容脚本注入到每个网页中,这会添加一个可供网页访问的 API。当站点调用 API 时,API 会触发内容脚本执行某些操作,例如通过异步回调向后台页面发送消息和/或将结果发送回内容脚本。

这里的主要困难是“注入”到网页中的内容脚本不能直接改变页面的 JavaScript执行环境它们共享 DOM,因此在内容脚本和网页之间共享事件对 DOM 结构的更改,但不共享函数和变量。例子:

  • DOM 操作:如果内容脚本<div>向页面添加元素,这将按预期工作。内容脚本和页面都将看到新的<div>.

  • 事件:如果一个内容脚本设置了一个事件监听器,例如,点击一个元素,当事件发生时监听器将成功触发。如果页面为从内容脚本触发的自定义事件设置了侦听器,则当内容脚本触发这些事件时,它们将被成功接收。

  • 函数:如果内容脚本定义了一个新的全局函数foo()(就像您在设置新 API 时可能尝试的那样)。该页面无法查看或执行foo,因为foo仅存在于内容脚本的执行环境中,而不存在于页面环境中。

那么,如何设置合适的 API?答案有很多步骤:

  1. 在低级别,使您的 API事件基于. 网页使用 触发自定义 DOM 事件dispatchEvent,内容脚本使用 侦听它们addEventListener,并在收到它们时采取行动。这是一个简单的基于事件的存储 API,网页可以使用它来让扩展为其存储数据:

    content_script.js(在你的扩展中):

    // an object used to store things passed in from the API
    internalStorage = {};
    
    // listen for myStoreEvent fired from the page with key/value pair data
    document.addEventListener('myStoreEvent', function(event) {
        var dataFromPage = event.detail;
        internalStorage[dataFromPage.key] = dataFromPage.value
    });
    

    非扩展网页,使用基于事件的 API:

    function sendDataToExtension(key, value) {
        var dataObj = {"key":key, "value":value};
        var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
        document.dispatchEvent(storeEvent);
    }
    sendDataToExtension("hello", "world");
    

    如您所见,普通网页正在触发内容脚本可以看到并做出react的事件,因为它们共享 DOM。事件附加了数据,添加到了CustomEvent构造函数中我这里的示例非常简单——一旦获得了页面中的数据(很可能将其传递后台页面以供进一步处理),您显然可以在内容脚本中做更多的事情

  2. 然而,这只是成功的一半。在我上面的例子中,普通网页必须自己创建sendDataToExtension创建和触发自定义事件非常冗长(我的代码占用 3 行并且相对简短)。您不想仅仅为了使用您的 API 就强迫站点编写神秘的事件触发代码。解决方案有点麻烦:<script>在共享 DOM 中附加一个标签,将事件触发代码添加到主页的执行环境中。

    content_script.js 中:

    // inject a script from the extension's files
    // into the execution environment of the main page
    var s = document.createElement('script');
    s.src = chrome.extension.getURL("myapi.js");
    document.documentElement.appendChild(s);
    

    任何定义在 中的函数都myapi.js可以访问主页面。(如果您正在使用"manifest_version":2,则需要将 包含myapi.js在清单的 列表中web_accessible_resources)。

    myapi.js:

    function sendDataToExtension(key, value) {
        var dataObj = {"key":key, "value":value};
        var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
        document.dispatchEvent(storeEvent);
    }
    

    现在普通网页可以简单地做:

    sendDataToExtension("hello", "world");
    
  3. 我们的 API 流程还有一个问题myapi.js脚本在加载时将不可用。相反,它将在页面加载时间后加载一段时间。因此,纯网页需要知道何时可以安全地调用您的 API。您可以通过myapi.js触发“API 就绪”事件来解决此问题,您的页面会监听该事件。

    myapi.js:

    function sendDataToExtension(key, value) {
        // as above
    }
    
    // since this script is running, myapi.js has loaded, so let the page know
    var customAPILoaded = new CustomEvent('customAPILoaded');
    document.dispatchEvent(customAPILoaded);
    

    使用 API 的普通网页

    document.addEventListener('customAPILoaded', function() {
        sendDataToExtension("hello", "world");
        // all API interaction goes in here, now that the API is loaded...
    });
    
  4. 加载时脚本可用性问题的另一个解决方案是将run_at清单中的内容脚本的属性设置"document_start"如下所示:

    清单.json:

        "content_scripts": [
          {
            "matches": ["https://example.com/*"],
            "js": [
              "myapi.js"
            ],
            "run_at": "document_start"
          }
        ],
    

    摘自文档

    在“document_start”的情况下,文件在来自 css 的任何文件之后注入,但在构建任何其他 DOM 或运行任何其他脚本之前。

    对于某些可能比“API 加载”事件更合适且更省力的内容脚本。

  5. 为了将结果发送页面,您需要提供一个异步回调函数。无法从您的 API 同步返回结果,因为事件触发/侦听本质上是异步的(即,您的站点端 API 函数在内容脚本通过 API 请求获取事件之前终止)。

    myapi.js:

    function getDataFromExtension(key, callback) {
        var reqId = Math.random().toString(); // unique ID for this request
        var dataObj = {"key":key, "reqId":reqId};
        var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj});
        document.dispatchEvent(fetchEvent);
    
        // get ready for a reply from the content script
        document.addEventListener('fetchResponse', function respListener(event) {
            var data = event.detail;
    
            // check if this response is for this request
            if(data.reqId == reqId) {
                callback(data.value);
                document.removeEventListener('fetchResponse', respListener);
            }
        }
    }
    

    content_script.js(在你的扩展中):

    // listen for myFetchEvent fired from the page with key
    // then fire a fetchResponse event with the reply
    document.addEventListener('myStoreEvent', function(event) {
        var dataFromPage = event.detail;
        var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId};
        var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData});
        document.dispatchEvent(fetchResponse);
    });
    

    普通网页:

    document.addEventListener('customAPILoaded', function() {
        getDataFromExtension("hello", function(val) {
            alert("extension says " + val);
        });
    });
    

    reqId如果您一次有多个请求,是必要的,这样他们就不会读取错误的响应。

我认为这就是一切!因此,当您考虑到其他扩展也可以将侦听器绑定到您的事件以窃听页面如何使用您的 API 时,不适合胆小的人,而且可能不值得。我之所以知道这一切,是因为我为学校项目制作了一个概念验证密码学 API(随后了解了与之相关的主要安全陷阱)。

总而言之:内容脚本可以侦听来自普通网页的自定义事件,该脚本还可以注入带有函数的脚本文件,使网页更容易触发这些事件。内容脚本可以将消息传递到后台页面,然后后台页面存储、转换或传输来自消息的数据。

万一有人在 2020 年发现这一点:这种方法似乎不再适用(在 FF 中)。FF 不再允许将 JS 从扩展加载到页面中。我收到“安全错误:xxxxxxxxx.net 上的内容可能无法加载或链接到 moz-extension://46a55a72-0d87-4ca7-86e6-6d2bc890970e/myapi.js”。
2021-03-16 14:22:34
在注入脚本之前,有什么方法可以检查页面的安全详细信息/HSTS 状态吗?
2021-03-22 14:22:34
@RobW 我终于想到了。我从来没有CustomEvent亲自构造过,但我认为我在这里做得对。
2021-03-27 14:22:34
谢谢详细的解释!+1 用于指出执行环境。
2021-04-01 14:22:34
考虑使用CustomEvent构造函数更新您的答案它的语法看起来比不推荐使用的document.createEvent方法好得多
2021-04-09 14:22:34