Thursday, January 17, 2008

The Pain of Cross-Domain

In my last post, I talked about S3 Caching of RESTful services. The sad part about this approach is that you inevitably will run into cross-domain issues if you build your client to hit S3. So, in this post, I'll run down what techniques you can do to deal with cross-domain issues.

Cross Domain Flash
This is the easiest one. Flash natively supports cross domain communication, but only via a whitelist published on the server hosting the data. So, for example, if your Flash file is hosted at mydomain.com, and it wants to talk to myservice.com, the myservice.com host must publish a crossdomain.xml file that whitelists mydomain.com. Once whitelisted, the communication can take place. Below is an example of a crossdomain.xml file you'd place at the root of myserver.com's server to whitelist Flash widgets loaded at mydomain.com.

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy
SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="mydomain.com" />
</cross-domain-policy>

For S3, just throw up a crossdomain.xml file with public read access for the domain you host your Flash from, and you're good to go.

Cross Domain AJAX
For Cross-Domain AJAX (communication via Javascript) you have a few different options. The one most people will point you to is to use a proxy. Specifically, you build a web service that retrieves and serves content from another domain, that you host in your own domain. I don't like this approach because it pushes more load onto your infrastructure, and the main goal behind S3 Caching is to take load off of your servers. (In addition to minimizing points of failure.)

We could dedicate a web server in our domain to just shuffle data between S3 and our clients as a proxy, but in that case, we might as well store our files locally on disk instead of using S3. S3 caching is meant to be used for scenarios where even adding a single machine to your enterprise is cost-prohibitive (for example, if you are bootstrapping), so we shouldn't consider this approach.

The solution I've settled upon is to use a library by a clever fellow named Jim Wilson called SWFHttpRequest. It provides an interface nearly identical to XMLHttpRequest, so it can be relatively easily dropped into existing Javascript frameworks. For example, to use SWFHttpRequest with Prototype, you simply add this code:
Ajax.getTransport = function() {
return Try.these(
function() {return new SWFHttpRequest()},
function() {return new XMLHttpRequest()},
function() {return new ActiveXObject('Msxml2.XMLHTTP')},
function() {return new ActiveXObject('Microsoft.XMLHTTP')}
) || false;
};
I use OpenLaszlo, and here's a bit of code that will get your DHTML Laszlo apps to use it:

if (typeof(LzHTTPLoader) != "undefined") {
// Laszlo
LzHTTPLoader.prototype.open = function (method, url, username, password) {
if (this.req) {
Debug.warn("pending request for id=%s", this.__loaderid);
}

{
try {
this.req = new SWFHttpRequest();
} catch (err) {
this.req = window.XMLHttpRequest? new XMLHttpRequest():
new ActiveXObject("Microsoft.XMLHTTP");
}
}
this.__abort = false;
this.__timeout = false;
this.requesturl = url;
this.requestmethod = method;
}
}

I basically just took the code from the Laszlo source and jacked in a try/catch that attempts to bind to an SWFHttpRequest if it is available.

The downsides? First, and foremost, this requires Flash 9. We're mostly working with rich clients here though, and Flash 9 has 95% penetration, so we'll deal. Additionally, code must be updated such that it will not begin AJAX requests until SWFHttpRequest has been loaded. Basically, you just need to hold off until the global SWFHttpRequest has been defined, and you're good to go. (Of course, you can omit this step if you don't require cross-domain AJAX on start-up.)

Cross-Domain IFrame Communication
The last (and arguably most painful) type of cross-domain communication is between frames. Tassl has several nested IFRAMEs which are used for drawing HTML over the OpenLaszlo Flash app. There are cases where I need to trigger events in the outer frame from the inner frames, which are generally cross-domain. (The inner frames will often have content loaded from S3.)

Well, the way you can do it is by using the fragment identifier. The general technique is outlined by a Dojo developer. In short: the inner frame sets the location of the outer frame to be the same as it was plus a fragment identifier which contains the data to send. The outer frame polls it's own location to check for changes in the fragment identifier. When there is a change, the outer frame assumes it came from the inner frame, and it takes the fragment out as the data.

The naive technique worked until IE7 came along, and then things got messy when you have an iframe within an iframe that need to talk to one another. Microsoft responded with an explanation. I'll re-hash the solution here.

The following set up works in IE7:

-- outer frame (hosted at facebook.com) (Frame Z)
-- app iframe (hosted at secure.tassl.com) (Frame A)
-- app content iframe (hosted at S3) (Frame B)
-- data pipe iframe (hosted at secure.tassl.com) (Frame C)

Note that this issue doesn't occur if there is no "Frame Z".

So, the innermost application iframe (Frame B) must include its own iframe for communication now. (Frame C) As opposed to before, where it could simply pass back a fragment identifier directly to frame A.

To send data from from B to Frame A, Frame B must set the location on Frame C in the same way it did before directly to Frame A. Note that it must do this via window.open(url, nameOfFrameC), instead of trying to read Frame C's location object. By using window.open, you can avoid angering IE's cross-domain security checks.

To receive the data from Frame C, Frame A must have a polling routine that does the following:
  • Checks to see if Frame C exists in the dom. If not, bail out. (Using window.document.frames (in IE) or window.frames (in Firefox/Safari/Opera))
  • Acquire a reference to Frame C using another window.open trick:
    • var iframeC = window.open("", nameOfFrameC);
  • Read and decode iframeC.document.location.hash in the same way as the pre-IE7 technique.
So, by loading Frame C in the same domain as Frame A, and using the two window.open techniques, you are able to communicate from Frame B to Frame A. It's hairy, but it works. Make sure you integration test!

5 comments:

jimbojw said...

Hi there - good point about having to wait until the SWFHttpRequest class has been created before using it. I could add logic into the SWF such that it can call a function on load. That way, the class can announce its availability. Maybe I'll do that ...

jimbojw said...

Update: I just posted a new version which looks in the SWF url for a parameter called 'onload'. If specified, this function is called after the SWFHttpRequest object is ready for use.

For example, you could set the src attribute for the <embed> tag to "swfhttprequest.swf?onload=foo", and foo() would be executed after the SWF loads. Enjoy!

Greg said...

Ah, awesome addition. Thanks!

getify said...

Another alternative (similar to SWFHttpRequest) is "flXHR" ("flex-er") http://flxhr.flensed.com

It's also an invisible javascript+flash-based solution to direct cross-domain Ajax calls. The difference is that flXHR implements a completely identical API to the native XHR object, and totally automates all parts of embedding and managing the flash.

By emulating the XHR API, and abstracting out all the nasty flash details (even including flash plugin detection and auto-updating!), this makes flXHR a dead-simple, drop-in replacement for native XHR with cross-domain communication ability.

The best part about that is that it's super easy to then adapt/integrate flXHR into all the various JS frameworks, as they all can be easily "tricked" into using flXHR instances instead of native XHR, with no other code changes by the author necessary AT ALL.

The URL listed above has full documentation and a number of different examples of use, including how to easily adapt it into various JS frameworks.

Archie Pavia said...

Stunning! Gorgeous! So unusual! I like the information on this page is very useful for us all. I hope to continue providing information like this, thanks for sharing your creativity. -- luggage carry on size