Display Cross-domain Data Using postMessage()

By Rob Gravelle

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

Perhaps there is no greater bane to a web developer's existence than the same-origin policy. In the Fetch Cross-domain Content Using a PHP Proxy article, I presented one way to serve web content from another domain. Now I'd like to share a relatively new technique that utilizes the JavaScript postMessage() method.

Why Browsers Limit Data Sharing Between Web Pages

The same-origin policy is part of the web application security model. It basically permits scripts running on pages originating from the same site to access each other's data, but prevents scripts from accessing data that is served from a different domain. The idea is to prevent a malicious script on one page from obtaining access to sensitive data on another through that page's Document Object Model (DOM).

To illustrate, let's say that site one has a JS script in it. It has pretty much free reign over its own page content as well as the ability to fetch more content from its own server. However, should it try to fetch a resource from another server, it will fail with the following error message:

XMLHttpRequest cannot load http://www.domain.com/path/filename. Origin null is not allowed by Access-Control-Allow-Origin.

same-origin (38K)

Enter the postMessage() Method

PostMessage() is a global method that safely enables cross-origin communication. It's a lot like Ajax but with cross-domain capability. We'll give it a whirl by setting up two-way communication between a web page and an iframe whose content resides on another server. In order to avoid using multiple servers or hosting one of the pages on my own site, we'll cheat a little bit by viewing the parent locally and hosting the iframe page on a development server. You can use any server you like because the iframe only requires one .html file. I personally use WampServer.

The Main Page

Our parent page includes an iframe that contains the hosted postMessageReceiver.html page, along with a form for sending messages to the iframe:

<style>
body {
  background-color: lightgray;
}

iframe {
  display:block;
  width:500px;
  height:300px;
  margin-top: 5px;
  border: medium solid black;
  background-color: white;
}
</style>
<h1>postMessage() Demo</h1>

<div class="container-fluid">
<p>
  <form id="form">
    <input type="text" id="msg" placeholder="Type message to send"/>
    <input type="submit" value="Send message" />
  </form>
  <p id="msg"> </p>
  <iframe id="externalContent" src="http://localhost:8080/postMessage/postMessageReceiver.html"></iframe>
</p>
</div>

There is also a script that handles the sending and receiving of messages.

On the form submit, we need to reference the iframe's content window and invoke its global postMessage() method, passing in the JSON-encoded message and the iframe's origin. Note that the message does not have to be JSON; any string is fair game:

//expected origin of iFrame content...
var ORIGIN = "http://localhost:8080";

//send a message into the hosted iFrame...
document.getElementById("form").onsubmit = function(e){
  //target the iFrame
  var win = document.getElementById("externalContent").contentWindow;

  //use JSON.stringify() to send text...
  win.postMessage(JSON.stringify({ 'newMessage': document.getElementById("msg").value }), ORIGIN);

  return false;
};

There's also a function for accepting incoming messages from the iframe. Here, we have a check against the expected sender's origin so that we may ignore anything that comes in from an unexpected source:

origin_check (45K)

The message is then displayed in a paragraph element:

//listen for messages coming from the expected origin...
function listener(event){
  if(event.origin !== ORIGIN){
    return;
  }

  var response   = JSON.parse(event.data),
      newMessage = response['newMessage'];

  document.getElementById("msg").innerHTML = "received: \""+(newMessage ? newMessage : 'nada')+"\" from: "+event.origin;
}

The iframe Content

The iframe page - named postMessageReceiver.html - is hosted on the server. To make that apparent, there is a dynamically populated field that displays the domain.

This page also contains a form for sending data to the parent page:

<p><b>This iframe is located on <span id="domain"> </span>.</p>

<div id="msg"> </div>

<form id="form">
    <input type="text" id="msg" placeholder="Type message to send"/>
    <input id="send_msg" type="button" value="Send message to parent page" />
</form>

You'll notice that the JS code here is much the same as the parent's except for the code that displays the domain as well as one other thing which we'll get to in a moment:

var ORIGIN = 'file:';

document.getElementById('domain').innerText = location.protocol + '//' + location.host;

//these are functions that are called from the parent page into this one...
function listener(event){
  if(event.origin !== ORIGIN){
    return;
  }
  console.log(event);

  var response   = JSON.parse(event.data),
      newMessage = response['newMessage'],
      origin     = event.origin == 'file:' ? 'local parent page' : event.origin;
  
  document.getElementById("msg").innerHTML = "received: \""+(newMessage ? newMessage : 'nada')+"\" from: "+origin+".";
}

The other addition is in the code that sends messages to the parent page. If you look closely at the postMessage() call, an origin value of "*" is passed for the local page. That's done because the "file:" protocol is not permitted. The asterisk means that we don't care who the targetOrigin is:

//send a message to the parent page...
document.getElementById('send_msg').addEventListener('click', function(e) {
  window.parent.postMessage(JSON.stringify({ 'newMessage': document.getElementById("msg").value }), 
                           (ORIGIN == 'file:' ? '*' : ORIGIN));
}, false);

Here's the demo in action:

demo (44K)

Speaking of which, here is a .zip archive with the two pages. Just remember to put the postMessageReceiver.html file on your server and access postMessageDemo.html directly from your file system - i.e. "C:\postMessage\postMessageDemo.html".

Conclusion

You'll be happy to know that the window.postMessage API enjoys support in all of the most popular modern browsers, with only IE 11 providing partial support. Having said that, I ran my demo on it and had no issues.



Rob Gravelle

Rob Gravelle resides in Ottawa, Canada. His design company has built web applications for numerous businesses and government agencies. Email him.

Rob's alter-ego, "Blackjacques", is an accomplished guitar player, who has released several CDs and cover songs. His band, Ivory Knight, was rated as one of Canada's top hard rock and metal groups by Brave Words magazine (issue #92).



Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •  
  •  
Thanks for your registration, follow us on our social networks to keep up-to-date