Transmisión de mensajes

Debido a que las secuencias de comandos del contenido se ejecutan en el contexto de una página web, 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 puede usar secuencias de comandos de contenido para detectar la presencia de un feed RSS en una página y, luego, notificar al service worker que muestre un ícono de acción para esa página.

Esta comunicación usa el envío de mensajes, que permite que las extensiones y las secuencias de comandos de contenido escuchen los mensajes de la otra parte y respondan en el mismo canal. Un mensaje puede contener cualquier objeto JSON válido (nulo, booleano, número, string, objeto o array). Existen dos APIs de transferencia de mensajes: una para las solicitudes únicas y una más compleja para las 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 de mensajes de extensión cruzada.

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 a tabs.sendMessage(). Estos métodos te permiten enviar un mensaje único que se puede serializar con JSON 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 ofrecer retrocompatibilidad con extensiones más antiguas, puedes pasar una devolución de llamada como ú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 para 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 tiene el siguiente aspecto:

content-script.js:

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

Para enviar una solicitud a una secuencia de comandos de contenido, especifica a qué pestaña se aplica, como se muestra a continuación. Este ejemplo funciona en service worker, 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. Usan el mismo código en las extensiones y en las secuencias de comandos de 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 están a la escucha de eventos onMessage, solo la primera que llame a sendResponse() para un evento en particular podrá enviar la respuesta de manera correcta. Se ignorarán todas las demás respuestas a ese evento.

Conexiones de larga duración

Si quieres crear un canal de transmisión de mensajes de larga duración reutilizable, llama a runtime.connect() para pasar mensajes de una secuencia de comandos de contenido a una página de extensión, o a tabs.connect() para pasar mensajes de una página de extensión a una secuencia de comandos de contenido. Puedes ponerle un nombre a tu canal para distinguir entre los distintos 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 por cada elemento de entrada en 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(), 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."});
  });
});

Duración del puerto

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

Si hay varios marcos en una pestaña, la llamada a tabs.connect() invoca el evento runtime.onConnect una vez para cada fotograma de la pestaña. Del mismo modo, si se llama a runtime.connect(), se puede activar el evento onConnect una vez por cada fotograma del 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 del otro extremo.
  • La pestaña que contiene el puerto está descargada (por ejemplo, si se navega por la pestaña).
  • Se descargó el fotograma al 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.

Mensajes entre extensiones

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

Para escuchar las solicitudes y conexiones entrantes de otras extensiones, usa los métodos runtime.onMessageExternal o runtime.onConnectExternal. A continuación, se incluye un ejemplo de cada una:

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 ellas. Para enviar mensajes de una página web a una extensión, especifica en tu manifest.json con qué sitios web deseas comunicarte mediante la clave de manifiesto "externally_connectable". Por ejemplo:

manifest.json

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

Esto expone la API de mensajería en 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, los patrones de nombre de host, como "*", "*.com", "*.co.uk" y "*.appspot.com" no son compatibles. A partir de Chrome 107, puedes usar <all_urls> para acceder a todos los dominios. Ten en cuenta que, como afecta a todos los hosts, las revisiones de Chrome Web Store para 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 páginas web con las APIs de runtime.onMessageExternal o runtime.onConnectExternal, como en los mensajes de extensión cruzada. 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 hosts de mensajería nativa. Para obtener más información sobre esta función, consulta Mensajería nativa.

Consideraciones de seguridad

Estas son algunas consideraciones de seguridad relacionadas con la mensajería.

Las secuencias de comandos del contenido son menos confiables

Las secuencias de comandos del contenido son menos confiables que el service worker de extensión. 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 un atacante podría haber creado los mensajes de una secuencia de comandos de contenido y asegúrate de validar y limpiar todos los datos de entrada. Supongamos que los datos enviados a la secuencia de comandos de contenido pueden filtrarse a la página web. Limita el alcance de las acciones con privilegios que pueden activarse mediante mensajes recibidos de secuencias de comandos de contenido.

Secuencia de comandos entre sitios

Asegúrate de proteger tus secuencias de comandos de la secuencia de comandos entre sitios. Cuando recibas datos de una fuente no confiable, como entradas de usuarios, otros sitios web mediante una secuencia de comandos de contenido o una API, evita interpretarlos como HTML o usarlos de una forma que pueda permitir la ejecución de código inesperado.

Métodos más seguros

Siempre que sea posible, usa APIs que no ejecuten secuencias de comandos:

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;
});