Transmisión de mensajes

Debido a que las secuencias de comandos de contenido se ejecutan en el contexto de una página web y no en la extensión que las ejecuta, a menudo necesitan formas de comunicarse con el resto de la extensión. Por ejemplo, una extensión de lector de RSS podría usar secuencias de comandos de contenido para detectar la presencia de un feed RSS en una página y, luego, notificar al trabajador del servicio para que muestre un ícono de acción para esa página.

Esta comunicación usa el pase de mensajes, que permite que las extensiones y las secuencias de comandos de contenido escuchen los mensajes de los demás y respondan en el mismo canal. Un mensaje puede contener cualquier objeto JSON válido (nulo, booleano, número, cadena, array o objeto). Existen dos APIs de transmisión de mensajes: una para solicitudes únicas y una más compleja para conexiones de larga duración que permiten el envío de varios mensajes. Si deseas obtener información para enviar mensajes entre extensiones, consulta la sección Mensajes entre extensiones.

Solicitudes únicas

Para enviar un solo mensaje a otra parte de tu extensión y, de manera opcional, obtener una respuesta, llama a runtime.sendMessage() o tabs.sendMessage(). Estos métodos te permiten enviar un mensaje serializable JSON único desde una secuencia de comandos de contenido a la extensión o desde la extensión a una secuencia de comandos de contenido. Para controlar la respuesta, usa la promesa que se muestra. Para lograr la retrocompatibilidad con extensiones anteriores, puedes pasar una devolución de llamada como el último argumento. No puedes usar una promesa y una devolución de llamada en la misma llamada.

Para obtener información sobre cómo convertir devoluciones de llamada en promesas y usarlas en extensiones, consulta la guía de migración de Manifest V3.

El envío de una solicitud desde una secuencia de comandos de contenido se ve de la siguiente manera:

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Si deseas responder de forma síncrona a un mensaje, llama a sendResponse una vez que tengas la respuesta y muestra false para indicar que ya está lista. Para responder de forma asíncrona, muestra true para mantener activa la devolución de llamada sendResponse hasta que esté todo listo para usarla. Las funciones asíncronas no son compatibles porque devuelven una promesa, que no es compatible.

Para enviar una solicitud a una secuencia de comandos de contenido, especifica a qué pestaña se aplica la solicitud, como se muestra a continuación. Este ejemplo funciona en service workers, ventanas emergentes y páginas chrome-extension:// abiertas como pestaña.

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Para recibir el mensaje, configura un objeto de escucha de eventos runtime.onMessage. Estos usan el mismo código en las extensiones y las secuencias de comandos del contenido:

content-script.js o service-worker.js:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      sendResponse({farewell: "goodbye"});
  }
);

En el ejemplo anterior, se llamó a sendResponse() de forma síncrona. Para usar sendResponse() de forma asíncrona, agrega return true; al controlador de eventos onMessage.

Si varias páginas detectan eventos onMessage, solo la primera que llame a sendResponse() para un evento en particular tendrá éxito y enviará la respuesta. Se ignorarán todas las demás respuestas a ese evento.

Conexiones duraderas

Para crear un canal de transferencia de mensajes reutilizable y duradero, llama a runtime.connect() para pasar mensajes de una secuencia de comandos de contenido a una página de extensión, o bien a tabs.connect() para pasar mensajes de una página de extensión a una secuencia de comandos de contenido. Puedes asignarle un nombre a tu canal para distinguir entre los diferentes tipos de conexiones.

Un posible caso de uso para una conexión de larga duración es una extensión automática para completar formularios. La secuencia de comandos de contenido puede abrir un canal a la página de la extensión para un acceso específico y enviar un mensaje a la extensión para cada elemento de entrada de la página para solicitar los datos del formulario que se deben completar. La conexión compartida permite que la extensión comparta el estado entre los componentes de la extensión.

Cuando se establece una conexión, a cada extremo se le asigna un objeto runtime.Port para enviar y recibir mensajes a través de esa conexión.

Usa el siguiente código para abrir un canal desde una secuencia de comandos de contenido y enviar y escuchar mensajes:

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

Para enviar una solicitud de la extensión a una secuencia de comandos de contenido, reemplaza la llamada a runtime.connect() en el ejemplo anterior por tabs.connect().

Para controlar las conexiones entrantes de una secuencia de comandos de contenido o una página de extensión, configura un objeto de escucha de eventos runtime.onConnect. Cuando otra parte de tu extensión llama a connect(), se activa este evento y el objeto runtime.Port. El código para responder a las conexiones entrantes se ve de la siguiente manera:

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

Vida útil del puerto

Los puertos se diseñaron como un método de comunicación bidireccional entre diferentes partes de la extensión. Una trama de nivel superior es la parte más pequeña de una extensión que puede usar un puerto. Cuando una parte de una extensión llama a tabs.connect(), runtime.connect() o runtime.connectNative(), se crea un puerto que puede enviar mensajes de inmediato con postMessage().

Si hay varios marcos en una pestaña, llamar a tabs.connect() invoca el evento runtime.onConnect una vez por cada marco de la pestaña. De manera similar, si se llama a runtime.connect(), el evento onConnect puede activarse una vez por cada fotograma en el proceso de extensión.

Es posible que desees saber cuándo se cierra una conexión, por ejemplo, si mantienes estados separados para cada puerto abierto. Para ello, escucha el evento runtime.Port.onDisconnect. Este evento se activa cuando no hay puertos válidos en el otro extremo del canal, lo que puede deberse a alguna de las siguientes causas:

  • No hay objetos de escucha para runtime.onConnect en el otro extremo.
  • Se descarga la pestaña que contiene el puerto (por ejemplo, si se navega por la pestaña).
  • Se descargó el marco en el que se llamó a connect().
  • Se descargaron todas las tramas que recibieron el puerto (a través de runtime.onConnect).
  • El otro extremo llama a runtime.Port.disconnect(). Si una llamada a connect() genera varios puertos en el extremo del receptor y se llama a disconnect() en cualquiera de estos puertos, el evento onDisconnect solo se activa en el puerto de envío, no en los otros puertos.

Mensajería entre extensiones

Además de enviar mensajes entre diferentes componentes de tu extensión, puedes usar la API de mensajes para comunicarte con otras extensiones. Esto te permite exponer una API pública para que la usen otras extensiones.

Para detectar solicitudes y conexiones entrantes de otras extensiones, usa los métodos runtime.onMessageExternal o runtime.onConnectExternal. Este es un ejemplo de cada uno:

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

Para enviar un mensaje a otra extensión, pasa el ID de la extensión con la que deseas comunicarte de la siguiente manera:

service-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

Envía mensajes desde páginas web

Las extensiones también pueden recibir y responder mensajes de otras páginas web, pero no pueden enviar mensajes a páginas web. Para enviar mensajes de una página web a una extensión, especifica en tu manifest.json con qué sitios web deseas comunicarte con la clave de manifiesto "externally_connectable". Por ejemplo:

manifest.json

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

Esto expone la API de mensajería a cualquier página que coincida con los patrones de URL que especifiques. El patrón de URL debe contener al menos un dominio de segundo nivel; es decir, no se admiten patrones de nombres de host como "*", "*.com", "*.co.uk" y "*.appspot.com". A partir de Chrome 107, puedes usar <all_urls> para acceder a todos los dominios. Ten en cuenta que, debido a que afecta a todos los hosts, las revisiones de Chrome Web Store de las extensiones que lo usan pueden tardar más.

Usa las APIs de runtime.sendMessage() o runtime.connect() para enviar un mensaje a una app o extensión específica. Por ejemplo:

webpage.js

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

Desde tu extensión, escucha los mensajes de las páginas web con las APIs de runtime.onMessageExternal o runtime.onConnectExternal, como en los mensajes entre extensiones. Por ejemplo:

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

Mensajería nativa

Las extensiones pueden intercambiar mensajes con aplicaciones nativas que están registradas como un host de mensajería nativa. Para obtener más información sobre esta función, consulta Mensajes nativos.

Consideraciones de seguridad

Estas son algunas consideraciones de seguridad relacionadas con los mensajes.

Las secuencias de comandos de contenido son menos confiables.

Las secuencias de comandos de contenido son menos confiables que el service worker de extensiones. Por ejemplo, una página web maliciosa podría comprometer el proceso de renderización que ejecuta las secuencias de comandos de contenido. Supongamos que es posible que un atacante haya creado los mensajes de una secuencia de comandos de contenido y asegúrate de validar y limpiar todas las entradas. Supongamos que los datos que se envían a la secuencia de comandos de contenido podrían filtrarse a la página web. Limita el alcance de las acciones privilegiadas que pueden activar los mensajes recibidos de las secuencias de comandos de contenido.

Secuencia de comandos entre sitios

Asegúrate de proteger tus secuencias de comandos de las secuencias de comandos entre sitios. Cuando recibas datos de una fuente no confiable, como la entrada del usuario, otros sitios web a través de una secuencia de comandos de contenido o una API, ten cuidado de no interpretarlos como HTML ni usarlos de una manera que pueda permitir que se ejecute código inesperado.

Métodos más seguros

Usa APIs que no ejecuten secuencias de comandos siempre que sea posible:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
Métodos no seguros

Evita usar los siguientes métodos que hagan que tu extensión sea vulnerable:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});