HttpHandler with cross-origin resource sharing support

Recently I was playing around with Cross-Origin Resource Sharing (you can read a W3C Working Draft for it here), which is a mechanism to enable client-side cross-origin requests. As you can see, the last working draft is from 27 July 2010, so the technology is still in development. Let's take a look, how we can use it from JavaScript:
...
try {
//Create XMLHttpRequest and assign it to 'request' variable
...

if (request) {
//Our request URL
var jsonUrl = 'http://localhost:60000/CrossOrigin';
//Check if XMLHttpRequest support CORS
if (request.withCredentials !== 'undefined') {
//If it does, prepare request
request.open('GET', jsonUrl, true);
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
request.onreadystatechange = function () {
if (request.readyState == 4) {
if (request.status == 200) {
document.getElementById('divTarget').innerHTML = request.responseText;
}
}
};
//If not, than check if XDomainRequest is available
} else if (typeof XDomainRequest != 'undefined') {
//If it is, prepare request
request = new XDomainRequest();
request.open('GET', jsonUrl);
request.onload = function () {
document.getElementById('divTarget').innerHTML = request.responseText;
};
} else {
throw (null);
}
} else {
throw (null);
}

//Send request
request.send(null);
} catch (e) {
//There is no support for CORS, use something else instead (for example JSONP)
...
}
The first condition checks for native support of CORS through XMLHttpRequest. This is the way to go for FireFox, Chrome and Safari - you don't need to write any special code, just standard XMLHttpRequest. The second condition is for Internet Explorer, which supports cross-origin requests through XDomainRequest. I didn't managed to get it to work in Opera.
So we know how to perform request, let's take a look underneath. What you see below, are requests from FireFox:
OPTIONS
Connection: keep-alive
Keep-Alive: 115
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Host: localhost:60000
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 ( .NET CLR 3.5.30729; .NET4.0E)
Origin: http://localhost:50000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: x-requested-with

GET
Connection: keep-alive
Keep-Alive: 115
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Host: localhost:60000
Referer: http://localhost:50000/CORS.htm
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 ( .NET CLR 3.5.30729; .NET4.0E)
X-Requested-With: XMLHttpRequest
Origin: http://localhost:50000
The first request (OPTIONS) is a preflight request. Its job is to ensure that the resource is ok with the request. It uses following headers to perform check:
  • Origin - It contains the origin of the request. You should put Access-Control-Allow-Origin header with the same value into response if you accept the origin (you can also use '*' as a value if you consider your resource public).
  • Access-Control-Request-Method - The method which will be used in actual request. In response you should put Access-Control-Allow-Methods header with list of allowed methods separated by commas.
  • Access-Control-Request-Headers - Comma separated list of custom headers which will be attached to request. You should put a list of headers which you accept into Access-Control-Allow-Headers header in response.
The preflight request can be sometimes skipped when the methods are simple (for example IE does it with GET method). When the browser receives proper response, the actual request is being made. It differs from standard request by having Origin header - you should put a proper Access-Control-Allow-Origin into response. Unfortunately Chrome and Safari doesn't put Origin into request, but they still require Access-Control-Allow-Origin in response. That makes using '*" necessary if you want those browser to work.

We now have enough knowledge to start writing our HttpHandler:
public class CrossOriginHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}

public void ProcessRequest(HttpContext context)
{
//Clear the response (just in case)
ClearResponse(context);

//Checking the method
switch (context.Request.HttpMethod.ToUpper())
{
//Cross-Origin preflight request
case "OPTIONS":
//Set allowed method and headers
SetAllowCrossSiteRequestHeaders(context);
//Set allowed origin
SetAllowCrossSiteRequestOrigin(context);
break;
//Cross-Origin actual or simple request
case "GET":
//Disable caching
SetNoCacheHeaders(context);
//Set allowed origin
SetAllowCrossSiteRequestOrigin(context);
//Generate response
context.Response.ContentType = "text/plain";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write("<h1>Hello World! [powered by Cross-Origin Resource Sharing]</h1>");
break;
//We doesn't support any other methods than OPTIONS and GET
default:
context.Response.Headers.Add("Allow", "OPTIONS, GET");
context.Response.StatusCode = 405;
break;
}

context.ApplicationInstance.CompleteRequest();
}
#endregion

#region Methods
protected void ClearResponse(HttpContext context)
{
context.Response.ClearHeaders();
context.Response.ClearContent();
context.Response.Clear();
}

protected void SetNoCacheHeaders(HttpContext context)
{
context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
context.Response.Cache.SetValidUntilExpires(false);
context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
context.Response.Cache.SetNoStore();
}
#endregion
}
This code should be easy to understand, but there are two key methods missing. Let's start with the one which puts Access-Control-Allow-Methods and Access-Control-Allow-Headers headers into response:
private void SetAllowCrossSiteRequestHeaders(HttpContext context)
{
//We allow only GET method
string requestMethod = context.Request.Headers["Access-Control-Request-Method"];
if (!String.IsNullOrEmpty(requestMethod) && requestMethod.ToUpper() == "GET")
context.Response.AppendHeader("Access-Control-Allow-Methods", "GET");

//We allow any custom headers
string requestHeaders = context.Request.Headers["Access-Control-Request-Headers"];
if (!String.IsNullOrEmpty(requestHeaders))
context.Response.AppendHeader("Access-Control-Allow-Headers", requestHeaders);
}
Now let's add the one which deals with Origin:
private void SetAllowCrossSiteRequestOrigin(HttpContext context)
{
string origin = context.Request.Headers["Origin"];
if (!String.IsNullOrEmpty(origin))
//You can make some sophisticated checks here
context.Response.AppendHeader("Access-Control-Allow-Origin", origin);
else
//This is necessary for Chrome/Safari actual request
context.Response.AppendHeader("Access-Control-Allow-Origin", "*");
}
As you can see, the method takes the absence of Origin header in Chrome and Safari into consideration.Complete sample application can be found here, go ahead and make some use of it.