Monday, September 24, 2007

CodeProject Article

Too much work, not much blogging :(

Nevertheless, I was able to publish a small article on CodeProject about Filter.NET:
http://www.codeproject.com/dotnet/extending_IIS.asp

check it out :)

Tuesday, September 4, 2007

Samples: Change Case

This sample attempts to demonstrate a managed view (although a bit modified) of the native sample UpCase Microsoft has provided some time ago to uppercase the payload.

Its simple to see how easy it is to do the same in managed code and at the same time enable this same behavior to any page you request. The sample here allows to uppercase or lowercase the payload of any http response by adding /uc or /lc to the url requested, respectively.



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(OnPreProcHeaders);
events.SendRawData += new EventHandler(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);
}
}
}
}
}


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.

Version 1.0.1 is out

While building another sample I bumped with an issue. The callback used to read raw data (ReadRawData or SendRawData events), called Cb_ReadRaw, was missing the last byte.

The code was:

void __stdcall Cb_ReadRaw
(
PHTTP_FILTER_RAW_DATA pRaw,
unsigned char * rawBuffer,
int rawLength
)
{
if( pRaw->cbInData && rawLength )
{
FillMemory(rawBuffer, rawLength, 0);
RtlMoveMemory(
rawBuffer,
pRaw->pvInData,
((DWORD)rawLength - 1) <>cbInData);
}
}

and was fixed with:

void __stdcall Cb_ReadRaw
(
PHTTP_FILTER_RAW_DATA pRaw,
unsigned char * rawBuffer,
int rawLength
)
{
if( pRaw->cbInData && rawLength )
{
FillMemory(rawBuffer, rawLength, 0);
RtlMoveMemory(
rawBuffer,
pRaw->pvInData,
(DWORD)rawLength <>cbInData);
}
}

As simple as the sample may have been, its still a quite good example on using managed filters. I'll let you know what the sample was soon enough ...

Saturday, September 1, 2007

Samples: Domain Redirect

From the top of my head, I see at least two scenarios where it is required that all http requests are done to a specific domain (versus the NETBIOS machine name): third party SSO requirements, Kerberos over HTTP authentication (SPNEGO).

Most SSO solutions out there for HTTP rely on setting a domain cookie on clients computer. As an example suppose all your boxes (Windows or Linux, doesn't matter) in DNS are named xxx.company.com. Also assume you have a third party SSO solution like CA SiteMinder (formerly Netegrity) setup in your environment. This solution, after authenticating, will set a domain cookie for company.com in your computer. For every subsequent request you do to any of the machines in your environment (xxx.company.com) the cookie is sent as part of the request. That cookie, after examined and validated, will determine if you have already logged in to the environment and will determine who you are.

As for Kerberos over HTTP, its required that the SPN (target principal) be registered in AD and in DNS. So, to make sure that everyone sends a well-formed request to every box for Kerberos authentication (instead of NTLM) its useful to automate the process of checking if the hostname of the request is expected and if it is not, redirect the browser to the proper hostname.

Ok, done with the formalities. Lets get to the code to make this work:



using System;
using System.Collections.Generic;
using System.Text;
using KodeIT.Web;

namespace FilterDotNet.Samples.DomainRedirect
{
public class Filter : IHttpFilter
{
internal const string SSO_DOMAIN = ".company.com";

void IHttpFilter.Init(IFilterEvents events)
{
events.PreProcHeaders += new EventHandler(OnPreProcHeaders);
}

void IHttpFilter.Dispose()
{
}

void OnPreProcHeaders(object sender, PreProcHeadersEventArgs e)
{
//
// If the host was empty, or not sent at all,
// let the request continue its path.
//
// If the host contains a dot (.) it may indicate
// an IP address was used (troubleshooting for ex)
// or a valid host header was used.
//

string hostName = e.Context.Headers[HttpHeader.Host];
if (String.IsNullOrEmpty(hostName) hostName.Contains("."))
{
return;
}

//
// If the request is being done locally on the box,
// let the request continue its path.
//

hostName = hostName.ToLower();
if (hostName.Equals("localhost") hostName.StartsWith("localhost:"))
{
return;
}

//
// Getting here means we do have to redirect this request
//

int colonIndex = hostName.IndexOf(':');
string portNumber = "80";
if (-1 != colonIndex)
{
portNumber = hostName.Substring(colonIndex + 1);
hostName = hostName.Substring(0, colonIndex);
}

string redirectResponse = String.Format(
"HTTP/1.1 301 Moved Permanently\r\n" +
"Connection: close\r\n" +
"Location: {0}://{1}{2}:{3}{4}\r\n" +
"\r\n",
e.Context.IsSecureConnection ? "https" : "http",
hostName,
SSO_DOMAIN,
portNumber,
e.Context.Url);

e.Context.WriteClient(ASCIIEncoding.ASCII.GetBytes(redirectResponse));
e.Context.TerminateRequest(false);
}
}
}


Its easy to see that the redirection only takes place if it was not made from the localhost and also if no IP address was used to reach the web server. Obviously, more can be done here, like injecting request headers with important information like the machine's real name, any environment variables you see useful, and finally setting the target domain as a configured element.

Thursday, August 30, 2007

Filter.NET samples available for download

All samples made until now are packaged with instructions and available for download in the release section. The samples are:
  • ServerHeaderChange - How to change the response server header
  • CustomAuthSession - How to manage the concept of session duration with Basic Authentication
  • TraceRequest - View all requests made to IIS
  • TraceResponse - View all responses sent from IIS
Get them here:
http://www.codeplex.com/filterdotnet/Release/ProjectReleases.aspx

Filter.NET Project Template for VS2005

Just to let you know there is a Visual Studio 2005 Project Template available for download.

Get it here:
http://www.codeplex.com/filterdotnet/Release/ProjectReleases.aspx?ReleaseId=6465

This project template will set up a class library inheriting from IHttpFilter with a reference to the required assembly KodeIT.Web.dll.

Hope you find it useful.

Wednesday, August 29, 2007

Samples: Basic Authentication with Session Duration

Basic Authentication is one of the authentication methods supported by any Web Server and by any browser. It should not be used in the clear, and normally its used in conjunction with a server certificate (SSL) to encrypt all information going in the wire.

When using Basic Authentication, the credentials you set on the first authentication dialog are sent on every single request until the TCP Session ends (the browser is closed). This is so you don't keep getting the dialog every time.

There are times, however, when you'd wish the credentials dialog was shown again by the browser after some period of idle time. You never know if the end user went to get a cup of coffee and left his workstation and his browser open, or you may have a very sensitive application that requires the user to validate himself every, say, 10 minutes.

While this can be acomplished 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.

What I'm about to describe next assumes the environment uses 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. Lets 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(OnPreProcHeaders);
events.Authentication += new EventHandler(OnAuthentication);
events.AccessDenied += new EventHandler(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.

Monday, August 27, 2007

Filter.NET Framework 1.0 release

The latest release of Filter.NET 1.0 is out, get it here:

http://www.codeplex.com/filterdotnet/Release/ProjectReleases.aspx?ReleaseId=6465

There are minor changes related to the registration the native filter with IIS, so this step can be fully automated.

http://www.codeplex.com/filterdotnet/Wiki/View.aspx?title=IisRegistration&referringTitle=Home

There also new attributes to the httpFilters tag to help on protecting the viewing of error details related to managed filters:

http://www.codeplex.com/filterdotnet/Wiki/View.aspx?title=Configuration&referringTitle=Home

I expect to find time to start publishing more enterprise oriented samples.
Let me know any issue you find. Enjoy!

Tiago Halm

Saturday, August 25, 2007

Class Library documentation complete!

I've just completed the documentation of the class library. Most of the content is taken from MSDN (as it should be, given this framework mirrors the behavior of IIS 5.x and IIS 6.0 regarding ISAPI Filters into the .NET managed environment).

To be most effective at understanding the implications and possible uses of a managed filter browse the documentation available on MSDN. Some content you should be familiar with can be found in:

Believe me these are some of the best places where to find the raw information about IIS and ISAPI Filters. In there you should be able to grasp the most native concepts and start applying them with a managed perspective to Filter.NET.

Feel free to post questions to myself as well :)

Friday, August 17, 2007

Samples: Trace IIS Requests and Responses

I'll be posting a series of samples on Filter.NET covering common uses one would give to an ISAPI Filter. Your comments and suggestions are welcome.

Trace Request

ISAPI filters have a series of events (11 to be exact), and these are all mapped into Filter.NET along with their properties. One such event is the native SF_NOTIFY_PREPROC_HEADERS which maps to the managed PreProcHeaders event.

The PreProcHeaders event is triggered by IIS when the HTTP headers have arrived (IIS reads TCP/IP packets until it sees a double CRLF), and allows an ISAPI Filter to view, change or remove headers. As such, this is an excellent opportunity for an ISAPI to trace the incoming request headers.

During this event, we not only have access to all request headers sent, but we also have access to the
server variables which IIS makes available containing much more useful information. ALL_RAW is one of them, with all headers sent in the request.

I think its time we give it a try then:




using System;
using System.Diagnostics;
using KodeIT.Web;

namespace FilterDotNet.Samples
{
class TraceRequest : IHttpFilter
{
void IHttpFilter.Init(IFilterEvents events)
{
events.PreProcHeaders += new EventHandler(OnPreProcHeaders);
}

void OnPreProcHeaders(object sender, PreProcHeadersEventArgs e)
{
RequestHeadersEvent context = e.Context;
string requestHeaders = String.Format("{0} {1} {2}\r\n{3}",
context.Method,
context.Url,
context.Version,
context.ServerVariables[ServerVariable.ALL_RAW]);

Trace.WriteLine(requestHeaders);
}

void IHttpFilter.Dispose() { }
}
}



Looks simple, right?

To install the sample, just register the assembly on Filter.NET configuration file - KodeIT.Web.dll.config - reset IIS and you're done. Open up
DebugView from Microsoft (formerly SysInternals), make a request to IIS and watch ...

Trace Response

No, I didn't forget about the http response :) Neither did IIS, and thats why there is an event useful for just that, to retrieve the full http response sent by IIS. The native event is SF_NOTIFY_SEND_RAW_DATA and the managed event is SendRawData.

When this event is triggered, IIS presents us with an array of bytes that will be sent to the client, and we can view and change those bytes. Right now, we're just interested in viewing them, so lets do exactly that:



using System;
using System.Diagnostics;
using System.Text;
using KodeIT.Web;

namespace FilterDotNet.Samples
{
class TraceResponse : IHttpFilter
{
void IHttpFilter.Init(IFilterEvents events)
{
events.SendRawData += new EventHandler(OnSendRawData);
}

void OnSendRawData(object sender, RawDataEventArgs e)
{
Trace.WriteLine(ASCIIEncoding.ASCII.GetString(e.Context.GetData()));
}

void IHttpFilter.Dispose() { }
}
}



This can also be part of the first managed filter instead of being a separate one. Again, compile it, register it, open DebugView, make a request to IIS and watch the responses sent by IIS.

Avoiding CLR versioning issues

Since Filter.NET is a framework dependent on .NET 2.0, you may run into CLR versioning issues if any of the AppPools contain virtual directories configured with ASP.NET v1.1. While this is unavoidable in IIS 5.x when subscribing to SF_NOTIFY_READ_RAW_DATA, it can easily be overcome in all other cases.

As you may know (or not know) ISAPI filters can be installed at global scope or site scope. Global scope means that every single request gets filtered by the ISAPI, while site scope means that only requests for that specific site are the ones filtered. ISAPI Filters that subscribe to read the raw data (only available in IIS 5.x) are required to be installed at global (or server) scope. All other filters can be installed at site scope.

But we're talking about CLR versioning issues, like having ASP.NET v1.1 and ASP.NET v2.0 running on the same process. What does this have to do with sites?

Just make sure you organize your IIS sites (and virtual directories beneath it) by ASP.NET version. Then, organize and assign the AppPools accordingly. Having done that, and to avoid CLR versioning issues, you can safely install Filter.NET at the site level on the sites that have ASP.NET v2.0 (and ASP, PHP, CGI, PERL, ect...)

Thursday, August 16, 2007

Filter.NET (v0.9 beta) is out

Finally, I was able to publish the current beta version of Filter.NET.

What is Filter.NET?

Filter.NET is a framework designed to expose the ISAPI Filters API to .NET while maintaining the power, flexibility and efficiency of the IIS platform. (official version)

Basically, its my attempt to allow .NET developers to create ISAPI filters in IIS 5.x and IIS 6.0. After performing some stress testing and looking at cost/benefit given by the garbage collected language along with its ease of programming, I believe it was a good choice.

However, its for you to decide. Do check it out and let me know how it goes.
I'll be posting more info here about it.