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:
So we know how to perform request, let's take a look underneath. What you see below, are requests from FireFox:
We now have enough knowledge to start writing our HttpHandler:
...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.
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)
...
}
So we know how to perform request, let's take a look underneath. What you see below, are requests from FireFox:
OPTIONSThe 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:
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
- 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.
We now have enough knowledge to start writing our HttpHandler:
public class CrossOriginHandler : IHttpHandlerThis 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:
{
#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
}
private void SetAllowCrossSiteRequestHeaders(HttpContext context)Now let's add the one which deals with Origin:
{
//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);
}
private void SetAllowCrossSiteRequestOrigin(HttpContext context)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.
{
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", "*");
}