Partager via


Cross-Document Messaging and RPC

Øyvind Sean Kinsey | June 29, 2010

 

Even though we, for security reasons, most often do not wish pages from different domains to be able to communicate, sometimes we do. And then, we discover that there is no 'proper' way to do so - the current standards and the current technologies are built to disallow it. So we turn to workarounds, we use dynamic script tags included from external domains, we use JSONP, or we try our best using  postMessage or the  IFrame URL technique (FIM). These solutions often become quite complex and fragile, and transporting the pure string messages between the domains might very well end up making up most of the code.

Typical cross-domain scenarios:

  • A document in domain foo.com with an iframe pointing to bar.com, where either document tries to access elements, properties or code from the other
  • A document in domain foo.com trying to use the XMLHttpRequest object to load URLs from bar.com

In this article I will go through the methods available in the different browsers with example code for each one. I will then show you how you can utilize a normalized API for things like RPC, and finally I will introduce to you a ready-made framework for all of this.

One important thing to remember when it comes to working around the Same Origin Policy (SOP), is that it was set in place for a reason, and so any workarounds need to take this into account. In addition, the solution we choose needs to work reliably, and across all the targeted browsers.

So before we begin, let’s define some qualities that should be asserted in any implementation.

  • Only data should be able to pass the boundary (this means strings as objects would incur a security risk)
  • Only the intended recipient should be able to read the messages
  • The recipient must be able to assert who the sender of the message is in order to avoid spoofing
  • The messages must be delivered reliably
  • Messages of arbitrary size should be supported
  • No context must leak (one window cannot be able to reference  any objects owned by another)
  • Should work equally in all targeted browsers

Failing to meet any of these means that the solution will either be susceptible to attacks or that it might not work reliably for all users/situations.

 

Available methods

Note: The example code given here is the minimum needed to show how the different techniques work. Additional code could be added that would provide the missing qualities, but this will not be covered here.

postMessage

Browser Support: Available in IE8, Firefox 3, Chrome 2, Safari 4 and Opera 9

This is a feature that is defined in the draft for the upcoming HTML5 standard, but which has already been implemented in all major browsers. The standard defines a method, window.postMessage and a corresponding message event, which allows one document to post messages, and another to register to receive messages.

Example

foo.com 
    <iframe src="https://bar.com/" id="barFrame">
    </iframe>

    var win = document.getElementById("barFrame").contentWindow;
    win.postMessage("hola!", "https://bar.com"); // make sure only https://bar.com can receive this  message

bar.com 
    window.addEventListener("message", function(message) {
        if (message.origin == "https://foo.com") { // make sure we only receive messages from those we trust
            alert(message.data);
        }
    });

Caveats:

  • Is only supported by relatively new browsers

Fragment Identifier Messaging

Browser Support: Available in all browsers

This is probably the most known technique, and it works due to a small loophole in the SOP. In order for one document to be able to navigate another, write access to document.location is allowed, and what this means is that we can write information by passing it in the url. Since we normally don't want to reload the document we are talking to, we use the fact that if we set the location property to the same url, with only the part after the " # " changed, no reload will occur. And since the document being written to has full read access to its location property it can easily retrieve it.

Example

foo.com 

    <iframe src="https://bar.com/" id="barFrame">
    </iframe>

    var win = document.getElementById("barFrame").contentWindow;
    win.location = "https://bar.com/#hola!";

bar.com 

    var prevMsg = location.hash;
    window.setInterval(function(){
        if (location.hash !== prevMsg) {
            prevMsg = location.hash;
            alert(prevMsg);
        }
    }, 100);

Caveats

  • Difficulties signaling when a message could be expected. Using timers are not advised in order to conserve resources
  • No way of identifying the sender
  • Messages might be written faster than the recipient can read them
  • Different browsers have different max sizes for the URLs. For example, IE6 supports URLs with a max length of 4095 characters.

'window.name'

Browser Support: Available in all browsers where the windows name property is persisted when navigating across domains

This method works due to another loophole that has now been shut in newer browsers. The basic way it works is that if you set the  ' name ' property of a window in your own domain, and then redirect the window to that of another, then the loaded document will be able to read its name property and so receive the message. And again, since we don't want the recipient’s document reloaded, we use a helperframe for this.

Example

foo.com 

    <iframe src="https://bar.com/" name="barFrame" id="barFrame">
    </iframe>
    <iframe src="sender.html" id="helper">
    </iframe>

    var helper = document.getElementById("helper").contentWindow;
    helper.sendMessage("hola!", "https://bar.com/receiver.html"); //sendMessage is defined in sender.html

foo.com/sender.html 

    window.sendMessage = function(message, url){
        window.name = message;
        location.href = url;
    };
bar.com 

    function onMessage(message) {
        alert(message);
    }

bar.com/receiver.html 

    parent.frames["barFrame"].onMessage(window.name); // pass the message along
    window.history.back(); // move back to the sender.html document

Caveats

  • Requires additional helper documents, and additional frames
  • Requires additional requests to the server (unless aggressive caching is used)

'NIX'

Browser Support: Available in IE6 and IE7

This is definitely one of the more obscure techniques and it was brought to my attention when I rummaged through the source code of the Apache Shindig project. It works due to an iframes opener property being writable for the parent window, while being readable by the iframe itself, in addition to it being able to store not only primitives, but any kind of object. This can then be used to exchange functions for passing messages back and forth.

Example

foo.com 

    <iframe src="https://bar.com/" id="barFrame">
    </iframe>

    var postMessage;
    function onMessage(msg) {
        alert(msg);
    }
     
    var win = document.getElementById("barFrame").contentWindow;
    win.opener = {
        setPostMessage: function(fn) {
            postMessage = fn;
    },
    postMessage: function(msg) {
        window.setTimeout(function(){ //defer the call so that it is run in the 'correct' 'context'
            onMessage(msg);
        },0);
    }
    };
    window.setTimeout(function(){ //wait until the child document has loaded and postMessage has been set
        postMessage("hola!");
    }, 100);

bar.com 

    function onMessage(msg) {
        alert(msg);
    }
    window.opener.setPostMessage(onMessage);
    window.opener.postMessage("right back at ya");

Caveats

  • Leaks the windows contexts.  One window can easily get access to 'privileged' data and not just what is passed.
  • Numeral problems related to security, hijacking, man-in-the middle etc.
  • No way of identifying the sender.

In addition to the above mentioned methods you also find techniques like JSONP to retrieve data, and cross-domain POSTs to send data, but common for these are that you require server components to maintain state, and so they aren't truly solutions for Cross-Document Communication, where both documents are able to maintain a context and a state.

Utilizing a normalized API

With a little work (actually, a lot), all of the above techniques can be normalized into a full-duplex transport with a postMessage function for sending, and an onMessage function for receiving messages, and in this case a message is always a string primitive.

/**
* @param {String} msg The message to transport
* @param {String} recipient The domain of the intended recipient  
*/
function postMessage(msg, recipient) {
    // implementation
}
 
/**
* @param {String} msg The message
* @param {String} origin The originating windows domain  
*/
function onMessage(msg, origin) {
    // process message 
}

So far so good, but of what use is really a string based transport? Wouldn't it be better if we could call methods, supply arguments, and consume return values instead? As it turns out, this is actually not hard at all as we have two other resources available, the JSON-RPC protocol, which defines how to format a string with the information needed to invoke a method and to return its return value, and the JSON serializer/deserializer (either natively or through Douglas Crockfords JSON2 library). With these tools (a string transport, a protocol for Remote Procedure Calls using string messages, and a JSON serializer), a cross-domain RPC implementation is only moments away.

Let’s start with the procedure call:

According to the JSON-RPC specification a call should have the following format

'{"jsonrpc":"2.0", "method": "methodName", "params:" ["a", 2, false]}'

This is a format that we easily can achieve by serializing an standard javascript object using JSON.stringify. And once we have transported the string across the boundary then we can recreate the javascript object using JSON.parse.

Example

Note: In this example the assumption is that we have a working transport between one document and another.

foo.com 

var procedureCall = {
    jsonrpc: "2.0",
    method: "alertMessage",
    params: ["my message"]
};
//serialize the message
var jsonRpcString = JSON.stringify(procedureCall);
//send the message
postMessage(jsonRpcString, "https://bar.com"); // https://bar.com is the intended recipient

bar.com 

function alertMessage(msg) {
    alert(msg);
}
function onMessage(msg, origin) {
    //deserialize the message
    var procedureCall = JSON.parse(msg);
    //execute the call
    switch (procedureCall.method) {
        case "alertMessage":
            // call the requested method with the provided arguments
            alertMessage.apply(null, procedureCall.params);
            break;
        case ....
    }
}

That wasn't too complicated now was it? Now, if we want to also be able to call methods and receive return values we can mark the message with an id parameter so that we can link return values to their handlers.

Example

foo.html 

var calls = {}, callId = 0;
 
function onMessage(msg, origin) {
    //deserialize the message
    var procedureCall = JSON.parse(msg);
    if (procedureCall.method) { // this is a method call
        switch(procedureCall.method) {
            ...
        }
    }else{ // this is a result
        // retrieve the callback function
        var fn = calls[procedureCall.id];
        // execute it with the result 
        fn(procedureCall.result);
        // remove the callback
        delete calls[procedureCall.id];
    }
}
 
function rpcAdd(a, b, fn) {
    var id = ++callId; //get a new id
    calls[id] = fn; //store the function that should receive the return value
    postMessage(JSON.stringify({
        jsonrpc: "2.0",
        method: "add",
        params: [a, b],
        id: id
    }), "https://bar.com");
}
 
// this looks pretty much like any other asynchronous call right?
rpcAdd(3, 5, function(result) {
    alert("the result is: " + result);
});

bar.com 

function add(a, b) {
    return a + b;
}
 
function onMessage(msg, origin) {
    //deserialize the message
    var procedureCall = JSON.parse(msg);
    //execute the call
    switch (procedureCall.method) {
        case "add":
            // call the requested method with the provided arguments
            var result = add.apply(null, procedureCall.params);
            //return the value
            postMessage(JSON.stringify({
                jsonrpc: "2.0",
                id: procedureCall.id,
                result: result
            }), "https://foo.com");
            break;
        case ....
    }
}

Again, it's not hard when you have the right tools for the job :)

So in summary, what is needed for cross-document RPC?

Now, in order for all of this to be used reliably, the following is needed

  • Code must be written for each of the available techniques in order to satisfy the demands stated in the introduction, this means adding reliability, queuing, sender and recipient verification etc. AND making them full-duplex (going both ways)..
  • The different transports needs to be abstracted so that they have a common interface, and wrapped with logic to select the appropriate one
  • The transports need to be properly initialized before usage, either manually or automatically through the query string
  • Logic for generating RPC stubs, for handling RPC calls and notifications, and for handling all the edge cases for RPC needs to be added

Trust me; this is not done in an hour or two!

The solution

Luckily, there's no need for all of you to go through all of the steps above as there is already a framework that will do all of this for you. easyXDM is a JavaScript library that enables you as a developer to easily work around the limitation set in place by the Same Origin Policy, and it does so by doing all of the above, and by doing this with quality JavaScript code. It provides two levels of abstractions; a Socket class that is used for string based messaging, and an RPC class that provides Remote Procedure Calls.

Using the easyXDM.Socket class

You only need to pass a single argument to easyXDM for it to set up a transport, and that is the URL to the remote end. The rest, including passing the needed parameters to the other end is handled by easyXDM.

Example

foo.com 

//To enable the use of the NameTransport, some additional arguments needs to be passed, but this will not be covered here.

var socket = new  easyXDM.Socket({
    // this is the URL to the other end, the provider. This is only needed for the main document, the consumer.
    remote: "https://foo.com/index.html",
    // onMessage is the function that will be called with incoming messages.
    onMessage: function(msg, origin) {
        alert("received message:" + msg + " from " + origin);
    },
    // onReady is called once the transport is ready to use.
    onReady: function(){
        // here you can put code that should run once the transport is up and running
    }
});
 
// each Socket has a method 'postMethod' that can be used to send messages
// messages sent prior to the onReady event being fired will be buffered and executed once ready
socket.postMessage("hola!");

bar.com/index.html 

// on the provider end, easyXDM automatically sets up the transport based on information passed in the URL.
var socket = new easyXDM.Socket({
    onMessage: function(msg, origin) {
        alert("received message:" + msg + " from " + origin);
    }
});
socket.postMessage("hola!");

Try the demo

This socket supports all browsers - IE6+, IE8, Firefox 3, Chrome 2, Safari 4 and Opera 9 with transit speeds of less than 15ms, and the rest depending on the underlying transport. It will also satisfy all the demands that we stated earlier, and some more.

Using the easyXDM.Rpc class

Using the RPC class for Remote Procedure Calls isn't much harder either

Example

foo.com 

// the first object passed here is pretty much identical to that passed to the Socket constructor
var rpc= new easyXDM.Rpc({
    remote: "https://foo.com/index.html"
},
// this is where we define the methods that we want to expose, and the remote methods that we want to have stubs generated for
{
    // here we define the methods that we want to expose
    local: {
        barFoo: function(a, b, c, fn){
            // here we can implement the exposed method.
            // if its a synchronous method then we can use
            // 'return value;' to return the value
            // if its asynchronous (e.g. doing ajax) then we use
            // fn(value);
            return a + b.toString() + c.toString();
        }
    },
    // these define the stubs that easyXDM should create
    remote: {
        fooBar: {}
    }
});
 
// send a JSON-RPC 2.0 Notification (no callback functions results in a notification)
rpc.fooBar();

bar.com/index.html 

// again, here we do not need to supply any information regarding the transport, this is handled automatically
var rpc= new easyXDM.Rpc({}, {
    local: {
        fooBar: function(){
            // this was called using a JSON-RPC Notification, lets run a regular function call
            rpc.barFoo("a", 1, false, function(result) {
                alert("the result of rpc.barFoo was " + result);
            });
        }
    },
    remote: {
        barFoo: {}
    }
});

Try the demo

As you can see, the instance of the RPC class will be augmented with stubs for all of the methods, making the use no different from any other asynchronous method!

Use cases

easyXDM is really just a framework that facilitates Cross-Document Messaging and RPC, and so you can think of it as a building block - all by itself it does little, but as a foundation, it can do wonders.

  • Auto-resizing iframes
  • Facebook Connect-like APIs (vkontakte.com with 75 million have based their API on easyXDM)
  • Bookmarklets that augments the current page
  • Cross-Domain AJAX
  • Mashups
  • Bridging multiple web applications

Important note about the iframe

As you can see above no markup is needed, and this is due to easyXDM creating the iframes as necessary. This means that you cannot use easyXDM to connect existing iframes, something which is due to easyXDM needing full control in order to set up the transport.

By default the frame will be hidden from view as this is how most API's will work, but easyXDM also supports visible frames.

Example

var rpc= new easyXDM.Rpc({
        remote: "https://foo.com/index.html",
        onReady: function(){
            ....
        },
        container: document.getElementById("container"),
        props: {
            style: {
                border: "1px solid red",
                width: "100px",
                height: "200px"
            }
        }
    }, {
    local: {
        ...
    },
    remote: {
        ...
    }
});

As you can see, we pass a container to control where in the DOM the iframe should be placed, and we pass a props object that contains all the properties that should be applied to the iframe. The props object is deep-copied onto the iframe and so we can use 'style: {...}' to set the style.

Conclusion

Adding Cross-Document Communication to your applications might sound complicated, but it need not be - it's all about using the right tool for the job. easyXDM is one such tool, it's flexible, it's reliant and it's very easy to use as it does all of the heavy lifting for you. And did I mention that it is completely framework agnostic and cross-browser compatible?! easyXDM is licensed under the MIT License and for more information, check out the easyXDM web site as well as its API Documentation.

 

About the Author

Sean is the CTO of BRIK, a small development company in Norway. He specializes in Front-End engineering, but is known to stick his hands into everything from redundant hardware to software loadbalancers.

Sean's experience ranges from teaching low-level ATM, Wi-Fi and IP-over-HF as an Officer in the Norwegian Military to building advanced web based applications for the fitness industry. What he enjoys the most though, is to push the boundaries of what others believe is possible, and then preferably by using everyday household items like HTML, javascript and a dash of CSS! 

When not working, Sean enjoys running, biking and good beer.

Find Sean on: