by Jim Karabatsos - GUI Computing
Recently, we had a need to replace a Unix-based web server with IIS running on NT. As the webs hosted by the server were predominantly HTML documents anyway, moving the server was going to be a pretty straightforward affair.
It turns out one thing that the Unix server was doing was redirecting some sub-webs to cgi scripts, which were then redirecting to entire new domain names after displaying a "please update your bookmarks" page for a while. As an example, consider this e-zine. Before it moved to its own domain (www.avdf.com) it was referenced as a sub-web off our corporate web site (as www.gui.com.au/avdf). When we moved it, we wanted to be sure that anyone following a link or a bookmark to the old URL would find the target file in the new web.
What is needed is the ability to say have the web server look for URL starting with a particular path, and map all those URLs to some server-side logic that works out where it should go. In order for that to be possible, the full URL originally requested needs to be passed to that logic in some fashion.
We thought that would be pretty straightforward. Surely IIS was capable of doing this. This must be a fairly common requirement in the ever-fluid world or web publishing.
Well, if there is a way to do it, I couldn't find it. I am getting really annoyed at the amount of time I find myself spending trying to find information that I need. The current state of documentation from most of the major vendors is woeful. I really hate Microsoft's current documentation structure which, roughly speaking, consists of a "getting started" document and an alphabetic reference. Both those documents are necessary, but we also need to get from the people who created the software an idea of how they expected certain features would be used to do useful things.
Anyway, as I step down from my soapbox, after spending too much time using the help system and browsing MSDN, I gave up and decided to write an ISAPI filter that does what we needed. The entire source code is provided so you can use it and adapt it for your own purposes.
This is not the first ISAPI filter that I have developed. It is not even the first one that I have presented in AVDF. Rather than waste time covering existing ground, I will refer you to the URLVersion filter previously published for the background information and we will just move on to discuss the specifics of this filter. That's a real benefit of using on-line publishing - I can be sure that you can get access to the old article.
Like URLVersion, URLMapper is written in Delphi. I actually used Delphi 3 for the latter but that's just because I had Delphi 3 installed - there are no differences between 2 and 3 that impact on the code in any way. This is because an ISAPI filter is by definition a fairly low-level bit of code. If you have to ask why I didn't use C++ then welcome to your first issue of AVDF and may I suggest you check out some of my other articles too.
URLVersion was already redirecting the server whenever it found a newer version of a requested document in one of the update directories. My initial approach was to use the same mechanism to redirect. Indeed, it should be even simpler because I don't need to look for updates at all, just redirect any URL that started with a given string.
I actually coded up a version that did just that. When it received the SF_NOTIFY_URL_MAP notification callback from IIS, it checked the URL and, if it was one we were interested in, it changed the mapping to point to a particular ASP page. Needless to say, nothing is ever that easy. It turns out that, by the time IIS notifies any loaded filters that it has mapped a URL to a file and gives them a chance to change the mapping, it has already made up its mind about what type of file it is. It will process the file that way even if the ISAPI filter changes the mapping to another file type altogether. This is even documented behaviour (I found out later). Which genius designed that feature? Wanna bet this is just a bug that was fixed using the time-honoured technique of documenting it as expected behaviour?
Anyway, I then started looking at the alternative notifications. Without any useful documentation, I just had to go on intuition. I came across the SF_NOTIFY_PREPROC_HEADERS notification and thought I would try it. After some experimentation to get and log the URL during the callback so I knew what I was looking for, I determined that I could indeed do what I wanted in that callback.
Here's how it works. At the time that the SF_NOTIFY_PREPROC_HEADERS is sent, IIS makes available to the ISAPI filter the URL. This URL is a string containing everything from the first slash after the domain name (including that slash). So if a browser asked for "http://www.gui.com.au/avdf/jan97/something.html", the URL that the ISAPI filter would see would be "/avdf/jan97/something.html". What we can then do is change that to an ASP page, passing the old URL as a parameter. We could, for example, change the URL to the string "/scripts/redirector.asp?URL=avdf/jan97/something.html" and the ASP could then dynamically create a page with a message and an automatic transfer to the new URL.
Getting and setting the URL require a call through a function pointer, but Delphi makes that kind of thing at least as easy as C++, so that is not a problem. Take a look at the GetURL and SetURL procedures in the code to see how it is done if you have not seen it before.
I decided that I would make the tool as generic as possible within the constraints of the time I had available. I did not hard-code any paths in the filter; instead, they are read from the registry when the filter is loaded by IIS. The filter actually allows for up to 100 mappings, where each mapping is a pair of URLs, an old one and a new one. For the mapping above, we would define the keys:
OldPath1 = "/avdf/" NewPath1="/scripts/redirector.asp?URL=/avdf/"
That way, the filter does not actually need to redirect to an ASP at all. Note that the portion of the old URL that was after the OldPath is always tacked onto the end of the NewPath. With more time, I would probably have opted to have the NewPath contain embedded escape sequences that could be replaced with the various components of the OldPath, but I just could not justify the time.
I'll leave you to look at the code to see how it is done. If you have not seen an ISAPI filter before, then you will definitely want to refer to the article describing URLVersion to get an understanding of the anatomy of these beasts. Otherwise, it is all fairly straightforward code. The tricky part, as always, is deciding where to hook into the event stream.
The one question that you may have (if you know Delphi) is why I elected to use an array rather than a list object. Using an array means that I have set myself a fixed upper limit of 100 mappings that I support, whereas using a dynamic data structure (either a list or a home-grown vector) would allow the size to remain variable.
The answer is performance. Remember that the code will be called every time the headers of the HTTP request are processed. Every time, the ISAPI filter will need to iterate the mappings and decide if it is interested or not. We want to make sure that we do that as quickly as possible, so using a static array is the way to go. Sometimes, efficiency really is a design priority. It is for the same reason that I create a parallel array of lower-case OldPaths and set it up during GetFilterVersion so that I only need to convert the URL to lower-case for comparisons.
There is also one thing you need to be aware of if you decide to use this filter or a derivative of it. The filter will examine each mapping in numeric order and, as soon as a match is detected, it is processed without examining other mappings. This means that a mapping at a higher level will override a mapping at a lower level if it appears first in the list of mappings. For example, let's assume that the mappings contain the following keys:
OldPath1 = "/avdf/" NewPath1 = "/scripts/redirector.asp?URL=/avdf/" OldPath2 = "/avdf/jan97" NewPath2 = "/scripts/redirJan97.asp?URL=/avdf/"
In this case, the second mapping would never be matched because OldPath1 matches the first part of OldPath2. I thought about this and decided to leave it alone. As it is, the IIS administrator has total control over the order mappings are processed. I am not going to try to second-guess an experienced user. If she really wants those two mappings, then she will have to swap their order.
If you decide you want the lowest-level mappings processed first regardless of the order that they appear in the mappings list in the registry, just sort the global array of old paths in descending order of length. Take care to keep the other arrays in sync with it as you sort. Do this in the GetFilterVersion processing so it is done just once. Personally, I think that this sort of tool wants to make as few assumptions about what the user wants as possible. The source is available for download.
Editor's Note : Another, very similar, ISAPI filter, based on this technique, is documented in the article Of IIS Logging, Caching and ISAPI filters in this issue.