Background
Cross-Origin HTTP request (A.K.A. Cross-Domain AJAX request) is an issue that most web developers might encounter, according to Same-Origin-Policy, browsers restrict client JavaScript in a security sandbox, usually JS cannot directly communicate with a remote server from a different domain. In the past developers created many tricky ways to achieve Cross-Domain resource request, most commonly using ways are:
- Use Flash/Silverlight or server side as a "proxy" to communicate with remote.
- JSON With Padding (JSONP).
- Embeds remote server in an iframe and communicate through fragment or window.name, refer here.
And so on..
Those tricky ways have more or less some issues, for example JSONP might result in security hole if developers simply "eval" it, and #3 above, although it works, both domains should build strict contract between each other, it neither flexible nor elegant IMHO:)
W3C had introduced Cross-Origin Resource Sharing (CORS) as a standard solution to provide a safe, flexible and a recommended standard way to solve this issue.
Mechanism
From a high level we can simply deem CORS is a contract between client AJAX call from domain A and a page hosted on domain B, a tipical Cross-Origin request/response would be:
DomainA AJAX request headers
Host DomainB.com
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0) Gecko/20100101 Firefox/4.0
Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
Accept-Language en-us;
Accept-Encoding gzip, deflate
Keep-Alive 115
Origin http://DomainA.com
DomainB response headers
Cache-Control private />
Content-Type application/json; charset=utf-8
Access-Control-Allow-Origin DomainA.com
Content-Length 87
Proxy-Connection Keep-Alive
Connection Keep-Alive
The blue parts I marked above were the kernel facts, "Origin" request header "indicates where the cross-origin requestor preflight request originates from", the "Access-Control-Allow-Origin" response header indicates this page allows remote request from DomainA (if the value is * indicate allows remote requests from any domain).
As I mentioned above, W3 recommended browser to implement a "preflight request" before
submitting the actually Cross-Origin HTTP request, in a nutshell it is an HTTP "OPTIONS" request:
OPTIONS DomainB.com/foo.aspx HTTP/1.1
If foo.aspx supports OPTIONS HTTP verb, it might return response like below:
HTTP/1.1 200 OK
Date: Wed, 01 Mar 2011 15:38:19 GMT
Access-Control-Allow-Origin:
http://DomainB.com
Access-Control-Allow-Methods: POST, GET, OPTIONS, HEAD
Access-Control-Allow-Headers: X-Requested-With
Access-Control-Max-Age: 1728000
Connection: Keep-Alive
Content-Type: application/json
Only if the response contains "Access-Control-Allow-Origin" AND its value is "*" or contain the domain who submitted the CORS request, by satisfying this mandtory condition browser will submit the actual Cross-Domain request, and cache the result in "Preflight-Result-Cache".
Implementation
Let's take a look at server side code samples below (ASP.NET and PHP):
ASP.NET (C#)
protected void Page_Load(object sender, EventArgs e)
{
String data = String.Empty;
String returnJSONStr = String.Empty;
switch (Request.HttpMethod)
{
case "GET":
data = Request.QueryString["Data"];
returnJSONStr = "{\"Data\":\"Hi remote friend, you tried to passed me data: *" + data + "* through HTTP GET.\"}";
break;
case "POST":
data = Request.Form["Data"];
returnJSONStr = "{\"Data\":\"Hi remote friend, you tried to POST some mock data: *" + data + "* to me.\"}";
break;
case "OPTIONS":
break;
default:
returnBadRequestResponse();
break;
}
if (String.IsNullOrEmpty(data))
returnBadRequestResponse();
else
{
Response.AddHeader("Access-Control-Allow-Origin", "*");
Response.ContentType = "application/json";
Response.Write(returnJSONStr);
}
}
private void returnBadRequestResponse()
{
Response.StatusCode = 400;
Response.ContentType = "application/json";
Response.Write("{\"Error\":\"Bad HTTP request type!\"}");
}
PHP
if(isset($["Data"]))
{
$method=$_SERVER['REQUEST_METHOD'];
$data="";
if($method=="POST")
{
$data=$_POST["Data"];
$fakeData=new FakeData();
$fakeData->Data="Hi remote friend, you tried to POST some mock data: *"+data+"* to me.";
$fakeData->Time=new DateTime("now");
}
elseif($method=="GET")
{
$fakeData=new FakeData();
$fakeData->Data="Hi remote friend, you tried to passed me data: *"+data+"* through HTTP GET.";
$fakeData->Time=new DateTime("now");
}
else
{
RaiseError();
}
header('Content-type: application/json');
$jsonStr= json_encode($fakeData);
echo($jsonStr);
}
else
{
RaiseError();
}
function RaiseError()
{
http_send_status(405);
header("Status: 405 Method Not Allowed");
}
class FakeData
{
public $Data;
public $Time;
}
Then client Ajax call code:
var cor = null;
if (window.XMLHttpRequest) {
cor = new XMLHttpRequest();
}
else {
alert("Your browser does not support Cross-Origin request!");
return;
}
cor.onreadystatechange = function () {
if (cor.readyState == 4) {
document.getElementById('lbl').innerHTML = cor.responseText;
}
};
var data = 'Some fake data';
if (method == 'POST') {
cor.open('POST', 'http://WayneYe.com/Demo/CORSDemo/CORSDemoServer.aspx', true);
cor.withCredential = "true";
cor.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
cor.send('Data=' + data);
}
else if (method == 'GET') {
cor.open('GET', 'http://WayneYe.com/Demo/CORSDemo/CORSDemoServer.aspx?Data=' + data, true);
cor.withCredential = "true";
cor.send(null);
}
The JS code works for all mainstream browsers (IE8+, FF 3.6+, Chrome 8+), I didn’t use XDomainObject which introduced in IE8 because XMLHttpRequest is already supported by IE8+, FF and Chrome, Safari, in additional XDR seems have a lot of restrictions (refer: http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx).
Conclusion
Cross-Origin Resource Sharing provides a safe, flexible and a standard way for web developers to achieve Cross-Origin communication, maybe it is time to forget those tricky or inelegant ways like JSONP, Flash/Silverlight/server bridge or even window.name and so on:)
References