CGI Is on Target -- Martin discovers ISAPI filters are trickier than they look and retreats to CGI.
By Martin Heller
REMEMBER LAST MONTH, when I decided to write WINDOWS Magazine's ad rotation system for its Web site as an ISAPI filter rather than a CGI application? I changed my mind. Let me explain why.
At first glance, I thought the ISAPI filter samples in the ActiveX software development kit and in Visual C++ 4.1 would give me a flying start on writing an ISAPI filter. With this I could preprocess HTML files, detect a comment tag of the form <!-AD ->, look up the next ad in a rotation table, and substitute the HTML text that would insert the correct ad graphic and hypertext jump. You may recall I was having a little trouble. Well, I'll tell you what I learned.
First, what you really need to know about ISAPI filters isn't documented: You need to know the sequence and format of events coming from the Web server. You can figure it out by debugging, but preparing to debug an ISAPI filter takes a bit of work.
To start, you need a machine that runs Internet Information Server (IIS), but you have to take the Internet services down. Then give yourself some special privileges. In the Windows NT User Manager on the machine that runs IIS, go to Policies/User Rights, check the "Show advanced user rights" box and pick "Act as part of the operating system." Add that right and the "Generate security audits" to your own user account or to a group to which your account belongs, such as Administrators. You'll run IIS under a debugger from your own account, instead of letting it run as a service.
Log out and log in again to acquire the privileges you just gave yourself. Now, run Performance Monitor to make sure no one's using them and stop all three IIS services (WWW, ftp and gopher) with Internet Service Manager.
Copy your ISAPI filter DLL and its PDB file to a convenient place, perhaps your INETSRV directory. Don't use your original copy because you'll have trouble rebuilding it while it's in use. Now run REGEDIT and find the HKEY_LOCAL_MACHINE/System/CurrentControlSet/Services/W3SVC/Parameters key. You'll find an item under that key for Filter DLLs, which will probably already have an entry like C:/INETSRV/SSPIFILT.DLL. This Registry entry holds a comma-separated list, so add your DLL after the one that's there, with a comma between them. If you use a semicolon instead, like I did for an unpleasant couple of hours, your DLL won't load.
Now you're ready to fire up the debugger. If you're using Microsoft Developer Studio, make a project for INETINFO.EXE, and add the program arguments -e W3Svc under the General category in the Debug options tab. Also add your DLL to the local names box under the "Additional DLLs" category. As the psychiatrist said at the end of Portnoy's Complaint, "Und now, ve begin."
Open the source file for your filter DLL and set any breakpoints you'll need. I suggest setting breaks at every function that handles a filter event. Now select the Build menu, then Debug/Go. When Developer Studio bleats about INETINFO not having debug information, pat it on the head and let it continue. You'll know INETINFO is running from the indicator on the menu bar.
Switch to a browser (it needn't be on the same machine) and call up a page from the Web server you're now debugging. You should see the debugger pop up at one of the breakpoints you've set.
A couple of hitches bit me when I was trying to debug my ISAPI filter. One appears to be a design deficiency or bug in Developer Studio's integrated debugger. To view a block of data at a pointer, I had to open a memory window and type in the value of the pointer. Other debuggers allow you to click on the pointer to see the buffer it points to or right-click on the pointer to bring up the memory window at the right place. Having to type in a hex value to see the correct buffer every time a breakpoint fires is not my idea of fun.
So what did I find out about the sequence? It's complicated. Let's consider the case of a simple HTML page with no server-side includes and one graphic. Let's also consider only SF-NOTIFY-URL-MAP, SF-NOTIFY-SEND-RAW-DATA and SF-NOTIFY-END-OF-SESSION events.
The first event you'll see is the URL mapping request, SF_NOTIFY_URL_MAP. The buffers for this event hold the logical URL requested and the physical path being mapped. You can change the mapping at this point or return. You also have access to a context flag that will survive throughout the connection. It's important to initialize it. If the parameter passed is HTTP_FILTER_CONTEXT * pfc, the flag you want to initialize is pfc> pFilterContext. I set it to 1 if the request is for an HTML file, and 0 if it's for anything else. If you don't do this you'll have difficulty knowing what's going on in the following events.
The next event you'll get will be for SF_NOTIFY_SEND_RAW_DATA. It'll probably be the requested file's HTML header, but it could be an HTML error message saying access has been denied, the file doesn't exist, or it isn't any newer than the version the client already has cached. If the header looks genuine and error-free for the text/html MIME type, I set the context flag to 2; otherwise I set it to 0. In either case, I return without modifying the headers.
With any luck, you'll get the actual HTML next. I try to process it only if the context flag is 2. In my case, the job is to scan for a tag of the form <!-AD ->. If I find such a tag I have to process it; if not, I can return. Processing the tag could be complicated and might require allocating a new, larger buffer for the expanded HTML. The end of connection event is a good time to clean up or reorganize buffers.
You'll remember I said this page had one graphic. The request for the graphic will come in after the HTML. You'll see the URL mapping event, then raw data with the text/gif or text/jpeg headers and then the raw data for the graphic. You could process the graphic in some way at this point, but in my case the important thing was not to try to scan graphic data looking for HTML tags. This would cause a minor disaster if, by some horrible coincidence, the graphic data contained the exact data I was looking for.
That is the very simplest case. If server-side includes are enabled, which by default happens only on STM files, the raw data for an HTML page comes to the filter DLL in many pieces: first the unexpanded page, then the page prior to the first <!-include> tag, then the expansion of the include, then the page between the first include and the next include, and so on.
It struck me that what I was trying to do was probably 99 percent implemented in the piece of IIS doing the server-side includes. I realized I was well on my way to being able to implement a more general server-side exec for IIS. And I realized I'd had it up to my neck.
So I decided on a CGI implementation for the Web site. Once I nailed myself down it took only a day to write a CGI program to return the next ad in rotation and maintain display counts, one console program to initialize data files for the rotation program and another to report display counts.
There were only a few interesting things about my CGI implementation. First, I followed my own advice and wrote to the Win32 API set, avoiding the C and C++ runtime libraries. Second, I used a mutex to synchronize access to the data files. And third, I designed the data files for efficient access.
Why a mutex? I'm glad you asked. If I allowed write sharing of the data file containing the ad display counts and the pointer to the next ad, one instance of the CGI application could write the file after another instance head just read it, and the counting wouldn't work. If I simply opened the file for exclusive use, a second instance would fail when trying to open the file. It would then have to retry the open until it succeeded. With a mutex, each instance can wait for access without doing any looping, and the Windows NT system won't give the process any CPU time until the mutex is available. The code looks like this:
HANDLE mutex_handle = CreateMu-tex( NULL, FALSE, mutex_name);
DWORD dwRC = WaitForSingleOb-ject(mutex_handle, time-out);
Once the mutex has been claimed (by waiting for it), the process should be able to open the data file for exclusive access:
HANDLE fh = CreateFile( file_name, GENERIC_READ|GENERIC_WRITE,0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL,NULL);
I chose to keep the counts file simple, since it contains only binary long integers. The first one holds the number of ads being rotated, the second the counter for the next ad and the rest the display counts for the ads. Three ReadFile calls bring in the whole file:
ReadFile(fh, &n_ads,sizeof (n_ads),&n_read,NULL);
ReadFile(fh, &next_ad, sizeof (next_ad),&n_read, NULL);
ReadFile(fh, ad_counts, n_ sizeof(int), &n_read, NULL);
Once the counts are in memory, I figure out the next one in sequence, update its count and rewrite the file:
SetFilePointer(fh, 0, NULL, FILE_BEGIN);
WriteFile(fh, &n_ads, sizeof (n_ads), &n_read, NULL);
WriteFile(fh, &next_ad, sizeof (next_ad), &n_read, NULL);
WriteFile(fh, ad_counts,n_ads*sizeof(int),&n_read, NULL);
The next_ad variable now holds an index to the ad I want to return to the Web server. I gave the text file fixed-length records so access could be achieved with one seek and one read:
sizeof(ad_text), NULL, FILE_BEGIN);
ReadFile(fh, ad_text, sizeof
(ad_text), &n_read, NULL);
The easiest way to return the actual text to the Web server is to send the string to stdout:
And that's what I learned about ISAPI filters.
Senior Contributing Editor Martin Heller writes about and does Windows programming from Andover, Mass. Contact Martin at his Web page at http://www.winmag.com/people/mheller. Martin Heller's e-mail ID is: email@example.com