HTML - Web Messaging

Types of Web Messaging

Channel Messaging

Channel Messaging lets two or more browsing contexts (windows, tabs, or iframes) talk to each other even if they are on different origins. It uses a message channel object to set up a link between the contexts and send messages between them.

Channel Messaging is useful when you need to set up a direct, two-way link between contexts. For example, you can use it to let a parent window control an iframe or let two tabs share data in real-time.

Channel Messaging Example

// In the first browsing context
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;

// Send port2 to the second browsing context
window.postMessage('init', '*', [port2]);

// Listen for messages on port1
port1.onmessage = (event) => {
  console.log('Received message:', event.data);
};

// In the second browsing context
window.onmessage = (event) => {
  if (event.data === 'init') {
    const port2 = event.ports[0];
    port2.onmessage = (event) => {
      console.log('Received message:', event.data);
    };
    port2.postMessage('Hello from the second context!');
  }
};

Cross-Document Messaging

Cross-Document Messaging, also known as postMessage, allows different browsing contexts (windows, tabs, or iframes) to talk even if they are on different origins. It uses the postMessage() method to send a message event to the receiving context, which can then handle it using an event listener.

Cross-Document Messaging is helpful when you need one-way messages between contexts. For instance, when a parent window needs an iframe to do something or one tab needs to send data to another tab.

Cross-Document Messaging Example

// In the sending context
const targetWindow = window.parent; // Or window.opener, or document.getElementById('myIframe').contentWindow
targetWindow.postMessage('Hello from the sender!', '*');

// In the receiving context
window.addEventListener('message', (event) => {
  if (event.origin === 'http://example.com') {
    console.log('Received message:', event.data);
  }
});

Web Workers

Web Workers run scripts in the background without affecting page performance. They allow heavy processing in a separate thread so that the main page stays responsive.

Web Workers are useful for tasks that take time like complex calculations and data processing.

Web Workers Example

// In the main script
const worker = new Worker('worker.js');
worker.onmessage = (event) => {
  console.log('Received message from worker:', event.data);
};
worker.postMessage('Hello from main script!');

// In worker.js
self.onmessage = (event) => {
  console.log('Received message in worker:', event.data);
  self.postMessage('Hello from worker!');
};

Security Considerations

When using Web Messaging, be aware of potential security risks and follow best practices for secure implementation.

One main risk with Web Messaging is cross-site scripting (XSS) attacks. If you don't validate and sanitize the data you receive through messages, an attacker could send harmful code that gets executed on your page. This could allow them to steal sensitive data, perform actions on behalf of the user, or take control of the page.

HTML code example for XSS risk

<p>This    is   a   paragraph   with    extra   spaces.</p>

When a browser renders this code, it will display the text as:

This is a paragraph with extra spaces.

To prevent XSS attacks, always validate and sanitize any data received through messages. Don't assume that the data is safe just because it comes from another window or worker that you control. Treat all received data as untrusted input and handle it accordingly.

Another risk is when using Web Messaging to communicate between different origins. Be careful about which origins you trust. If you accept messages from any origin without checking, an attacker could send messages from a harmful site that trick your page into doing something it shouldn't. To reduce this risk, always check the origin property of the message event and only accept messages from trusted origins.

When using Channel Messaging, know that any context receiving a message port has full access to that channel. This means they can send messages on the channel and even close it. So only share message ports with contexts that you fully trust.

With Web Workers, a main risk is that a worker could hang or use too many resources if it gets stuck in an infinite loop or tries to do too much work. To prevent this, use the Worker.terminate() method to stop a worker if needed immediately. Structure your worker code to respond to messages quickly and avoid long-running tasks.

Example: Using Worker.terminate() method

var myWorker = new Worker('worker.js');

// Terminate the worker after 5 seconds
setTimeout(function() {
    myWorker.terminate();
}, 5000);

This code stops the worker after a specified time to prevent resource overuse.

Here are some best practices for secure Web Messaging:

  • Always validate and sanitize received message data
  • Only accept messages from trusted origins
  • Be careful when sharing message ports
  • Use Worker.terminate() to stop misbehaving workers
  • Keep your worker code focused and responsive
  • Don't use Web Messaging to share sensitive data unless necessary
  • Use encryption for sensitive data if needed
  • Test your Web Messaging code thoroughly for potential vulnerabilities

Browser Support

Most modern web browsers support Web Messaging. This includes Chrome, Firefox, Safari, and Internet Explorer 11 and above. The WebSocket API and Server-Sent Events, used for real-time messaging, are also supported in most browsers.

Older browsers may have limited or no support for some Web Messaging features. For example:

  • Internet Explorer 8 and below do not support the window.postMessage() method used in Cross-Document Messaging.
  • Internet Explorer 10 and below do not support Web Workers.
  • Internet Explorer does not support Channel Messaging.

If you need to support older browsers, you may need to use workarounds or fallbacks. Some options include:

  • For Cross-Document Messaging in old IE versions, you can use a library like easyXDM which provides a fallback transport using iframes and the location.hash property.

Example using easyXDM library

// In the sending context
var socket = new easyXDM.Socket({
  remote: "http://example.com/remote.html",
  onMessage: function(message, origin) {
    console.log("Received message:", message);
  }
});
socket.postMessage("Hello from the sender!");

// In the receiving context (remote.html)
var socket = new easyXDM.Socket({
  onMessage: function(message, origin) {
    console.log("Received message:", message);
  }
});
  • For Web Workers in IE10 and below, you can detect support and provide a fallback that runs the script in the main thread instead.

Example of Web Workers fallback

if (typeof(Worker) !== "undefined") {
  // Web Workers supported, use worker
  var worker = new Worker("worker.js");
} else {
  // Web Workers not supported, run script in main thread
  var workerFallbackScript = document.createElement('script');
  workerFallbackScript.src = 'worker.js';
  document.body.appendChild(workerFallbackScript);
}

For Channel Messaging alternatives: You can use other messaging techniques like Cross-Document Messaging or a server-based solution when Channel Messaging isn't available.

It's important to test your Web Messaging code in all the browsers you plan to support. Use feature detection to check for support and provide appropriate fallbacks where needed to give all your users a good experience.

The browser landscape is always improving. Most users are now on modern browsers with many features. So while fallbacks for old browsers may still be necessary sometimes, they are becoming less critical over time as legacy browser usage continues to decline.

Practical Examples

Real-time Chat Application

Building a real-time chat application shows the power of Web Messaging. Here's a guide to creating a basic chat app using Channel Messaging and Web Workers:

  1. Set up the HTML structure for the chat interface, including an input field for messages and a container for displaying the chat history.

  2. Create a new Web Worker to handle message processing and storage. This worker will receive messages from the main script, store them, and send back updated chat history when requested.

Example: Web Worker for Chat (chat_worker.js)

let chatHistory = [];

self.onmessage = (event) => {
  if (event.data.type === 'newMessage') {
    chatHistory.push(event.data.message);
    self.postMessage({ type: 'updateHistory', history: chatHistory });
  } else if (event.data.type === 'getHistory') {
    self.postMessage({ type: 'updateHistory', history: chatHistory });
  }
};
  1. In the main script, create a new Channel Messaging channel and send one of the ports to the worker.

Example: Main Script (script.js)

const chatWorker = new Worker('chat_worker.js');
const channel = new MessageChannel();
chatWorker.postMessage({ type: 'init', port: channel.port1 }, [channel.port1]);
  1. Set up event listeners for message input and channel communication. When a new message is entered, send it to the worker via the channel. Listen for history updates from the worker and display them in the chat interface.

Example: Event Listeners and Messaging

const messageInput = document.getElementById('messageInput');
const chatContainer = document.getElementById('chatContainer');

channel.port2.onmessage = (event) => {
  if (event.data.type === 'updateHistory') {
    const messagesHTML = event.data.history.map(message => `<p>${message}</p>`).join('');
    chatContainer.innerHTML = messagesHTML;
  }
};

messageInput.addEventListener('keypress', (event) => {
  if (event.key === 'Enter') {
    const message = messageInput.value;
    channel.port2.postMessage({ type: 'newMessage', message });
    messageInput.value = '';
  }
});
  1. When page loads, request initial chat history from worker.

Example: Request Initial Chat History

chatWorker.postMessage({ type: 'getHistory' });

With these steps in place, you have a basic real-time chat application that uses Web Messaging to communicate between main script and Web Worker.

Cross-Domain Communication

Web Messaging can also enable communication between different domains using Cross-Document Messaging:

  1. In parent window, create an iframe with src attribute set to URL of page you want to communicate with.

Example: Parent Window (parent.html)

<iframe id="myIframe" src="https://example.com/iframe.html"></iframe>
  1. In iframe's script, listen for message events and handle them based on expected data format.

Example: Iframe Script (iframe.js)

window.addEventListener('message', function(event) {
  if (event.origin === 'https://example.com' && event.data.type === 'userData') {
      console.log('Received user data:', event.data.value);
   }
});
  1. In parent window's script, get reference to iframe window and send messages using postMessage() method.

Example: Parent Window Script (parent.js)

const iframeWindow= document.getElementById('myIframe').contentWindow;
const userData= { name:'John Doe', age :30 };

document.getElementById("myIframe").addEventListener("load", () =>{
   iframeWindow.postMessage({type :'userData ', value :userData },'https://example.com ');
});

With this setup, the parent window can send messages containing user data to iframe, and iframe can listen for these messages & process accordingly. This allows secure communication between two contexts even though they are hosted on different domains.

When using Cross-Document messaging, validate origin of received messages ensuring they come from trusted sources preventing cross-site scripting (XSS) attacks & other security vulnerabilities.