This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


April 1998

Microsoft Systems Journal Homepage

Take IIS Customization to the Next Level by Writing ISAPI Filters and Script Interpreters

Download IIScode.exe (57KB)

Leon Braginski is an engineer at Microsoft in the Developer Support Internet Unit. He is a coauthor of several Internet-related articles. Matt Powell also works for Microsoft in the Developer Support Internet Unit, where he specializes in Internet server technologies. This article is adapted from the book Running Microsoft® Internet Information Server by Leon Braginski and Matt Powell to be published by Microsoft Press in Spring 1998.

ISAPI extensions are a very powerful part of your programming toolbox. Because ISAPI extensions run on a relatively low level of the Overall Internet Information Server (IIS) architecture, they can accomplish a wider variety of tasks more efficiently than components such as Active Server Pages (ASP) applications. ISAPI extensions have been a good choice for performing many more advanced tasks, but there are a couple of other options that provide even greater functionality.
      Although you may have read about all the various options and services available for configuring IIS, this article takes IIS customization to the next level. We'll explore writing ISAPI filters—DLLs that let you modify server request processing. We'll also discuss how to create your own scripting capabilities by writing a server-side script interpreter (also known as a script processor). You may have already used script interpreters that come with IIS such as ASP, Server Side Includes (SSI), and the Internet Database Connector (IDC).

ISAPI Filter Architecture
      Even though the logic of a Web client's interaction with the server is not too complex (how hard can it be to receive a request and send a reply back?), to effectively process a client's request, the server must do a lot of work. An ISAPI filter is one way to change the default behavior of IIS and affect how the HTTP requests are handled. In effect, you write your own building block and plug it into the IIS framework.
      The processing of each client's request can be separated into multiple steps.

  1. IIS reads the raw request data.
  2. IIS processes the HTTP request headers.
  3. IIS maps the URL in the form of http://server/document.htm to a physical path on the machine like d:/inetpub/wwwroot/document.htm.
  4. The request may include user name and password information that requires authentication by IIS.
  5. If the client sends POST data with its request, the server starts to read the data in chunks.
  6. IIS and potentially a server application send HTTP headers to the client.
  7. The server sends the response data to the client.
  8. The server ends processing of the request. Note that the session still may be kept open.
  9. The server writes data about the request to the log.
  10. The server session is closed.
      With ISAPI filters, you can write code that will be executed by the server for each step described above. If you don't like how IIS maps a requested URL to a physical drive location, you can write a filter to change how the URL is mapped. Want to redirect a client request to different documents depending on the browser being used by the client? No problem. The best thing about filters is that they are completely transparent for higher-level IIS applications. ASP applications, ISAPI extensions, and anything else invoked on the server side won't even know the filter was involved.

Filter Implementation
      Implemented as DLLs, filters are registered with IIS and loaded into its process space upon server or site startup. You register filters with IIS using the Management Console's ISAPI Filters property page. Once the filter is loaded it tells IIS which steps in the server request processing it needs to be notified of. So, if you want to modify the default action performed by the Web server when a URL is mapped to a file, the filter would need to register its interest in receiving URL mapping notifications.
      Since a filter can request to receive a number of different notifications, IIS informs the filter DLL what step is currently taking place in the request processing. It does so by calling one of the functions in the filter with a specific notification value. Each step described in the filter architecture has a value associated with it. the Se values and filter-related structures are defined in the HTTPFILT.H header file distributed with Visual C++ or the Win32® Platform SDK.
      The fundamental difference between ISAPI extensions and ISAPI filters is that extensions are loaded and called only when requested. Even though ISAPI extensions are cached, they are eventually unloaded. ISAPI filter DLLs are loaded by the server at startup and stay in memory until the server or site is shut down. Filters are called for each request that arrives at the server, while extensions are called only when they are asked to be called. For most notifications, if the server receives 1000 requests from 1000 different clients, the filter DLL will be called 1000 times. It's easy to see that a poorly written filter may have disastrous consequences. It may either bring IIS down or slow it to an unusable speed.
      A filter DLL has to export two entry points: GetFilterVersion and HttpFilterProc. The first function provides filter information to the server and supplies a list of events that the filter will be handling. It is called only once per session—when the server or site starts. Because more than one filter can be registered with IIS at any time, you specify a priority level for your filter. Filters with a higher priority have the first shot to handle the notifications. If filters have the same priority, they're loaded in the Order they appear in the Management Console ISAPI Filters property page.
      HttpFilterProc is the main function in an ISAPI filter. For each event that a filter registers its interest in, HttpFilterProc is called with the notification type and the notification data structure. In the case of URL map notifications, the data structure includes information on the URL and its physical path.
      A filter may also provide an optional entry point called TerminateFilter that will be called when the filter is being unloaded. This will only happen when IIS or the Web site is stopped. And, of course, there is another entry point that may be useful for your filter, because it's a DLL after all. Often you can use DllMain to do some initialization and cleanup. the Operating system calls this optional function to notify it of attaching and detaching processes, as well as attaching and detaching threads.
      If you search the Win32 Platform SDK documentation you won't find DllMain right away. You will find the Win32 function DllEntryPoint. DllMain is the default name for the DLL entry/exit function if you use the Microsoft linker. The linker embeds the address of the default entry/exit function in the DLL image. When you use the /DLL flag to tell the linker that the target is a DLL, the function entry point is _DllMainCRTStartup. This function is part of the Visual C++ runtime; after it performs some initializations, it calls DllMain. Other 32-bit C/C++ development suites may have a function with a different name.
      Now let's look at the different functions that an ISAPI filter exports.
       GetFilterVersion The GetFilterVersion function sets the filter priority and registers the filter for the event notifications of interest. In the following code (from our sample filter) we set a high filter priority by using SF_NOTIFY_ ORDER_HIGH. This ensures that when multiple filters are loaded, our filter is handled before low priority filters. The notification we are interested in handling is SF_ NOTIFY_URL_MAP, which is triggered by IIS when it maps a URL to its physical location. GetFilterVersion also registers a short description with the server and indicates whether the filter wants notifications for secure, encoded connections over SSL links, nonencoded connections, or both. Our sample filter's description string is "Smart Redir Filter." It will work for connections coming to secure ports (indicated by SF_NOTIFY_SECURE_PORT) and to nonsecure ports (SF_NOTIFY_NONSECURE_PORT).

 BOOL WINAPI GetFilterVersion (PHTTP_FILTER_VERSION pFilterVersion)
 {
    pFilterVersion->dwFilterVersion = HTTP_FILTER_REVISION;
    strcpy (pFilterVersion->lpszFilterDesc, "Smart Redir Filter");
    pFilterVersion->dwFlags= (SF_NOTIFY_ORDER_HIGH     | 
                              SF_NOTIFY_SECURE_PORT    |
                              SF_NOTIFY_NONSECURE_PORT |
                              SF_NOTIFY_URL_MAP);
   return TRUE;
 }
      GetFilterVersion is called only once when the filter is loaded by IIS. If we were going to do any initialization, GetFilterVersion would be a good place to do it. The next step is to add the code to the filter that does the real work.
      HttpFilterProc This is the core function of an ISAPI filter DLL. HttpFilterProc is called by the server with the first parameter being a pointer to an HTTP_FILTER_CONTEXT structure.

  DWORD WINAPI HttpFilterProc ( PHTTP_FILTER_CONTEXT pfc, 
      DWORD notificationType, LPVOID pvNotification );
      The HTTP_FILTER_CONTEXT structure is unique for each request. It not only carries request identification information, it also carries pointers to other functions that may be used in your filter.

 typedef struct _HTTP_FILTER_CONTEXT
 { 
   DWORD cbSize;
   DWORD Revision;
   PVOID ServerContext;
   DWORD ulReserved;
   BOOL fIsSecurePort;
   PVOID pFilterContext;
   BOOL (WINAPI * GetServerVariable);
   BOOL (WINAPI * AddResponseHeaders);
   BOOL (WINAPI * WriteClient);
   VOID * (WINAPI * AllocMem);
   BOOL (WINAPI * ServerSupportFunction);
 } HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
GetServerVariable, AddResponseHeaders, WriteClient, AllocMem, and ServerSupportFunction are pointers to the corresponding functions implemented by IIS. The functions the Mselves are not implemented in the filter. If you're going to call a function residing in the server code, you must have a pointer to the function. That's what the HTTP_ FILTER_CONTEXT structure is for—returning pointers to the functions that can be used in the filter code. the Se functions are also called callback functions, and are used by the filter to communicate with IIS.
      The second and third parameters to the HttpFilterProc function are the notification type and a pointer to a data structure that is specific to the notification. Notification-specific structures may contain a combination of data (such as the requested URL for a URL map notification) and pointers to other functions that may only make sense for a specific notification. For example, here is the structure that is received by the filter when IIS is finished preprocessing an HTTP request's headers:

 typedef struct _HTTP_FILTER_PREPROC_HEADERS
 {
     BOOL (WINAPI * Gethe Ader);
     BOOL (WINAPI * Sethe Ader);
     BOOL (WINAPI * AddHeader);
     DWORD HttpStatus;
     DWORD dwReserved;
 } HTTP_FILTER_PREPROC_HEADERS, *PHTTP_FILTER_PREPROC_HEADERS;
Note that Gethe Ader, Sethe Ader, and AddHeader are pointers to functions specific to the HTTP header manipulation and so only really makes sense for preprocessing HTTP request and response headers.
      Figure 1 summarizes the notification types and the corresponding data structures for each notification received by ISAPI filter DLLs. Once again, all the structures and notifications are declared in the HTTPFILT.H header file.
      Once HttpFilterProc has been called with certain notifications and data and the filter has had a chance to process the information, your filter needs to return from HttpFilterProc. Do this as quickly as possible because IIS cannot do anything for the request while the filter DLL is handling a notification. That's why filters should be as efficient as possible. The server expects HttpFilterProc to return a DWORD. The return value tells the server what to do next with the client's connection and the client's request. Figure 2 lists the return values and their meanings.
      Even though the sequence of events in the HttpFilterProc may seem straightforward, it isn't for all requests. When a server uses the NTLM authentication scheme, a client and server typically send three requests/responses to each other to authenticate the client. The filter will be called for each request. Even a simple SF_NOTIFY_URL_MAP notification may be posted more than once for the same request. This happens when a client invokes an ISAPI extension that calls the ServerSupportFunction with the SE_REQ_ MAP_URL_TO_PATH parameter to map a URL location to its physical path.
      TerminateFilter This is an optional entry point that gets called by the server before the server is about to be unloaded from memory. Here is the prototype:

 DWORD WINAPI TerminateFilter( DWORD dwFlags )
In the current version of IIS neither the dwFlags parameter nor the return value is used. Hence, you can ignore dwFlag and just return 1 from the function and everything will work fine.

The SmartRedir Filter Sample
      It's time to take a look at a real filter. It is very simple to write a filter that permits or denies access to a specific directory based on the browser type. Say you have a situation in which your Web server has a directory called ieonly. All documents in this directory use features of Microsoft® Internet Explorer 4.0 or 4.01, like Dynamic HTML. Because no other types of browsers can properly show the pages in the ieonly directory, they should not be able to access the directory or its files.
      Our filter monitors the URLs requested by the clients, and if the "ieonly" string is present anywhere in the URL, our SmartRedir filter will check the User-Agent header to get the description of the client's browser. If the browser is not Internet Explorer 4.0 or 4.01 and the request is for files in the ieonly directory, the filter does not allow the browser to access the requested document. Instead it redirects the browser to the GETIE.HTM file, which explains that the user must get the latest version of Internet Explorer to access this resource.
      But why bother writing a filter? If the ieonly directory contains only ASP files, we could add VBScript or JScript code in each file to check the User-Agent and redirect browsers other than Internet Explorer 4.0. This solution wouldn't work for static HTML files and image files that don't have any script that executes when they are processed. Converting current HTML files to ASP files may not be practical and would still not do anything for our image files. A redirection filter fits the task perfectly because it will work for any kind of file in the ieonly directory.
      You've already seen the sample's GetFilterVersion function that registers the notifications we're interested in and sets the priority and description of the filter. Figure 3 shows the heart of the filter, the HttpFilterProc function. The first thing we do is verify that it only handles SF_NOTIFY_ URL_MAP notifications. This is actually redundant code because the filter registered interest only for the SF_ NOTIFY_URL_MAP notification. But we wanted to illustrate how you would go about handling multiple notification types in the same filter by checking notification type parameters. If SmartRedir were to see a notification it was not looking for it would return quickly.
      The next step is to determine what URL the client has requested. The filter receives the URL via the pszURL member of the HTTP_FILTER_URL_MAP structure. The correct procedure would be to carefully scan the entire URL to see if there is an ieonly subdirectory, as in the following URL: http://server/ieonly/subdir/hello.htm. You'll want to scan carefully because you don't want to get confused over a URL like http://server/wrong_ieonly_url/hello.htm that shouldn't trigger the filter. For illustrative purposes, we went the short route, just scanning the URL string for an "ieonly" substring. To avoid problems with mixed-case strings, we converted the URL to lowercase and searched for the lowercase substring "ieonly." If the "ieonly" substring does not exist in the requested URL, then the SmartRedir filter just returns with SF_STATUS_REQ_NEXT_NOTIFICATION, allowing other filters (if they are installed) to handle the request.

Checking the Browser Software
      If the URL contains the "ieonly" string, the next step is to determine what software the client is using. We need to prevent browsers other than Internet Explorer 4.0 and 4.01 from accessing any resources in the ieonly subdirectory. The client's browser sends the User-Agent header that identifies the browser. If the filter used the SF_NOTIFY_ PREPROC_HEADERS notification, we could just use the Gethe Ader callback function, exposed via the HTTP_ FILTER_URL_MAP structure, to get the User-Agent value. SmartRedir handles the URL map notification that comes after preprocessing headers. By this time, the server has had a chance to get each header and set the corresponding server variable, so the next step is to get the value of the HTTP_USER_AGENT variable. This variable is a string; the GetServerVariable callback function exposed via the HTTP_FILTER_CONTEXT structure will do the job. As always, there is a catch: we don't know ahead of time how large this agent string will be. It can be as short as a few characters with some browsers, or as long as the string


 Mozilla/4.0 (compatible; MSIE 4.01; Windows NT)
sent by Internet Explorer 4.01. To dynamically allocate the buffer for the variable, the first time we call GetServerVariable we set the size of the buffer to 0.

 (dwSize = 0)
This makes the call fail with error 122 (ERROR_INSUFFICIENT_BUFFER), but it will return the needed size in the dwSize parameter. In our filter, to make the code easier to understand, we don't check the first call for any errors. In production code, you will probably want to make sure all functions return successfully. After all, a filter should handle any situation gracefully, even if a client doesn't send a User-Agent header. Once we know the buffer size for the variable, we can allocate the memory we need. We call GetServerVariable again with a real buffer to get the value of the variable.
      This time we do check the error code. If GetServerVariable fails, we create a szTemp string with the error code and send it to the browser. Because the client and the server use the HTTP protocol, we can't just send error messages as text to the client. It is mandatory to send the HTTP status code first and then and only then send HTML data. The call to the ServerSupportFunction sends a "200 OK" response status and WriteClient sends szTemp to the browser. If an error occurs in the filter, the request should not be handled by any other filters and should be disconnected. In this scenario, my filter returns SF_STATUS_REQ_FINISHED.

Allocating Memory
      There are many ways to allocate memory in C or C++. There are also plenty of memory allocation APIs available through the Win32 API set. For each API that allocates memory you need a corresponding API to release the memory, otherwise the filter will have a memory leak. To spare you the memory allocation and deallocation mess, IIS exposes the AllocMem function via the HTTP_FILTER_CONTEXT structure. The cool thing about this function is that all allocated memory will be released automatically when the net session between the client and server ends. In production code you may want to check the success of the AllocMem call.
      So now we have allocated my buffer and copied the name of the browser software into that buffer. What next? We need to check the User-Agent for the presence of the "msie 4" substring that indicates that the browser is Internet Explorer 4.0 or 4.01. To prevent case sensitivity problems, we convert the User-Agent string to lowercase and search for the lowercase "msie 4" substring. Even though we know the User-Agent string for Internet Explorer is uppercase, we still want to be safe and do a case-insensitive search.
      If "msie 4" is present in the User-Agent, the browser is allowed to access the ieonly subdirectory. In this case, HttpFilterProc returns and allows the next filter in the chain (if there are any more) to process the request. If "msie 4" can't be found, the browser is not allowed to access the restricted location. SmartRedir must then redirect the browser to a URL that explains where to get the latest version of Internet Explorer 4.0.

Redirecting the Browser
      Well, unbelievable as it may seem, a browser other than Internet Explorer 4.0 or 4.01 might try to access my secret URL, \ieonly. In revenge, we crash the Offending browser, of course! Just kidding—the filter can't really crash a browser, but it can redirect the browser to a different location. The filter sends a "302 Redirect" status code to the browser and sets the HTTP Location header to the new URL. The "\r\n" (CR-LF) at the end of our location string indicates the end of the HTTP header. The second "\r\n" indicates that this is the end of all the headers we're sending.
      SmartRedir has a hardcoded name for the HTML file used as a redirection target.


  #define FILENAME "getie.htm"
It's defined at the beginning of my C source file. Getie.htm needs to reside in the root directory. Therefore, sending the HTTP header

  "Location: http://server/getie.htm\r\n\r\n"
forces the browser to show the getie.htm file. Setting this header requires the filter to fill the "server" with the correct server name, however. To make sure the filter works on any installation of IIS, the filter should determine the name of the computer it is running on and use this name in the header.
      Getting the server name is a piece of cake with the SERVER_NAME server variable. It contains the host name of the server (or sometimes the IP address of the server). The following code actually creates the Location header and sends it to the client with the "302 Redirect" HTTP status code.

 wsprintf (szTemp, "Location: http://%s/%s\r\n\r\n",
           lpszServer, FILENAME);
 pFC->ServerSupportFunction (pFC,
                            SF_REQ_SEND_RESPONSE_HEADER, 
                            (PVOID) "302 Redirect",
                            (DWORD) szTemp,0); 
      After redirecting the client to another URL, the filter notifies the server that this request is finished by returning SF_STATUS_REQ_FINISHED from HttpFilterProc.

Taking SmartRedir One Step Beyond
      SmartRedir is functional enough to be used in real life. The drawback in our implementation is that the User-Agent string ("msie 4") and the protected directory ("ieonly") are hardcoded. To change the Se values the filter needs to be recompiled, which is probably unacceptable for any reasonable software. Is there a convenient way to change some values in the filter while it is loaded? In other words, we may want to dynamically change the redirection target without unloading the filter or restarting IIS. We can actually do this quite easily by adding an ISAPI extension interface to the ISAPI filter.
      Both ISAPI filters and ISAPI extensions are implemented as DLLs. Nothing prevents smartredir.dll from exporting two more entry points that would make it an ISAPI extension and filter at the same time. the Se entry points are GetExtensionVersion and HttpExtensionProc. Adding ISAPI extension entry points to the filter enables the filter to be invoked in the same manner as any other ISAPI DLL—for instance, with a URL like http://server/scripts/smartredir.dll. Now, we could declare strings that identify the User-Agent ("msie 4") and the redirection target (http://server/getie.htm) as global parameters. That would make the M accessible from the HttpFilterProc function and from the HttpExtensionProc function. You could add code to the HttpExtensionProc function that would modify the global strings by invoking the ISAPI extension with parameters like this:


  http://server/scripts/smartredir.dll?agent=NewAgent&redir=http://server/getienow.htm
      Adding an ISAPI extension interface means you can change the filter's behavior without having to unload it, restart IIS, and recompile it. But before you rush out to modify SmartRedir, we should mention a few words of caution. There is a good chance that the ISAPI filter functions and the ISAPI extension functions will be invoked by multiple clients at the same time. Access to all global variables should be strictly controlled. If one client invokes the ISAPI extension and tries to modify the User-Agent string while the ISAPI filter tries to use the global variable, you may run into some problems. To prevent this, access to all global variables should be controlled via synchronization objects of some sort. Windows NT® uses synchronization objects to control multiple threads that are accessing the same variable (or other shared data) at the same time. For SmartRedir, access to global variables can be guarded with a synchronization object called a critical section. Critical sections are objects that allow only a single thread to own the M at any one time. You'd want to create a critical section and write code to take ownership of the critical section before manipulating the global variables.
      Figure 4 contains skeleton code that you can use to add an ISAPI extension interface to the SmartRedir sample. It has all the necessary steps to give you a solid idea of what you will need to do.

Advanced Filters
      You can do many things with ISAPI filters. You can implement a custom authentication scheme by handling the SF_NOTIFY_AUTHENTICATION event. Both the Basic or NTLM authentication methods control access based on valid Windows NT accounts; that is, if an account does not exist for the supplied credentials, the user won't gain access to the secure resource. You can write a filter that accesses the user name and password for a request (they are members of the HTTP_FILTER_AUTHENT structure), and then performs authentication based on a proprietary user account database instead of the Windows NT user accounts. Once the check of the database is successful, the filter replaces the supplied user name and password with the name and password of a valid Windows NT account.
      Suppose that when the browser brings up a dialog box to enter the user name and password for accessing a password-protected URL, you want the user to be able to enter a user name and password such as "JoeUser" and "password." If there is no valid Windows NT account for JoeUser, without your custom authentication filter, IIS could not authenticate the user. Your custom authentication filter validates the "JoeUser" and "password" credentials itself, and then replaces the M with a valid Windows NT account and password. In this manner, the JoeUser credentials will have allowed the client to access the protected resource.
      Another popular use for ISAPI filters is in custom logging software. You can create a filter that gets the SF_ NOTIFY_LOG notification. ISAPI filters can also be used to encode and decode the raw data that is being sent across the socket. In fact, this is how SSL is implemented. A filter called SSPIFILT.DLL hooks the SF_NOTIFY_READ_RAW_DATA and the SF_NOTIFY_SEND_RAW_DATA notifications to decode or encode the data accordingly.
      Another feature that could be implemented using ISAPI filters is your own authentication scheme. You could do this by hooking the SF_NOTIFY_ACCESS_DENIED notification and building the appropriate HTTP headers to perform your custom authentication handshake. When the response comes back, you need to hook the SF_NOTIFY_PREPROC_ HEADERS event to read the custom response headers and react appropriately. In this way you could add something like the new Digest Authentication scheme that is being considered as part of the HTTP specification.
      One more common use for ISAPI filters is simply monitoring the various events to track down problems on your server. For instance, if you are writing an ISAPI extension and you want to see the exact response data that is generated, you could write an ISAPI filter that hooks the SF_ NOTIFY_SEND_RAW_DATA event and displays the data being sent. This could be very useful while debugging.
      Now that you're busy thinking about all the powerful things you could do with your very own ISAPI filter, let's step back and look at another way to customize IIS. Let's take a look at writing your own script interpreter.

Custom Script Interpreters
      Using the built-in SSI facility available with IIS, you can create dynamic pages based on HTML-like template files. Consider the following scenario: XYZ Corporation needs to build a set of Web pages from predefined templates, similar to SSI. XYZ Corporation advertises garden supplies on the Web. The pages would consist of the item names and prices. Since there could be hundreds of different items on a page, updating the price of each would be a huge headache for the Webmaster. Just imagine opening an HTML file, looking for each item, and changing its price.
      If the prices change often, the work of editing such a document becomes unacceptable. A smarter solution would be to use certain tags in the initial documents, such as SHOVEL_PRICE, SCOOPER_PRICE, and so on, to indicate where the appropriate information should be inserted. When a request for this page arrives, you might be able to dynamically replace the tags with the actual prices. You could store the real values in either a database or just in a flat file. You could implement this in several ways: using the Internet Database Connector, writing your own ASP pages, or even by writing a raw data filter that would replace the Outgoing placeholders with the appropriate values. You could also write your own custom SSI processor.
      But with all the Other tools available, isn't creating something from scratch a waste of time? Not necessarily. If you happen to already have a proprietary mechanism for creating templates to display information, you could write your own script interpreter to read your file format and create appropriate HTML. You could even use custom script maps to take files in your own proprietary file format and convert it into HTML that can be displayed in a browser.
      Script interpreters are ISAPI extension DLLs just like any other ISAPI extensions, with one exception. Instead of referring to the extension DLL explicitly in the URL, an association is made with certain file extensions and the ISAPI extension DLL so that IIS will run the ISAPI extension when a request is received for a file with the specified extension. Adding a new entry for a particular file type will inform IIS to invoke your custom DLL for this file type. Of course, IIS already has a number of mappings for certain file types such as ASP.

SSIDemo
      We wrote an ISAPI extension, SSIDemo.dll (see Figure 5), and associated files with the .bpi extension with it. As soon as a request for a file with the extension .bpi arrives, IIS will load SSIDemo.dll just like any ISAPI extension request. The request for http://MyServer/MyTestFile.bpi does not refer to the SSIDemo.dll directly—it refers to the .bpi file—but IIS now knows to load SSIDemo.dll for this file extension. Once the DLL is loaded, you can access the full file path of MyTestFile.bpi through the lpszPathTranslated member of the Extension Control Block structure. At this point, you can open the file, read through it, and send its modified version to the client with the WriteClient callback function.
      Let's look at the details of SSIDemo. This sample replaces all tags in a template file (here, the files have the .bpi extension). Each of our template file's "tags" have a text string associated with it. the Se tag/string associations are stored in a special file that you can edit to create any associations you desire. The file name and location are configured via the registry:


 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\SSI
The value is named DictFile and it is type REG_SZ. We have complied with the SSI format used in the NCSA's HTTPd (the HTTP Daemon was the very first Web server ever written). So my tags have the following format:

 <!--- #TAG --->
      SSIDemo.dll will parse the .bpi file for all tags and replace the M with their string value from the dictionary file. The dictionary file consists of one string pair per line, like this:

 TAG1 = This is tag
Note that tags are case-sensitive.
      We decided to do something more than just simple text substitution. If a tag is preceded with a special character (I'll use @), the DLL will handle it as a server-side variable and replace it with the value returned from the callback GetServerVariable. The @ character can be changed by the addition of a VariableCharacter value to the registry location shown previously. Even though it has REG_SZ type, only the first character is significant. So if you put this entry into the .bpi file

 <!--- #@HTTP_USER_AGENT --->
it will be replaced with the actual value from the User-Agent header.

GetExtensionVersion Code
      The first thing that needs to be accomplished in the GetExtensionVersion code (see Figure 6) is reading the parameters from the registry: the file name with the tag/string pairs (referred to as the dictionary file) and the character used to indicate server variable tags. We also need to open the dictionary file. In this sample, we assume the file will fit into a buffer allocated with the LocalAlloc Win32 API. the Se initialization tasks need to be done only once for the entire life of the DLL, so it's a good idea to perform the M in the GetExtensionVersion function, which is called only once.
      Once the dictionary file is read, we can close the registry and the file. We check for locking and sharing violations when the dictionary file is opened with the CreateFile function and read with the ReadFile function. If the file happens to be locked (for instance, if someone happened to be editing the dictionary file), then the program waits 100 milliseconds and retries the Operation. It will retry three times. If for some reason the code can't read the dictionary file, it still continues executing. Because a .bpi file may have only server variables and no other tags, the absence of a dictionary file is not a critical error.
      You may have noticed an interesting function in Figure 6 called DebugOut. DebugOut is something we use to send our DLL's debug output to the debugger window. The code for DebugOut looks like this:


 #ifdef _DEBUG
     void DebugOut (CHAR *lpszFmt, ...)
     {
         CHAR szBuff[1024];
         va_list vargs;
         va_start (vargs,lpszFmt);
         vsprintf (szBuff, lpszFmt, vargs);
         va_end (vargs);
         OutputDebugString (szBuff);
         return;
     }
 #else
     void DebugOut (CHAR *lpszFmt, ...)
     {  
         return;
     }
 #endif
the OutputDebugString API used in DebugOut sends szBuff to the debugger window (as long as the DLL is built with the debug compiler and linker flags). You can use any debugger like Microsoft Visual C++ or DBMON.EXE from the Windows Platform SDK to see the debug output. DBMON.EXE creates a console window that will display all debug output.

HttpExtensionProc
      At this point the dictionary file is opened and the stage is set for reading the .bpi file and for parsing all tags in it. Since the full path of the .bpi file is passed in the ECB structure, opening this file is straightforward:


 hFile = CreateFile(pECB->lpszPathTranslated, 
                    GENERIC_READ,
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    (LPSECURITY_ATTRIBUTES) NULL, 
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_READONLY,
                    (HANDLE) NULL);
We still want to make sure that the file is not locked and there are no sharing violations. We want to keep this file opened for as little time as possible; if someone needs exclusive access to edit it, we don't want the file locked up because of our request processing. As soon as the file contents are read to the buffer it's closed.
      One way to handle substitution of all tags is to go over the entire buffer and replace tags with their values. The problem with this approach is that you don't know beforehand the size of the buffer required to do this. The new buffer can be either smaller (if real values from the dictionary file are shorter than tags) or larger than the Original one. We need to change the buffer length dynamically. Once the Original buffer (the One with the .bpi file content) is changed, we can issue a single call to WriteClient to send it to the browser.
      In the sample we chose an easier and probably smarter approach, with the drawback that we might be making many calls to WriteClient. This is the method:
  1. Find the first occurrence of the string "<!---". This indicates that this is a beginning of a possible tag.
  2. Send the content of the buffer just before this mark to the browser with the WriteClient ISAPI callback function.
  3. Look for an occurrence of the string "--->".
  4. If it is just an HTML comment (no # marks inside), send it to the browser as is.
  5. If there is a # character inside the comment mark, replace the entire comment from "<!---" to "--->" with the tag's value and send it to the browser using WriteClient.
  6. Advance the pointer to the next "<!---" string and repeat the process again.
      Figure 7 contains the code for this procedure. lpData is a pointer to the buffer with the contents of the .bpi file. You may have noticed that we used GetValueFromTag in our code. This is where we get the real value for the found tag. The real value comes from the dictionary file, which is currently in a memory buffer pointed to by the global variable. Since we use only the global variable to read data, we don't need to perform any synchronization to access it. We also need to know how big the string associated with our tag is. The lengths of the strings are returned by the function GetValueFromTag shown in Figure 8.
      So that's the guts of our custom SSI extension. All that is left is to deallocate the global memory that holds the dictionary file buffer when the DLL is unloaded. Adding DllMain to the DLL will do the trick:

 BOOL WINAPI DllMain (HANDLE hInst,
                      ULONG ul_reason_for_call,
                      LPVOID lpReserved)
 {
    if (ul_reason_for_call == DLL_PROCESS_DETACH)
    {
        DebugOut ("DLL_PROCESS_DETACH for instance: 0x%X\n", hInst);
        LocalFree (lpDataDict);
    }
     return TRUE;
 }
      Before you look at SSIDemo.DLL in action, there is one more point you should be aware of. It is not necessary to implement a script interpreter as an ISAPI extension. You can use a CGI application, but it will be much slower. In fact, some script processors, such as some Perl interpreters, are available in CGI (EXE) or ISAPI (DLL) versions. Should you decide to create a new script mapping for a CGI script interpreter, make sure to add " %s %s" to the EXE file name in the IIS scripts map (accessible via Application properties in the MMC). For instance, the script map entry might look like this:

 C:\Winnt\System32\SSIDemo.exe %s %s.
      The first %s will be mapped to the physical path of the .bpi file, similar to lpszPathTranslated member of the ECB. The second %s will be mapped to the query string. In the case of a request for "/scripts/foo.bpi?Foo=Bar" it will be the Foo=Bar string. We don't use query strings in our sample.

Taking SSIDemo for a Test Drive
      Well, we didn't write all this code for nothing. Let's see how it works. The first step is to create a registry entry for the dictionary file and to optionally set the special character indicating server variable tags (if you're not happy with the default @ character). Our dictionary file is d:\inetpub\ wwwroot\dictfile.txt, and we created the registry entry DictFile to reflect its location. The contents of our dictionary file is as follows:


 FName = Leon
 LName = Braginski
 City = Bellevue
 State = WA
 Country = USA
      The next step is to create a template .bpi file. The .bpi file uses all the valid HTML tags and comments. Figure 9 shows the HELLO.BPI file that includes comments, tags, and server variables that would be processed by SSIDemo.dll. All that is left to do at this point is to access http://leonbr-hm/hello.bpi and enjoy it. Figure 10 shows what the user sees in their browser.
Figure 10 Browser View
Figure 10 Browser View


      By the way, because we used the debug version of the DLL, we can see the debug output using the DBMON.EXE utility. the Output for my request is shown in Figure 11.

Figure 11 DBMON.EXE Output
Figure 11 DBMON.EXE Output


      Now we have a working script interpreter that has customized how certain types of files will be used. You have also seen how an ISAPI filter can be used to modify how HTTP requests are processed. the Se customization capabilities allow you to make IIS handle HTTP requests in almost any manner you see fit.

From the April 1998 issue of Microsoft Systems Journal.