Speaking at MTS 2010 on "Rich user interface in ASP.NET MVC applications with jQuery plugins"

In few weeks (October 5th and 6th) I will be attending the MTS 2010 conference by Microsoft. On first day (October 5th) I will be speaking on "Rich user interface in ASP.NET MVC applications with jQuery plugins". There will be a little bit of talking and a lot of samples. I'm planning to cover CascadingDropDown, jTemplates, jQuery Treeview, jqGrid and few more.

I will do my best to share the presentation here (video if possible) but it will be all in polish (as it is polish conference).

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.

Attempting to retrieve a user's real IP address

Lately, I was in need of retrieving user's IP address. The first place I went looking was REMOTE_ADDR server variable. Unfortunately, due to all proxy servers out there, this variable can be pretty far from user's real IP address. After some googling I have put together a list of HTTP headers that might contain the real IP address:
  • CLIENT-IP
  • X-FORWARDED-FOR
  • X-FORWARDED
  • X-CLUSTER-CLIENT-IP
  • FORWARDED-FOR
  • FORWARDED
It's important to know, that X-FORWARDED-FOR may contain a comma+space separated list of IP addresses.
Having that list in mind, we can write a simple method which will attempt to retrieve user's real IP address:
public string DetermineIP(HttpContext context)
{
if (context.Request.ServerVariables.AllKeys.Contains("HTTP_CLIENT_IP") && CheckIP(context.Request.ServerVariables["HTTP_CLIENT_IP"]))
return context.Request.ServerVariables["HTTP_CLIENT_IP"];

if (context.Request.ServerVariables.AllKeys.Contains("HTTP_X_FORWARDED_FOR"))
foreach (string ip in context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"].Split(','))
if (CheckIP(ip.Trim()))
return ip.Trim();

if (context.Request.ServerVariables.AllKeys.Contains("HTTP_X_FORWARDED") && CheckIP(context.Request.ServerVariables["HTTP_X_FORWARDED"]))
return context.Request.ServerVariables["HTTP_X_FORWARDED"];

if (context.Request.ServerVariables.AllKeys.Contains("HTTP_X_CLUSTER_CLIENT_IP") && CheckIP(context.Request.ServerVariables["HTTP_X_CLUSTER_CLIENT_IP"]))
return context.Request.ServerVariables["HTTP_X_CLUSTER_CLIENT_IP"];

if (context.Request.ServerVariables.AllKeys.Contains("HTTP_FORWARDED_FOR") && CheckIP(context.Request.ServerVariables["HTTP_FORWARDED_FOR"]))
return context.Request.ServerVariables["HTTP_FORWARDED_FOR"];

if (context.Request.ServerVariables.AllKeys.Contains("HTTP_FORWARDED") && CheckIP(context.Request.ServerVariables["HTTP_FORWARDED"]))
return context.Request.ServerVariables["HTTP_FORWARDED"];

return context.Request.ServerVariables["REMOTE_ADDR"];
}

So how about this CheckIP method? The purpose of this method is to ensure that IP address is both a valid IP and does not fall within a private network range:
private bool CheckIP(string ip)
{
if (!String.IsNullOrEmpty(ip))
{
long ipToLong = -1;
//Is it valid IP address
if (TryConvertIPToLong(ip, out ipToLong))
{
//Does it fall within a private network range
foreach (long[] privateIp in _privateIps)
if ((ipToLong >= privateIp[0]) && (ipToLong <= privateIp[1]))
return false;
return true;
}
else
return false;
}
else
return false;
}

Now we are missing only two things. The first one is a method which converts IP from string to long:
private long ConvertIPToLong(string ip)
{
string[] ipSplit = ip.Split('.');
return (16777216 * Convert.ToInt32(ipSplit[0]) + 65536 * Convert.ToInt32(ipSplit[1]) + 256 * Convert.ToInt32(ipSplit[2]) + Convert.ToInt32(ipSplit[3]));
}

private bool TryConvertIPToLong(string ip, out long ipToLong)
{
try
{
ipToLong = ConvertIPToLong(ip);
return true;
}
catch
{
ipToLong = -1;
return false;
}
}

The second thing is an array of private network IP ranges:
private long[][] _privateIps = new long[][] {
new long[] {ConvertIPToLong("0.0.0.0"), ConvertIPToLong("2.255.255.255")},
new long[] {ConvertIPToLong("10.0.0.0"), ConvertIPToLong("10.255.255.255")},
new long[] {ConvertIPToLong("127.0.0.0"), ConvertIPToLong("127.255.255.255")},
new long[] {ConvertIPToLong("169.254.0.0"), ConvertIPToLong("169.254.255.255")},
new long[] {ConvertIPToLong("172.16.0.0"), ConvertIPToLong("172.31.255.255")},
new long[] {ConvertIPToLong("192.0.2.0"), ConvertIPToLong("192.0.2.255")},
new long[] {ConvertIPToLong("192.168.0.0"), ConvertIPToLong("192.168.255.255")},
new long[] {ConvertIPToLong("255.255.255.0"), ConvertIPToLong("255.255.255.255")}
};

Now we are good to go. Please keep in mind, that this approach doesn't guarantee that you will retrieve real IP address, as all of those headers are optional.