Transmissão de mensagens

Como os scripts de conteúdo são executados no contexto de uma página da Web, e não da extensão que os executa, muitas vezes eles precisam de maneiras de se comunicar com o restante da extensão. Por exemplo, uma extensão de leitor de RSS pode usar scripts de conteúdo para detectar a presença de um feed RSS em uma página e, em seguida, notificar o service worker para exibir um ícone de ação para essa página.

Essa comunicação usa a transmissão de mensagens, que permite que extensões e scripts de conteúdo escutem as mensagens umas das outras e respondam no mesmo canal. Uma mensagem pode conter qualquer objeto JSON válido (nulo, booleano, número, string, matriz ou objeto). Há duas APIs de transmissão de mensagens: uma para solicitações únicas e outra mais complexa para conexões de longa duração que permitem o envio de várias mensagens. Para informações sobre o envio de mensagens entre extensões, consulte a seção Mensagens entre extensões.

Solicitações únicas

Para enviar uma única mensagem para outra parte da extensão e, opcionalmente, receber uma resposta, chame runtime.sendMessage() ou tabs.sendMessage(). Esses métodos permitem enviar uma mensagem serializável JSON única de um script de conteúdo para a extensão ou da extensão para um script de conteúdo. Para processar a resposta, use a promessa retornada. Para compatibilidade com extensões mais antigas, transmita um callback como o último argumento. Não é possível usar uma promessa e um callback na mesma chamada.

Quando você envia uma mensagem, o listener de eventos que processa a mensagem recebe um terceiro argumento opcional, sendResponse. Essa é uma função que recebe um objeto serializável em JSON que é usado como o valor de retorno para a função que enviou a mensagem. Por padrão, o callback sendResponse precisa ser chamado de forma síncrona. Se você quiser fazer um trabalho assíncrono para receber o valor transmitido para sendResponse, é necessário retornar um true literal (não apenas um valor verdadeiro) do listener de eventos. Isso vai manter o canal de mensagens aberto para a outra extremidade até que sendResponse seja chamado.

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

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

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

Para informações sobre como converter callbacks em promessas e usá-los em extensões, consulte o guia de migração do Manifest V3.

O envio de uma solicitação de um script de conteúdo é assim:

content-script.js:

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

Se você quiser responder a uma mensagem de forma síncrona, chame sendResponse quando tiver a resposta e retorne false para indicar que ela foi concluída. Para responder de forma assíncrona, retorne true para manter o callback sendResponse ativo até que você esteja pronto para usá-lo. Não há suporte para funções assíncronas porque elas retornam uma promessa, que não é aceita.

Para enviar uma solicitação a um script de conteúdo, especifique a guia a que a solicitação se aplica, conforme mostrado abaixo. Este exemplo funciona em service workers, pop-ups e páginas chrome-extension:// abertas como uma guia.

(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 receber a mensagem, configure um listener de evento runtime.onMessage. Eles usam o mesmo código em extensões e scripts de conteúdo:

content-script.js ou 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"});
  }
);

No exemplo anterior, sendResponse() foi chamado de forma síncrona. Para usar sendResponse() de forma assíncrona, adicione return true; ao gerenciador de eventos onMessage.

Se várias páginas estiverem aguardando eventos onMessage, apenas a primeira a chamar sendResponse() para um evento específico vai conseguir enviar a resposta. Todas as outras respostas a esse evento serão ignoradas.

Conexões de longa duração

Para criar um canal de transmissão de mensagens reutilizável de longa duração, chame runtime.connect() para transmitir mensagens de um script de conteúdo para uma página de extensão ou tabs.connect() para transmitir mensagens de uma página de extensão para um script de conteúdo. Você pode nomear seu canal para distinguir diferentes tipos de conexões.

Um possível caso de uso para uma conexão de longa duração é uma extensão automática de preenchimento de formulários. O script de conteúdo pode abrir um canal para a página da extensão para um login específico e enviar uma mensagem para a extensão para cada elemento de entrada na página para solicitar os dados do formulário a serem preenchidos. A conexão compartilhada permite que a extensão compartilhe o estado entre componentes da extensão.

Ao estabelecer uma conexão, cada extremidade recebe um objeto runtime.Port para enviar e receber mensagens por essa conexão.

Use o código abaixo para abrir um canal em um script de conteúdo e enviar e detectar mensagens:

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 uma solicitação da extensão para um script de conteúdo, substitua a chamada para runtime.connect() no exemplo anterior por tabs.connect().

Para processar conexões recebidas de um script de conteúdo ou de uma página de extensão, configure um listener de evento runtime.onConnect. Quando outra parte da extensão chama connect(), ela ativa esse evento e o objeto runtime.Port. O código para responder a conexões recebidas tem esta aparência:

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

Duração da porta

As portas são projetadas como um método de comunicação bidirecional entre diferentes partes da extensão. Um frame de nível superior é a menor parte de uma extensão que pode usar uma porta. Quando parte de uma extensão chama tabs.connect(), runtime.connect() ou runtime.connectNative(), ela cria uma porta que pode enviar mensagens imediatamente usando postMessage().

Se houver vários frames em uma guia, chamar tabs.connect() vai invocar o evento runtime.onConnect uma vez para cada frame na guia. Da mesma forma, se runtime.connect() for chamado, o evento onConnect poderá ser acionado uma vez para cada frame no processo de extensão.

Talvez você queira descobrir quando uma conexão está fechada, por exemplo, se você está mantendo estados separados para cada porta aberta. Para fazer isso, detecte o evento runtime.Port.onDisconnect. Esse evento é acionado quando não há portas válidas na outra extremidade do canal, o que pode ter uma das seguintes causas:

  • Não há listeners para runtime.onConnect na outra extremidade.
  • A guia que contém a porta é descarregada (por exemplo, se a guia for navegada).
  • O frame em que connect() foi chamado foi descarregado.
  • Todos os frames que receberam a porta (via runtime.onConnect) foram descarregados.
  • runtime.Port.disconnect() é chamado por a outra extremidade. Se uma chamada connect() resultar em várias portas no lado do receptor e o disconnect() for chamado em qualquer uma dessas portas, o evento onDisconnect só será acionado na porta de envio, não nas outras.

Mensagens entre extensões

Além de enviar mensagens entre diferentes componentes na sua extensão, você pode usar a API de mensagens para se comunicar com outras extensões. Isso permite que você exponha uma API pública para outras extensões.

Para detectar solicitações e conexões recebidas de outras extensões, use os métodos runtime.onMessageExternal ou runtime.onConnectExternal. Confira um exemplo de cada um:

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 uma mensagem a outra extensão, transmita o ID da extensão com a qual você quer se comunicar da seguinte maneira:

service-worker.js

// The ID of the extension we want to talk to.
var 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:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

Enviar mensagens de páginas da Web

As extensões também podem receber e responder a mensagens de outras páginas da Web, mas não podem enviar mensagens para páginas da Web. Para enviar mensagens de uma página da Web para uma extensão, especifique em manifest.json quais sites você quer usar para se comunicar usando a chave de manifesto "externally_connectable". Exemplo:

manifest.json

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

Isso expõe a API de mensagens a qualquer página que corresponda aos padrões de URL especificados. O padrão do URL precisa conter pelo menos um domínio de segundo nível. Isso significa que padrões de nome de host como "*", "*.com", "*.co.uk" e "*.appspot.com" não são aceitos. A partir do Chrome 107, é possível usar <all_urls> para acessar todos os domínios. Como isso afeta todos os hosts, as análises da Chrome Web Store para extensões que usam o recurso podem levar mais tempo.

Use as APIs runtime.sendMessage() ou runtime.connect() para enviar uma mensagem a um app ou extensão específico. Exemplo:

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

Na sua extensão, ouça mensagens de páginas da Web usando as APIs runtime.onMessageExternal ou runtime.onConnectExternal, como em mensagens entre extensões. Veja um exemplo:

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

Envio de mensagens nativo

As extensões podem trocar mensagens com aplicativos nativos registrados como um host de mensagens nativo. Para saber mais sobre esse recurso, consulte Mensagens nativas.

Considerações sobre segurança

Confira algumas considerações de segurança relacionadas às mensagens.

Os scripts de conteúdo são menos confiáveis

Os scripts de conteúdo são menos confiáveis do que o service worker da extensão. Por exemplo, uma página da Web maliciosa pode comprometer o processo de renderização que executa os scripts de conteúdo. Suponha que as mensagens de um script de conteúdo possam ter sido criadas por um invasor e valide e limpe todas as entradas. Suponha que qualquer dado enviado para o script de conteúdo possa vazar para a página da Web. Limite o escopo das ações privilegiadas que podem ser acionadas por mensagens recebidas de scripts de conteúdo.

Scripting em vários locais

Proteja seus scripts contra cross-site scripting. Ao receber dados de uma fonte não confiável, como a entrada do usuário, outros sites por meio de um script de conteúdo ou uma API, evite interpretar isso como HTML ou usá-lo de uma maneira que possa permitir a execução de códigos inesperados.

Métodos mais seguros

Use APIs que não executam scripts sempre que possível:

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 não seguros

Evite usar os seguintes métodos que tornam sua extensão vulnerável:

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