Image of Navigational Map linked to Home / Contents / Search Writing an ISAPI Filter DLL

by Jim Karabatsos - GUI Computing
Image of Line Break

Despite what you may have heard, ISAPI is not quite dead yet. While Microsoft's new Active Server Pages represent a terrific advance in programming non-static Web pages, there are some things that are really cool about ISAPI that you might be interested in.

There are actually two types of ISAPI DLLs that you can write. By far the most common are ISAPI extensions. These are DLLs that perform much the same function that was previously performed by CGI scripts, namely the creation of HTML pages on demand based on whatever criteria are required, whether that be by looking up information in a database, by responding to the particular user or just based on environmental factors like the time of day. We are not going to look at these sorts of ISAPI DLLs in this article - they are actually pretty simple to write once you get a basic grasp of the fundamentals and you can even use VB4 or later provided you use Microsoft's OLE ISAPI wrapper. Be aware, however, that the sort of things you might want to do using ISAPI extension DLLs are probably easier to do using Active Server Pages.

The other sort of ISAPI DLL that you can create is an ISAPI Filter and this is the type that we will be focusing on in this article. Unlike an ISAPI Extension whose job is primarily to generate HTML pages, an ISAPI Filter's job is to modify the operation of the Web Server itself. At several points in the servicing of a request, the server will call into each ISAPI Filter DLL and give it an opportunity to intervene in the processing of that request. The Filter can totally override the various actions and generally do a whole lot of cool stuff.

It is always easier to explain this sort of thing using a real-world example. Recently, we encountered a need to provide a whole lot of HTML content on a CD-ROM. This content is placed in the CD-ROM drive of each stand-alone server and NT's Personal Web Server is used to publish that information to the other workstations on the LAN. Note that these are small LANs that do not connect with each other nor with the Internet.

This is actually quite easy to do. However, if just a single page of the many thousands of pages on the CD-ROM needs to change, an entirely new CD-ROM needs to be cut and distributed. This gets quite expensive when a single page might need to change every week but most of the content remains quite static.

What we really need is the ability to have the server check for an updated version of any given page before serving up the version from the CD-ROM. That way, we could just place the updated pages somewhere on the server's disk drive and the server would find and use those files first.

Easier said than done. IIS (Internet Information Server) and PWS (Personal Work Server), like most Web servers, only support a single Web root directory. We want the server to resolve the URL into a local path on the server as usual - in our scenario this would be a path on the CD-ROM. However, before actually sending that file back to the browser, we want it to look somewhere else first to see if a newer version of that file exists. Enter the ISAPI Filter. Our URLVERSION DLL does exactly this: it will remap any file to a newer version of that file if it exists in a separate virtual root directory.

Enough background - it is now time to dive into some source code. An ISAPI filter is nothing more than a DLL so any language that can create real DLLs can be used to write one. Unfortunately, this does NOT include Visual Basic because it can only create OLE DLLs - errr, ActiveX DLLs. So of course I used C++.

NOT.

It should come as no surprise to anyone who has read any of my previous columns that I used Delphi to write the DLL. I will not go into the reasons for that choice in this article; one advantage, however, is that there are very few files involved in a Delphi DLL project so it is much easier to present in an article. I could have used just the one file (the source code) but opted to split it into two so that the common declarations that would be used by all ISPAI filters could be placed in a separate unit to promote easier re-use in future projects. The main contents of that file came from the Web -- check out http://www.genusa.com/isapi/isapisrc.html for a whole lot of useful source in both Delphi and C++. I won't examine the contents of that file in this article as you have the source and it is basically a collection of various constant and record definitions. I did add a simple function to log a string to a text file for use in debugging. Delphi's smart linker will pull that out of any DLL that does not call that function.

To start writing the filter, we simply use the "library" keyword instead of the "program" keyword. That's all Delphi needs to tell it to create a DLL. No DEF and IMP files to muck around with. We start by "using" a few units :

   library URLVersion;
 
   uses
     SysUtils,
     Windows,
     Registry,
     ISAPIFilters in 'ISAPIFilters.pas';

SysUtils, Windows and Registry are units provided by Borland as part of Delphi. SysUtils gives us some extended string support, Windows is the Delphi equivalent to windows.h and Registry gives us the TRegistry object that greatly simplifies access to (what else?) the registry. ISAPIFilters is the unit of ISAPI definitions that I mentioned before.

When IIS or PWS first loads an ISAPI Filter, it calls the DLL entry point GetFilterVersion. This is a chance for the DLL to initialise itself and to notify the web server of its version number. In our filter, we will also query the registry to look up the "virtual" root directories at this time and store them in a global array. As always, we try to generalise the solution so we will actually allow up to sixteen virtual roots that are searched in order for a matching file. This actually allows for a versioning mechanism to be built on top of this filter, but we won't go down that road now.

First, we define the globals :

   const MAXPATHS = 16;

   var Paths : array[1..MAXPATHS] of string;
       NumPaths : integer;
       WWWRoot : string;
       lenWWWRoot : integer;

Then we define the function :

   //-------------------------------------------------------------------
   // GetFilterVersion - Required DLL function as per ISAPI Spec
   //                  - also used to read registry to get path info
   //-------------------------------------------------------------------
   function GetFilterVersion(var pVer : HTTP_FILTER_VERSION) : BOOL; export; stdcall;
   var sPaths  : string;
       iPos, i : integer;
   begin
     try
       pVer.dwFilterVersion := MAKELONG(0, 1);
       StrPCopy(pVer.lpszFilterDesc, 'URL Version - ISAPI Filter');
       pVer.dwFlags := (    SF_NOTIFY_SECURE_PORT
                         or SF_NOTIFY_NONSECURE_PORT
                         or SF_NOTIFY_URL_MAP
                         or SF_NOTIFY_ORDER_DEFAULT
                       );

Note how we are passed a pointer to an HTTP_FILTER_VERSION structure or record. We fill in the version information, the description and a set of flags to indicate that we want to be in the loop for both secure and insecure connections, we want to be involved in mapping URLs and that we will just accept the default for where we come in the calling order compared to other filters.

Now it is time to hit the registry :

       with TRegistry.Create do try
         RootKey := HKey_Local_Machine;
         if OpenKey('System\CurrentControlSet\Services\W3SVC\Parameters\Virtual 
	 Roots',
                    True) then begin
            if ValueExists('/') then begin
               WWWRoot := ReadString('/');
               iPos := Pos(',', WWWRoot);
               If iPos > 0 then
                  WWWRoot := Copy(WWWRoot, 1, iPos - 1);
               LenWWWRoot := Length(WWWRoot);
               end
            else begin
               WWWRoot := '';
               LenWWWRoot := 0;
               Result := False;
               Exit;
               end
            end
       finally free end;

The code above uses a temporary TRegistry object to look up the path to the server's virtual root. We look up the appropriate registry entry and store the path in the global variable WWWRoot and it's length in LenWWWRoot. Remember that the DLL is going to be called every time a URL is mapped to a path, so we want it to be as efficient as possible. We can calculate the length once now and just refer to it in future -- after all, the root will not change while the server is running.

Note the construct "with TXXX.Create do try ... finally free end;" which gives us an implicit object to work with and protects all the code that uses that object with a try .. finally block to ensure that the object is properly destroyed when we are done. This is a very useful construct in Delphi.

After doing this, we look elsewhere in the registry for the "virtual" roots that we will use. We will place these under HKLM\Software\GUI\URLFilter as a key "Paths" with a string value that contains all the virtual roots in order separated by semicolons (similar to the Path variable in DOS). We strip them apart into separate elements in the global string array "Paths" and we remember how many paths we found in the global variable NumPaths.

       NumPaths := 0;
  
       with TRegistry.Create do try
         RootKey := HKey_Local_Machine;
         If OpenKey('Software\GUI\URLFilter', True) then begin
            If ValueExists('Paths') then begin
               sPaths := ReadString('Paths');
               iPos := Pos(';', sPaths);
               while (iPos <> 0) do begin
                  Inc(NumPaths);
                  if NumPaths > MAXPATHS then begin
                    LogInfo('Too many paths - extra ignored');
                    NumPaths := MaxPaths;
                    Break;
                    end;
                  Paths[NumPaths] := Copy(sPaths,1,iPos - 1);
                  sPaths := Copy(sPaths, iPos + 1, Length(sPaths) - iPos);
                  iPos := Pos(';', sPaths);
                  end; // while
               if (Length(sPaths) > 0) And (NumPaths < MAXPATHS) then begin
                  Inc(NumPaths);
                  Paths[NumPaths] := sPaths;
                  end; // if Length(sPaths) > 0
               end // if ValueExists
            else begin
               result := False;
               Exit;
               end // else
       finally free end;

       If NumPaths > 0 then
         result := True
       else
         result := False;

     except
       result := False;
     end;
   end;

OK, that does it for the GetFilterVersion function. We don't have to be too fussed about performance in this one because it is only called once when the filter is loaded. The other required function that must be exported by an ISAPI Filter DLL is HTTPFilterProc and, because it is called very often, we must make sure that it executes quickly. Here it is:

   //-------------------------------------------------------------------
   // HTTPFilterProc   - Required DLL function as per ISAPI Spec
   //                  - This is where the work is done
   //-------------------------------------------------------------------
   function HttpFilterProc( var pfc          : HTTP_FILTER_CONTEXT;
                            NotificationType : DWORD;
                            pvNotification   : LPVOID)
                            : DWORD; export; stdcall;
   var
     pvHTTP_FILTER_URL_MAP : HTTP_FILTER_URL_MAP;
     Buffer                : array[0..1023] of char;
     BuffSize              : DWORD;
     HisAddress            : string;

   // ======== local procedures with access to scope variables =========
   
   //-------------------------------------------------------------------
   // MassageURL - change the URL if a newer version exists
   //-------------------------------------------------------------------
   function MassageURL(var URLPath : string) : boolean;
   var LoopCounter : integer;
       URLBranch   : string;

   begin

     Result := False;

     URLBranch := Copy(URLPath, lenWWWRoot + 1, Length(URLPath) - LenWWWRoot);

     for LoopCounter := 1 to NumPaths do begin
       if FileExists(Paths[LoopCounter] + URLBranch) then begin
         URLPath := Paths[LoopCounter] + URLBranch;
         Result := True;
         Exit;
         end; 
       end;
   
   end {function MassageURL};
 
   //-------------------------------------------------------------------
   // OnURLMap - Process SF_NOTIFY_URL_MAP notifications
   //-------------------------------------------------------------------
   function OnUrlMap : DWORD;
   var sURLPath : string;
   begin
     try
       // cast for convenience
       pvHTTP_FILTER_URL_MAP := HTTP_FILTER_URL_MAP(pvNotification^); 
       sURLPath := pvHTTP_FILTER_URL_MAP.pszPhysicalPath; 
       // convert to Pascal string
   
       if MassageURL(sURLPath) then
          StrLCopy(pvHTTP_FILTER_URL_MAP.pszPhysicalPath,
                   PChar(sURLPath),
                   pvHTTP_FILTER_URL_MAP.cbPathBuff);
  
       Result := SF_STATUS_REQ_NEXT_NOTIFICATION;
 
     except
       Result := SF_STATUS_REQ_ERROR;
     end;

    end {function OnUrlMap};

   // -------- HttpFilterProc mainline code ----------------------------

   begin

     case NotificationType of
       SF_NOTIFY_URL_MAP : Result := OnUrlMap;
  
     else
       Result := SF_STATUS_REQ_NEXT_NOTIFICATION;

     end;

   end;

It's easier to start reading this at the bottom. The final begin .. end pair is the main code for the function. It consists of a simple case statement that checks whether the NotificationType parameter is SF_NOTIFY_URL_MAP, which is the only one that we are interested in. If not, it simply exits with a return code of "SF_STATUS_REQ_NEXT_NOTIFICATION" which tells the server to continue processing and to give other filters a chance to process the same notification. On the other hand, if it is the notification we are looking for, we call OnUrlMap and return to the server whatever that function returns.

Note that OnUrlMap is a local function, one that is defined inside the HTTPFilterProc function. Neither C/C++ nor Visual Basic allow this sort of construct and it is not something that you use all that often. When you need it, however, it is very useful indeed. In our code, using local functions provides two very important benefits.

Firstly, because OnUrlmap (and MassageURL) are defined inside HTTPFilterProc, they have access to all the local variables defined in that function without needing to create pass them on the stack (which might necessitate the creation of a stack frame and the associated overhead). More importantly, we can structure our code for easy readability into separate functions while maintaining thread safety because all shared variables that might change are in the entry point function's stack frame. If you have ever had to manage thread local storage, you know what I mean. If you haven't, well, stick to Delphi <g>.

OnUrlMap and MassageURL are pretty straight-forward functions. MassageURL is passed a string by reference. It removes the WWWRoot from it and then loops through all the virtual roots, testing them with the remainder of the string to see if such a file exists. If it finds one, it modifies the string that was passed to it and returns true; otherwise it returns false.

OnUrlMap calls MassageURL passing it the filename to which the server has mapped the URL. If MassageURL returns true, it places the new mapping into the notification structure passed by the server. Notice that OnUrlMap is protected by a try .. except block. This uses the operating system-supported structured exception handling to ensure that if anything goes wrong, the server will not be brought down. Indeed, even though I made some really silly mistakes during development of this DLL, I never once managed to hurt the server because most of my code was inside a try .. except block. Notice that MassageURL does not need its own exception handler because it is only ever called from within OnUrlMap and so is protected by that handler. Note also that the main-line code in the function is not protected by any exception handler; this is because nothing can go wrong there and so there is no need to incur the overhead of creating an exception handler until we know we actually need to do some work.

Now all that remains is to export the two required functions by name from the DLL :

   exports
     HttpFilterProc,
     GetFilterVersion;

   end.

That's it. The entire source to the DLL, isapifilters.zip, (which you might prefer as a single file without the article commentary and is available on our web site. It contains the debugging code so you can follow what is going on. You can also download URLVersion.dpr.



Written by: Jim Karabatsos
March '97

Image of Arrow linked to Next Article
Image of Line Break
[HOME] [TABLE OF CONTENTS] [SEARCH]