Introduction
From an early start, extending IIS using ISAPI Filters has always been a job for the C/C++ gurus or low-level code oriented developers. With the arrival of ASP.NET, Microsoft has offered a new view on "extending" IIS, and I double-quote the term, because HttpModules are a contained and protected extension solution to the ASP.NET world. While HTTP modules allow you to intercept requests and extend the processing pipeline of ASP.NET, they only affect requests targeting the ASP.NET framework.
These ISAPI Filters, and similar ISAPI Extensions (and Wildcard ISAPI Extensions in IIS 6.0) are the available extension slots to IIS as a Web Server. It would be great to be able to build them in .NET too, right? And be able to extend IIS as a whole and still be using .NET.
I'll try to give a basic view of a new .NET framework - Filter.NET - to extend IIS via ISAPI Filters built in .NET.
What About IIS7?
IIS7 is Microsoft's unified view of a web server extendible and managable using the .NET framework. No longer is there the native extension and managed extension. All extension slots are now managed, and you'll be able to extend it all the way using .NET. (to be precise, you still have a native-only extension area, but hopefully needed only on very special cases).
However, all of this applies to IIS7 only. And IIS 5.x and IIS 6.0 will live for some years still. That is where Filter.NET comes in.
IIS Events
Similarly to HTTP modules, IIS has always had a list of events to which the interested ISAPI Filters could subscribe. It is through these events that an ISAPI Filter can inspect and modify IIS behavior to its needs. These events are reflected in Filter.NET giving the managed filter author (what I usually call an ISAPI Filter in .NET) the same tools available to the native filter author.
The events below show the mapping between both worlds, the native and the managed: (taken from MSDN)
Native | Managed | Description |
SF_NOTIFY_READ_RAW_DATA | ReadRawData | Occurs when data is being read from the client. May occur more than once per request |
SF_NOTIFY_PREPROC_HEADERS | PreProcHeaders | Occurs immediately after IIS has pre-processed headers, but before IIS has begun to process header content |
SF_NOTIFY_URL_MAP | UrlMap | Occurs after IIS has translated a URL to a physical path on the server |
SF_NOTIFY_AUTHENTICATION | Authentication | Occurs just before IIS authenticates the client |
SF_NOTIFY_ACCESS_DENIED | AccessDenied | Occurs just after IIS has determined that access is denied for the request resource, but before IIS has sent a response to the client |
SF_NOTIFY_SEND_RESPONSE | SendResponse | Occurs after the request has been processed by IIS, but before any headers are sent back to the client |
SF_NOTIFY_SEND_RAW_DATA | SendRawData | Occurs as IIS sends raw data back to the client. May occur more than once per request |
SF_NOTIFY_END_OF_REQUEST | EndOfRequest | Occurs at the end of the request |
SF_NOTIFY_LOG | Log | Occurs at the end of a request, just before IIS writes the transaction to the IIS log |
SF_NOTIFY_END_OF_NET_SESSION | EndOfNetSession | Occurs when the network session with the client is ending |
SF_NOTIFY_AUTH_COMPLETE | AuthComplete | Occurs after the client's identity has been negotiated with the client |
The events depicted are a translation from the native world, as it should be, since Filter.NET is basically a managed view of the native framework. While I could explain further the motions and raw details of ISAPI Filters, there are already very good sites with that information.
First Basic Example
Sometimes, the best way to grasp the potential of a tool, is to see it in action. The following examples assume Filter.NET is installed and setup. To do so, follow the steps outlined in CodePlex here.
Remember UpCase? This old Microsoft ISAPI Filter sample is a quite good example of the power of ISAPI Filters. Imagine building it in .NET. That exactly what I did, while I was building new samples daily for Filter.NET.
Here's what you get:
using System;
using System.Collections.Generic;
using System.Text;
using KodeIT.Web;
namespace FilterDotNet.Samples.ChangeCase
{
internal class Filter : IHttpFilter
{
void IHttpFilter.Dispose()
{
}
void IHttpFilter.Init(IFilterEvents events)
{
events.PreProcHeaders += new EventHandler<PreProcHeadersEventArgs>(
OnPreProcHeaders);
events.SendRawData += new EventHandler<RawDataEventArgs>(
OnSendRawData);
}
void OnPreProcHeaders(object sender, PreProcHeadersEventArgs e)
{
e.Context.Session.Clear();
if (e.Context.Url.ToLower().EndsWith("/uc"))
{
e.Context.Session["UpCase"] = true;
e.Context.Url = e.Context.Url.Substring(0,
e.Context.Url.Length - 3);
}
else if (e.Context.Url.ToLower().EndsWith("/lc"))
{
e.Context.Session["UpCase"] = false;
e.Context.Url = e.Context.Url.Substring(0,
e.Context.Url.Length - 3);
}
}
void OnSendRawData(object sender, RawDataEventArgs e)
{
if (e.Context.Session.ContainsKey("UpCase"))
{
bool upCase = (bool)e.Context.Session["UpCase"];
int streamIndex = 0;
byte[] bytes = e.Context.GetData();
if (!e.Context.Session.ContainsKey("Headers"))
{
for (int ix = 0; ix < (bytes.Length - 2); ix++)
{
if ((bytes[ix + 0] == 0x0D) && (
bytes[ix + 1] == 0x0A) &&
(bytes[ix + 2] == 0x0D) && (
bytes[ix + 3] == 0x0A))
{
e.Context.Session["Headers"] = null;
streamIndex = ix + 4;
break;
}
}
}
if (e.Context.Session.ContainsKey("Headers"))
{
if (upCase)
{
for (; streamIndex < bytes.Length; streamIndex++)
{
byte b = bytes[streamIndex];
if ((b >= 'a') && (b <= 'z'))
{
bytes[streamIndex] = (byte)((b - 'a') + 'A');
}
}
}
else
{
for (; streamIndex < bytes.Length; streamIndex++)
{
byte b = bytes[streamIndex];
if ((b >= 'A') && (b <= 'Z'))
{
bytes[streamIndex] = (byte)((b - 'A') + 'a');
}
}
}
e.Context.SetData(bytes);
}
}
}
}
}
Looking at the code you'll find a similarity to HttpModules
, the IHttpFilter
interface. Since IIS is event based, it made sense to workout a similar extension template, based on event subscription.
public interface IHttpFilter
{
void Init(IFilterEvents events);
void Dispose();
}
Familiar, isn't it? That was the whole point. The events available to subscribe to are the ones in the table above. The actions, in each event, are again a reflection of what the native mode already provided us.
By carefully looking at the code, we can see that the SendRawData
event finds the end of the HTTP headers before starting the conversion process. The PreProcHeaders
event is used to signal if the conversion should be made to uppercase, lowercase or not be done at all. The URL is adjusted afterwards if needed.
Second Basic Example
The second example is quite an interesting one. It came up when a friend of mine was talking about using basic authentication and was afraid that, even with SSL, one could not prevent the authentication credentials to be restricted by a timeout. With that in mind, I thought about "creating" a timeout behavior to that "out of the box" authentication mode, the same way we all normally do with custom authentication.
While this can be accomplished with managed modules (a.k.a. HttpModules
), it only affects ASP.NET apps, and your environment may have applications written in other languages, like ASP, PHP, etc. In these cases you want a solution that works for every page you find sensitive.
Going forward with this example, the IIS authentication option is assumed to be "Basic Authentication." With this in mind, the requirements for such a solution are:
- Wait for the user to authenticate via Basic Auth
- Record the beginning of the TCP session after the user has authenticated
- On every request, record the last access date
- On every request, check if the idle time is above a certain limit. If it is, then ask the user credentials again. Since the 401 response will end the current TCP Session, we're certain to start a new TCP Session when the user sends its credentials again.
Based on the requirements above, using managed filters (a.k.a HttpFilters
) is quite simple. The events we're interested in are:
- When the request first arrives at IIS (
PreProcHeaders
event) - When the Basic Auth credentials are about to be validated by IIS (
Authentication
event) - When the access is denied by IIS (
AccessDenied
event)
Assuming we have the sessions idle timeout defined somewhere (the configuration file for example), we're all set. Let's build the managed filter:
using System;
using System.Text;
using KodeIT.Web;
namespace MyFilter
{
public class Class1 : IHttpFilter
{
const string SESSION_LOGGEDIN = "LoggedIn";
public void Dispose() { }
public void Init(IFilterEvents events)
{
events.PreProcHeaders += new EventHandler<preprocheaderseventargs />(OnPreProcHeaders);
events.Authentication += new EventHandler<authenticationeventargs />(OnAuthentication);
events.AccessDenied += new EventHandler<accessdeniedeventargs />(OnAccessDenied);
}
void OnAccessDenied(object sender, AccessDeniedEventArgs e)
{
e.Context.Session.Remove(SESSION_LOGGEDIN);
}
void OnAuthentication(object sender, AuthenticationEventArgs e)
{
if (e.Context.ServerVariables[
ServerVariable.AUTH_TYPE].ToLower().Equals("basic"))
{
e.Context.Session[SESSION_LOGGEDIN] = DateTime.Now;
}
}
void OnPreProcHeaders(object sender, PreProcHeadersEventArgs e)
{
if (e.Context.Session.ContainsKey(SESSION_LOGGEDIN))
{
if ((DateTime.Now - (DateTime)e.Context.Session[
SESSION_LOGGEDIN]) > new TimeSpan(0, 0, 10))
{
string response = String.Format(
"HTTP/1.0 401 Unauthorised\r\nWWW-Authenticate:" +
"Basic\r\n\r\n");
e.Context.WriteClient(ASCIIEncoding.ASCII.GetBytes(
response));
e.Context.TerminateRequest(false);
}
else
{
e.Context.Session[SESSION_LOGGEDIN] = DateTime.Now;
}
}
}
}
}
You might wonder about the e.Context.Session
collection and its purpose. This collection is a property bag associated with the TCP Session. Whenever the a TCP Session is initiated (the first request from the browser arriving at IIS) the property bag is created. This property bag instance will be alive while the TCP Session is alive. When the TCP Session is terminated (handled by EndOfNetSession
event) the session instance is also destroyed.
Going back to the code, the TimeSpan(0, 0, 10)
you see here can be set on a configuration file. In this example its value is 10 seconds, which means that if you do not make a new HTTP request in 10 seconds, you will be asked for your credentials again.
Setting up Filter.NET
To setup Filter.NET, follow these simple steps:
- Download Filter.NET from here or at top of article
- Install by running filterdotnetfx.msi
- Register Filter.NET in IIS by running the following
C:\WINDOWS\Filter.NET\v1.0.1>filter_regiis.exe -i
ACL: Adding (R)ead, (E)xecute for IIS_WPG in C:\WINDOWS\Filter.NET\v1.0.1 ...
Account IIS_WPG does not exist, and is ignored.
ACL: Adding (R)ead, (E)xecute for ASPNET in C:\WINDOWS\Filter.NET\v1.0.1 ...
Done
IIS: Add ISAPI Filter Filter.NET_v1.0.1 to /LM/W3SVC ... done
IIS: Wait 30s for IIS to stop ... stopped
IIS: Wait 30s for IIS to start ... running
After this step, Filter.NET is installed in IIS.
Configuring Managed Filters
Filter.NET has a simple configuration file located at %SYSTEMROOT%\Filter.NET\[version]\bin\filter.config. The directory where this file is located is also the same directory where all managed filters are expected to be located (besides the GAC).
The configuration file format is:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="httpFilters"
type="KodeIT.Web.Configuration.HttpFiltersSection, KodeIT.Web,
Version=1.0.1.0, Culture=neutral,
PublicKeyToken=18823f5c6a796933" />
</configSections>
<httpFilters>
</httpFilters>
</configuration>
The <httpFilters>
tag has some useful attributes — errorDetail
and errorPage
— to define when to show error pages and what the error pages should look like. The available optional attributes are:
<httpFilters errorDetail="On|Off|LocalOnly" errorPage="somePage.htm">
</httpFilters>
As the snippet above shows, you can display errors with detail or without detail. You can also only display them when the request is made from the same box where IIS is located.
Where We Are
Filter.NET is one of the frameworks I found missing in the whole Microsoft .NET approach to IIS 5x and IIS 6.0. After creating several ISAPI Filters I thought about creating this framework, given the usefulness of the .NET framework. Performance wise, it's not as fast as C/C++ but you'll find its almost as fast. There are obvious gains in development, so I know it's a good choice!
This framework is also available here. There are several samples available, and I'll try to build more. Feel free to build more samples yourselves too.
I'll be blogging about this too.
Enjoy!