Friday, September 19, 2008

HttpModules, Your Best Friend

Keeps On Giving

HttpModules and HttpHandlers are probably one of your most versatile tools in ASP.NET. Learning these tools not only solidifies your understanding of the ASP.NET Request Life Cycle but will definitely save your bacon in years to come. What makes HttpModules even more powerful these days is that IIS 7 now supports HttpModules that you the developer can write. A developer can now write an HttpModule that could inspect/alter requests for all sites on a web server if need be! Talk about powerful.

Quick Refresher

Remember that HttpModules get called both before and after the end point (which is usually a page or some HttpHandler). They expose a series of events which you can subscribe to and use to alter/enhance the request. In the picture below you see a request coming in, passing through multiple HttpModules, hitting an HttpHandler (which could be an .aspx page) before passing back through the same modules. It's of note that you can "cherry pick" the events you want to subscribe to. Your HttpModule should be as lightweight as possible. In fact it has to be since your code will be called for every request in the web application! For more information on HttpHandlers/HttpModules check out this 15 seconds article.

Real Life Usage

Not too long ago I blogged about locking down application pages in wss/moss. Essentially there was a series of URLs that I didn't want the user to see lest they get prompted with an NTML authentication box. At the end of the post I also mentioned that an HttpModule could simply redirect people to another URL completely as a mild hack. Someone asked me to post the code and I though this would be as good an example as any for using an HttpModule.

What Does It Do?

This HttpModule is pretty straight forward, it hooks in at BeginRequest (quite early in the pipeline) and if the request matches a regular expression in the web.config, the user gets Response.Redirected to another URL. Let's have a look.

/// 
/// This class remaps all urls that match a set of regular expressions to
/// a given page.
///

public class RequestRemapper : IHttpModule
{
//List of regular expressions we're going to use to identify which URLs
//to remap to a dummy page.
private static string[] regularExpressionList =
ConfigurationManager.AppSettings["RequestRemapperRegularExpressions"].
Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

//Page we're going to redirect people too.
private static string redirectUrl = ConfigurationManager.AppSettings
["RedirectUrl"];
private List urlRegularExpressions;

///
/// A list of compiled regular expressions.
///

public List UrlRegularExpressions
{
get
{
if (urlRegularExpressions == null)
{
urlRegularExpressions = new List();

foreach (string regEx in regularExpressionList)
urlRegularExpressions.Add(new Regex(regEx, RegexOptions.Compiled
| RegexOptions.IgnoreCase));
}
return urlRegularExpressions;
}
}

///
/// Inspects the url to see if it matches a list of regular expressions that
/// we don't want users to see.
///

private void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = sender as HttpApplication;
HttpRequest request = application.Request;
HttpResponse response = application.Response;

//If this isn't the redirectUrl, it matches a url in our Url Filter List
//redirect them.
if (redirectUrl != request.Url.OriginalString && isUrlFiltered(request.Url))
response.Redirect(redirectUrl);
}

///
/// Checks to see if the given url matches any of those in
/// UrlRegularExpressions.
///

private bool isUrlFiltered(Uri url)
{
foreach (Regex regEx in UrlRegularExpressions)
{
if (regEx.IsMatch(url.OriginalString))
return true;
}

return false;
}

#region IHttpModule Members

//Nothing to release
public void Dispose() { }

public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}

#endregion
}
In addition to the above code you also need register the httpModule in the web.config and add an appSetting which is a semi-colon delimited list of regular expression describing all the "shapes" of URLs you want to remap.
<httpmodules>
<add type="RequestRemapper.RequestRemapper, RequestRemapper, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" name="RequestRemapper" />
</httpmodules>
...
<appSettings>
<add key="PageFilterRegularExpressions" value="http[s]?://.+((pages[/]?)$|
(documents[/]?)$|(forms[/]?)$|(forms/allitems.aspx?)$);" />
<add key="PageFilterRedirectUrl" value="http://someUrl"/>
</appSettings>

There's really tonnes of variants off this code, but it's not a terrible starting point. The starting regular expression remaps all urls that end with (pages, pages/, documents, documents/, forms, forms/, or forms/allitems.aspx). To help you build up and test a regular expression that meets your own URLs I'd recommend using a Regular Expression tester tool.

Not A Lot of Alternatives

HttpModules allow you to do things that would be pretty darn difficult using other ASP.NET tools. They're uniquely positioned to give you a lot of control over the request/response pipeline, this alone makes them worth learning. For the people asking for the MOSS redirect code, I hope that helps.

Take Care,
Tyler Holmes

No comments: