Transmisión de mensajes

Las APIs de mensajería te permiten comunicarte entre diferentes secuencias de comandos que se ejecutan en contextos asociados con tu extensión. Esto incluye la comunicación entre tu service worker, las páginas chrome-extension://y los secuencias de comandos de contenido. 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 de servicio para que actualice el ícono de acción de esa página.

Existen dos APIs de transmisión de mensajes: una para solicitudes únicas y otra más compleja para conexiones de larga duración que permiten enviar varios mensajes.

Para obtener información sobre el envío de 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 en 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. Ambas APIs devuelven una promesa que se resuelve en la respuesta proporcionada por un destinatario.

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

Respuestas

Para escuchar un mensaje, usa el evento chrome.runtime.onMessage:

// Event listener
function handleMessages(message, sender, sendResponse) {
  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must return an explicit `true`
  return true;
}

chrome.runtime.onMessage.addListener(handleMessages);

// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage({
  url: 'https://example.com'
});

Cuando se llama al objeto de escucha de eventos, se pasa una función sendResponse como tercer parámetro. Esta es una función a la que se puede llamar para proporcionar una respuesta. De forma predeterminada, se debe llamar a la devolución de llamada sendResponse de forma síncrona. Si deseas realizar un trabajo asíncrono para obtener el valor que se pasa a sendResponse, debes devolver un literal true (no solo un valor verdadero) desde el objeto de escucha de eventos. Si lo haces, el canal de mensajes permanecerá abierto en el otro extremo hasta que se llame a sendResponse.

Si llamas a sendResponse sin ningún parámetro, se envía null como respuesta.

Si varias páginas están a la escucha de eventos onMessage, solo la primera que llame a sendResponse() para un evento en particular logrará 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 de larga duración, llama a lo siguiente:

  • runtime.connect() para pasar mensajes de una secuencia de comandos de contenido a una página de extensión
  • tabs.connect() para pasar mensajes de una página de extensión a una secuencia de comandos de contenido.

Puedes nombrar tu canal pasando un parámetro de opciones con una clave name para distinguir entre diferentes tipos de conexiones:

const port = chrome.runtime.connect({name: "example"});

Un posible caso de uso para una conexión de larga duración es una extensión de autocompletado de 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 y 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:

const port = chrome.runtime.connect({name: "knockknock"});
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"});
  }
});
port.postMessage({joke: "Knock knock"});

Para enviar una solicitud desde la extensión a una secuencia de comandos de contenido, reemplaza la llamada a runtime.connect() del 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 así:

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  if (port.name !== "knockknock") {
    return;
  }
  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."});
    }
  });
});

Serialización

En Chrome, las APIs de transmisión de mensajes usan la serialización JSON. Esto significa que un mensaje (y las respuestas proporcionadas por los destinatarios) puede contener cualquier valor JSON válido (nulo, booleano, número, cadena, array u objeto). Los demás valores se forzarán a valores serializables.

En particular, esto es diferente de otros navegadores que implementan las mismas APIs con el algoritmo de clonación estructurada.

Duración del puerto

Los puertos están diseñados como un mecanismo de comunicación bidireccional entre diferentes partes de una extensión. Cuando parte de una extensión llama a tabs.connect(), runtime.connect() o runtime.connectNative(), se crea un Port 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 para cada marco de la pestaña. Del mismo modo, si se llama a runtime.connect(), el evento onConnect se puede activar 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 cualquiera de los siguientes motivos:

  • No hay ningún objeto de escucha para runtime.onConnect en el otro extremo.
  • Se descarga la pestaña que contiene el puerto (por ejemplo, si se navegó por la pestaña).
  • Se descargó el marco en el que se llamó a connect().
  • Se descargaron todos los marcos que recibieron el puerto (a través de runtime.onConnect).
  • runtime.Port.disconnect() es llamado por el otro extremo. 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 detectar solicitudes y conexiones entrantes de otras extensiones, usa los métodos runtime.onMessageExternal o runtime.onConnectExternal. Aquí tienes un ejemplo de cada uno:

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id !== allowlistedExtension) {
      return; // don't allow this extension access
    }
    if (request.getTargetData) {
      sendResponse({ targetData: targetData });
    } else if (request.activateLasers) {
      const 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.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

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

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

Envía mensajes desde páginas web

Las extensiones también pueden recibir y responder mensajes de páginas web. Para enviar mensajes desde una página web a una extensión, especifica en tu manifest.json los sitios web desde los que deseas permitir mensajes 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". Puedes usar <all_urls> para acceder a todos los dominios.

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

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (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 la mensajería 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);
  });

No es posible enviar un mensaje desde una extensión a una página web.

Mensajería nativa

Las extensiones pueden intercambiar mensajes con aplicaciones nativas registradas como host 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 de contenido son menos confiables

Las secuencias de comandos de contenido son menos confiables que el service worker de la 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. Supón que un atacante podría haber creado mensajes desde una secuencia de comandos de contenido y asegúrate de validar y limpiar todas las entradas. Supón que cualquier dato que se envíe a la secuencia de comandos de contenido podría filtrarse a la página web. Limita el alcance de las acciones privilegiadas que pueden activarse con los mensajes recibidos de las secuencias de comandos de contenido.

Secuencia de comandos entre sitios

Asegúrate de proteger tus secuencias de comandos contra el scripting entre sitios. Cuando recibas datos de una fuente no confiable, como la entrada del usuario, otros sitios web a través de un script de contenido o una API, ten cuidado de no interpretarlos como HTML ni usarlos de una manera que permita 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.
  const 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 hacen 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!
  const 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;
});