This is my personal blog. The views expressed on these pages are mine alone and not those of my employer.

Wednesday, October 12, 2005

AJAX Tutorial: How to Invoke Web Services From a Web Page On A Different Host

[Update: This technique doesn't work. I messed up and thought I had it working on my own machine, but I was wrong. The browser does indeed block the remote iframe calls, since they are to a different domain.]

While working at the Internet Archive this month, I had a need to call a RESTian web service that was running on a server seperate from the one the web page was pulled from. I initially thought this would be impossible; if I download a page from foo.com, and want to call some web service at somewhere-else.com, I thought the JavaScript same origin policy would prevent me from doing so.

It turned out I was wrong. There is a way to call remote web services from a web page that is seperate from the one that served the web page itself. This makes it possible to assemble AJAX web pages that call out to a host of different web services across the Internet, opening the door to more sophisticated applications.

The first thing to know is that both XMLHttpRequest and the XML parsers baked into many browsers can't call out to addresses that are different than the web page they are on, so they aren't appropriate.

What we have to do is a bit tricky. First, our remote web service must be RESTian, and should use the HTTP GET and POST verbs only. You should design your RESTian web services to use these anyway; I know, I know, using verbs like PUT and DELETE can make your service more elegant, but using those verbs makes it impossible for less capable web clients to access your service (like Safari, which doesn't support the PUT verb in XMLHttpRequest).

Second, we must have an iframe that invokes the service; the code below is more simplistic than you would use in your own program, but it gets the point across:
<iframe id="serviceResults" onload="resultsAvailable()"
src="http://some_other_domain.com/someservice">
</iframe>
You could change the iframe's src over and over programmaticly to invoke the remote web service with different values.

Next, your remote web service should return XML, but it must have one very important line so that Internet Explorer doesn't barf:
<?xml version="1.0"?>
<?xml-stylesheet type="text/css" href="blank.css"?>
<some-results>
<some-value message="foobar">Hello World</some-value>
</some-results>
Notice the xml-stylesheet directive; this is the important line that your web service results must return. By default Internet Explorer will download the XML results from the web service into the iframe and transform it into HTML, which will destroy the XML. This line tells Internet Explorer to use blank.css for transforming; blank.css doesn't exist, which will prevent Internet Explorer from transforming the document at all, leaving it as XML.

Once the iframe is loaded with the results and resultsAvailable() is called, you can do the following to manipulate the results as an XML document in both Firefox and Internet Explorer:
function resultsAvailable() {
var serviceResults = document.getElementById("serviceResults");
var doc = serviceResults.contentDocument;
if (typeof doc == "undefined") // Internet Explorer
doc = serviceResults.contentWindow.document;

// now you can call getElementsByTagName and getElementsById
// on the doc to grab your XML data, and use the DOM methods
// to walk over the results
var serviceValues = doc.getElementsByTagName("service-results");
var serviceValue = serviceValues[0];
var message = serviceValue.getAttribute("message");
var serviceData = serviceValue.firstChild.data;
}
I have tested this on FireFox 1.0 and IE 6.

A POST can be simulated by creating a form inside of the iframe, and then programmatically POSTing it to the address of the iframe with the input elements set to the values you want to send. The XML results will then fill the iframe and you can retrieve them the same way as above.

You can think about alot of ways to use this. If you wrap it in a script and create the iframe programmatically, then you can make a fancy API for others to script your remote service from their web page. Or, you can use it as a server side include to insert HTML data into the page using a remote web service. Or, imagine wrapping it in a bookmarklet (using the giant bookmarklet code I created recently), and use it to create an advanced bookmarklet that interacts with arbitrary, remote web services in the context of the currently displayed web page in the browser. Or, you can use this to establish a sophisticated RESTian API for your product that can be called into from a variety of advanced contexts.

My consulting company, Web 2.0 Consulting, is available to apply these kinds of techniques to your own products. I imagine and discover new ways of working with AJAX that can provide features in your product that no one else has yet. Contact me if you are interested.

Comments:
Potentially really useful, Brad.

I just made a change in Webjay to make this hack work -- append "?Accept=text/xml" to any XSPF URL and you'll get XML formatted in the way you need.

EG http://webjay.org/by/lucas_gonze/organism.xspf?Accept=text/xml
 
Nice. That's similar to how we are using this technique at the Internet Archive. Since you can't change the request header to specify what kind of MIME types you want, we have a type=xml or type=html attribute you can put on the query.
 
Are you sure this works with arbitrary domains?

I mean, what does prevent a malicious site from downloading some intranet xml or say access my gmail in this way? isn't that the whole idea behind the same origin rule?
 
Very nice!
 
When I tried this, in Firefox 1.5b2, I got "Error: uncaught exception: Permission denied to get property XMLDocument.getElementsByTagName", which is what I'd expect to happen with a cross-domain iFrame, otherwise this would be a security hole.
 
Thanks for this article. Does give an insight on what more this technology can do. One question: Can I use the same trick to create a RSS reader? Would I be able to contact sites offering RSS feeds and parse them with my code? Is that possible?
 
ttt, I'm using Firefox 1.0 and didn't get that. I'll have to check that.

Mandy, that's a great idea; you might be able to create an RSS reader doing this.
 
Hold off on using this technique a bit; first I thought it worked, then I thought maybe I was mislead because I loaded it from the localhost which can sometimes have different security policies, now I'm not too sure. I'm investigating :)
 
yeah.. an iframe would really be the only way to do it.

It's funny that you can't do this with XMLHttpRequest... this is the right way to do it. iframes should have this restriction too.

Since you can open an iframe to any arbitrary host you could get access to javascript on a host (say myspace.com which recently happened) and then launch a REST attack against an internal Intranet.

All you'd need to know is a bit about the target host. You could then post the data back to your servers within another iframe.

Good times.....

I think there are some restrictions on javascript execution with frames across sites though.
 
Post a Comment

Subscribe to Post Comments [Atom]



Links to this post:

Create a Link



<< Home

This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]