How to send cross-domain messages using Javascript

 
Thinfinity  - Cross-Domain Messages

A basic rule in Web security holds that there is no direct javascript access between different windows loaded in a browser unless these windows are from the same origin.

This means that the windows must share protocol, host and port or it will not be possible to access a value or object on another page from the JavaScript code, or to add content. So, how can we establish and maintain the communication between different pages loaded in a cross-domain environment?

Fortunately, benefiting from the window.postMessage API, the windows can send non intrusive messages to each other via javascript in a cross-domain environment. These messages can be text, or an object in JSON format (JavaScript Object Notation) —something already available in most of modern web browsers.

In this article we will show how to apply this API to send cross-domain messages using Javascript in a secure way.

 

The window.postMessage API

In order to send a message to another window, one must invoke a postMessage() method, introduced below:

targetWindow.postMessage(message, targetDomain, [extra])

where:

targetWindow Reference to the window that will receive the message.
message JSON object or text that will be sent to the target window.
targetDomain Domain to which the message should be posted.
It can also be written “*” not to limit messages to a particular domain, although this is not recommended —unless absolutely necessary.
extra A sequence of extra —optional— objects that could be transferred with the message. The ownership of these optional objects is no longer on the sending side, but is transferred to the destination one.

But if a tree falls in a forest and nobody’s around to hear it, does it make a sound? In other words, it is useless to send the message if no one is prepared to listen to it. In order to be heard, the event message must be attended. Although most modern web browsers employ the addEventListener() method to add the treatment of the event, old IE versions makes use of its own; so we will cover both alternatives:

if (window.addEventListener){
    addEventListener("message", listenerFunction, false)
} else {
    attachEvent("onmessage", listenerFunction)
}

Where listenerFunction will be used to process the message coming from another window. This function will receive the message in the data attribute of the event received:

function listenerFunction(e) {
    if (typeof e.data == "string") {
        console.log(“The message is a text: ” +  e.data);
    } else {
        console.log(“The message is a JSON object:” + JSON.stringify(e.data));
    }
}

 

Some considerations regarding security

Cross-window messaging security model is two-sided. In the event of knowing the domains of the both parties involved —which is usually the case—, and in order to make the exchange of information between different domains safer, it is recommended to check, both when sending and receiving, the domains that participate in the messaging exchange. The sender ensures that the receiving domain is targetDomain. If the sender tries to send a message to a domain different to targetDomain, an error will occur.

The receiver can check the origin attribute of the received message event object to make sure that this came from a valid origin. Therefore, if the domain of origin does not match a valid domain, it can be ignored.

 

Let’s do it!

The following example implements cross-domain communication between two pages: one loaded in the localhost domain, and the other one in IP 127.0.0.1. To handle the exchange of messages we will create two javascript classes (MessageSender and MessageReceiver).

The MessageSender class has a single published method (sendMessage) and, during its creation, it will receive the window to where the message should be sent (targetWindow, mandatory) and the domain to which the message should be directed (targetDomain, optional), which if not received will be replaced by “*”. Both arguments are sent within a JSON object.

var MessageSender = function(args) {
    args = args || {};
    var targetWindow = args.targetWindow;
    var targetDomain = args.targetDomain || "*";
    var ready = false;
    var sendMessage = function(message) {
        targetWindow.postMessage(message, targetDomain);
    }
    return {
        "sendMessage": sendMessage
    }
}

The MessageReceiver class has two published methods. The start method initiates the listening of the message, while the stop method ends it. Both methods could receive a callback function to do an additional processing in the start/stop messaging. This class, when instantiated, is capable of receiving a JSON object with a pair of attributes, both optional. The first is a callback function to process the message; the other, a validOrigin string that indicates the domain from which it would be valid to receive messages. Should any of these parameters is not sent, the first one will be replaced by a default message processor , while the other, by the same page’s domain (i.e., it will not be cross-domain and will only receive messages from its own domain). In case you do not want to control the origin of the message, the validOrigin attribute must explicitly assert “*”.

var MessageReceiver = function (args) {
    args = args || {};
    var started = false;
    var validOrigin = args.validOrigin || window.location.origin;
    var defaultMsgProcessor = function (e) {
        if (typeof e.data == "string") {
            alert("The message is test:\n'" + e.data + "'");
        } else {
            alert("The message is a JSON object:\n" + JSON.stringify(e.data));
        }
    }

    var msgProcessor = args.callback || defaultMsgProcessor;
    var processMessage = function (e) {
        if (validOrigin == "*" || validOrigin == e.origin) {
            msgProcessor(e);
        }
    };
    var startListening = function (onStartCallback) {
        if (!started) {
            if (window.addEventListener) {
                window.addEventListener("message", processMessage, false);
            } else {
                attachEvent("onmessage", processMessage);
            }
            started = true;
            if (onStartCallback) onStartCallback();
        }
    }
    var stopListening = function (onStopCallback) {
        if (started) {
            if (window.removeEventListener) {
                window.removeEventListener("message", processMessage);
            } else {
                window.detachEvent("onmessage", processMessage);
            }
            started = false;
            if (onStopCallback) onStopCallback();
        }
    }
    return {
        "start": startListening,
        "stop": stopListening
    };
};

Both classes will be included in the same javascript file, which we will call crossDomainMessenger.js.

In order to use these classes, we will create two html pages. The first one, crossDomainSource.html, includes the crossDomainMessenger.js file and displays, on the top part, a line containing a text field (where the messages to be sent will be added) and some buttons to send messages. At the bottom of this page, the second page will load in an iframe, which would receive the messages.

From javascript, it will create both a MessageSender and a MessageReceiver; the first one to send messages triggered by the buttons to the page loaded in the iframe; and the last one to address messages that could be sent to the internal page.

This second page will also create a MessageSender and MessageReceiver, with the difference that in this case, the important work that will done by the latter.

The page also has buttons to toggle the message listener; a button to clear the list of messages received; and, a last button to send a message to the other page, which would close the communication circuit between the two windows.

Below you will find the code for the two pages of the example:

crossDomainSource.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Cross domain message example - Sender</title>
    <style>
        html, body { height: 100%; margin: 0px; padding: 0px; }
        #divmsg { height: 50px; line-height: 50px; vertical-align: middle; padding-left: 10px; padding-right: 10px; }
        #divtarget, #targetWindow {display: block; position: absolute; }
        #divtarget { top: 51px; bottom: 5px; left: 5px; right: 5px; border: solid 1px; box-sizing: border-box; }
        #targetWindow { width: 100%; height: 100%; border: none; }
    </style>
    <script type="text/javascript" src="crossDomainMessenger.js"></script>
    <script type="text/javascript">
        var msgSender = null;
        var msg = null;

        function init() {
            var targetDomain = window.location.protocol + "//127.0.0.1:" + window.location.port;
            var tw = document.getElementById("targetWindow");
            tw.src = targetDomain + "/crossDomainTarget.html";
            msgSender = new MessageSender({ "targetWindow": window.frames["target"], "targetDomain": targetDomain });
            msg = document.getElementById("txtmsg");

            // Creates a MessageReceiver that accepts messages from targetDomain only
            new MessageReceiver({ "validOrigin": targetDomain }).start();
        }

        function sendText() {
            msgSender.sendMessage(msg.value);
        }

        function sendObject() {
            msgSender.sendMessage({ "message": msg.value, "currentDate": new Date().toJSON() });
        }

        function sendClear() {
            msgSender.sendMessage({ "cmd": "CLEAR" });
        }
    </script>
</head>
<body onload="init()">
    <div id="divmsg">
        <label for="txtmsg">Message:</label>
        <input type="text" id="txtmsg" size="30" />
        <input type="button" value="send text" onclick="sendText()"/>
        <input type="button" value="send object" onclick="sendObject()"/>
        <input type="button" value="send clear" onclick="sendClear()"/>
    </div>
    <div id="divtarget">
        <iframe id="targetWindow" name="target"></iframe>
    </div>
</body>
</html>

crossDomainTarget.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Cross domain message example - Receiver</title>
    <style>
        html, body { height: 100%; margin: 0px; padding: 0px; }
        #msgPanel > div { border-bottom: dashed 1px #808080; }
    </style>
    <script type="text/javascript" src="crossDomainMessenger.js"></script>
    <script type="text/javascript">
        var msgSender = null;
        var msgReceiver = null;
        var msgPanel = null;
        function init() {
            msgPanel = document.getElementById("msgPanel");
            // This MessageReceiver accepts messages from localhost only.
            msgReceiver = new MessageReceiver({
                "validOrigin": window.location.protocol + "//localhost:" + window.location.port,
                "callback": function (e) {
                    if (typeof e.data.cmd != "undefined" && e.data.cmd == "CLEAR") {
                        clearMsgs();
                    } else {
                        addMessage("The message is " +
                            ((typeof e.data == "string") ?
                                "test:\n'" + e.data + "'" :
                                "a JSON object:\n" + JSON.stringify(e.data)));
                    }
                }
            });
            start();
            msgSender = new MessageSender({ "targetWindow": window.parent });
        }

        function start() {
            msgReceiver.start(function () { addMessage("*** msgReceiver was started!") });
        }

        function stop() {
            msgReceiver.stop(function () { addMessage("*** msgReceiver was stopped!"); });
        }

        function clearMsgs() {
            msgPanel.innerHTML = "";
        }

        function sendHellow() {
            msgSender.sendMessage(window.location.origin + " says Hello!");
        }

        function addMessage(message) {
            var newmsg = document.createElement("div");
            newmsg.innerHTML = message;
            msgPanel.appendChild(newmsg);
        }
    </script>
</head>
<body onload="init()">
    <div>
        <input type="button" value="start" onclick="start()"/>
        <input type="button" value="stop" onclick="stop()"/>
        <input type="button" value="clear" onclick="clearMsgs()"/>
        <input type="button" value="send msg to opener window" onclick="sendHellow()" />
    </div>
    <div id="msgPanel"></div>
</body>
</html>

This is enough to understand how the messaging between windows works, even when they are in different domains. In upcoming posts we will demonstrate how to use this technique to establish a smooth communication between Thinfinity VirtualUI or Thinfinity Remote Desktop with other applications to which they are integrated.

 

2 thoughts on “How to send cross-domain messages using Javascript

  1. This is a really cool article and learned a lot. More than anything read so far:)

    Trying to show some students examples and having heck of time grasping setting up between two domains (ie; example.com and anotherexample.com).

    Wondering if you another article using domains instead of localhost? If not, would it be too much to ask for example?

    Many thanks in advance,

    Susan

    • Hi Susan!

      This example uses localhost to ensure a proper operation. Of course, you can also define a confidence relationship to send cross-domain messages between pages in completely different domains without any problem, setting up your desired domains instead of localhost.

      I hope this answers your question.

      Kind regards,
      Daniel

Leave a Reply

Your email address will not be published. Required fields are marked *