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.