Monday, September 24, 2007
CodeProject Article
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
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
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
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
- 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
http://www.codeplex.com/filterdotnet/Release/ProjectReleases.aspx
Filter.NET Project Template for VS2005
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
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!
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:
- Newsgroups
http://groups.google.pt/group/microsoft.public.platformsdk.internet.server.isapi-dev/topics
- David Wang's blogs (although covering most native aspects of IIS, it does give you a thorough understanding of IIS behavior to the managed filter author)
http://w3-4u.blogspot.com/
http://blogs.msdn.com/david.wang/
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
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
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
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.