Developer.com Click here to support our advertisers
Click here to support our advertisers
SOFTWARE
FOR SALE
BOOKS
FOR SALE
SEARCH CENTRAL
* JOB BANK
* CLASSIFIED ADS
* DIRECTORIES
* REFERENCE
Online Library Reports
* TRAINING CENTER
* JOURNAL
* NEWS CENTRAL
* DOWNLOADS
* DISCUSSIONS
* CALENDAR
* ABOUT US
----- Journal:

Get the weekly email highlights from the most popular online Journal for developers!
Current issue -----
developer.com
developerdirect.com
htmlgoodies.com
javagoodies.com
jars.com
intranetjournal.com
javascripts.com

REFERENCE

All Categories : ActiveX

Chapter 7

ISAPI


CONTENTS

The Internet Server Application Programming Interface (ISAPI) is an API that allows a developer to add extensions to the Internet Information Server (IIS), replacing or enhancing programming duties typically performed either by CGI scripts or an HTTP server. An ISAPI extension is an application that executes on the server, at the client's request, typically via an HTML form. The request performs some function on the server and returns a response to the client. ISAPI extensions, then, replace the functionality typically associated with CGI scripts.

An ISAPI filter is a different beast: a filter can look at the incoming and outgoing data, and if the filter is "interested" in the data, it will do something. This action may be invisible to the end user. For example, the filter may write additional information to the IIS Web log, read and write Cookie files, redirect the client from one Web page to another, and so forth.

This chapter gives you an introduction to using and creating ISAPI extensions and filters. It's a fairly broad subject, enough to consume yet another tome. We will cover the basic API structures that make up an extension or filter; show you how to load, run, and debug ISAPI applications; and describe the MFCs introduced in VC++ 4.1 that you can use to develop ISAPI applications. We will also discuss various sample applications that perform tasks typically assigned to ISAPI. And finally, we will look at a couple of the ISAPI tools and alternatives to ISAPI that are available from Microsoft and other publishers.

ISAPI versus CGI

The implications of ISAPI's role in Web programming are enormous. Instead of a separate binary, running in its own name space and consuming system resources, an application running in the IIS process space will run faster and more efficiently-a key consideration in handling Internet requests.

Before going into detail on the ISAPI specifications and examples, it's important that we review the CGI model of interactivity. In the CGI scheme:

  1. The client sends a request to the server via an HTTP GET or POST method.
  2. The connection persists while the server forks a CGI process to handle the request.
  3. That CGI process builds output and passes it back to the server.
  4. Finally, the server passes the protocol headers plus the CGI script output back to the client.

During this request/response cycle, other requests might be coming in, spawning more CGI processes in memory. It's easy to see how a Web server can get overloaded as the number of simultaneous TCP/IP connections rises. Worse, many of the CGI scripts that are spawned are interpreted shell scripts, which run orders of magnitude slower than compiled code. The Perl interpreter, by the way, parses scripts into bytecode and lies halfway between very slow interpreted code and compiled binaries. On the client side, an hourglass appears until the request is fulfilled. If the server has instantiated multiple CGI script instances, the end user may have a long wait.

An ISAPI program, on the other hand, does not spawn a new process after it has been called the first time. Rather, it runs in the server's process space, thus reducing both memory overhead and startup time. Only a single copy of this Internet Server Application (ISA) is loaded and is then shared if additional client requests for the same ISA come in. Furthermore, once an ISAPI application is loaded into the process space, it stays loaded until IIS is stopped and restarted, or until some unspecified period when IIS decides to unload it. This means new requests for the ISA will get a quicker response.

All in all, this speed improvement is a strong argument for using ISAPI to do anything that requires back-end server processing. Not to mention the fact that, until recently, no decent implementations of scripting languages were available for an NT server. Fortunately there are alternatives now, as you'll see in the sections at the end of this chapter. Given these alternatives, it may not always be necessary to use ISAPI.

ISAPI Isn't Always the Answer

Why not use ISAPI? The one downside is the choice of language: Typically you will develop ISAPI applications in VC++. Many Webmasters are more familiar with traditional Web scripting languages such as Perl, which don't require the extra effort and endless recompilation necessary under VC++. In addition, other languages are usually easier to debug, making for faster development of simple applications and easier maintenance down the road.

Particularly if you have a low-traffic Web site where performance is not an issue, Perl (or another such choice) offers a much simpler solution for quick-and-dirty Web application scripting.

NOTE
The language issue does not apply to ISAPI filters. Writing a filter as such is outside the definition of CGI and seems much too cumbersome to even contemplate. Most HTTP servers already allow for some types of filtering- including redirection, access log manipulation, and so forth-in their configuration files. These are, however, somewhat crude methods of filtering. ISAPI filters offer full control over the incoming and outgoing data.

ISAPI Nuts and Bolts

This section offers a brief overview of the structures and functions that are required to create an ISAPI extension or filter. We won't attempt to cover all the "gory details"-you will find those thoroughly explained in the ISAPI specification at

http://www.microsoft.com/win32dev/apiext/isaphome.htm

as well as the on-line reference material included with Visual C++.

The purpose here is to give the CGI-trained reader an idea of the differences between ISAPI applications and CGI programs. Unlike a typical CGI program, an ISAPI application requires more than just writing a few lines of code that print to stdout; you can't expect that to get back to the HTTP client. A few things are required before IIS will recognize the program as an ISA, and that's what we'll examine here.

NOTE
An ISAPI extension or filter is, to the Internet Information Server, an executable file. To the Windows user, this means the file is identified with a filename extension of .exe, .bat, or .com. IIS also recognizes files with a .dll extension as executable. When given a URL that references a DLL, IIS will attempt to load and run the file, looking for a particular entry-point function: either GetExtensionVersion (for extensions) or GetFilterVersion (for filters). Beginning with the 2.0 version of IIS, the server will also recognize a file with the extension .ISA, for Internet Server Application, as a filter or extension. (You can either build the file with that extension, or simply rename the .dll file.

Extensions

ISAPI extensions are called in the same manner as CGI requests, with either a GET or POST method form; or an HREF with a query string attached to it; and/or an HREF with extra path info. We won't review the CGI spec here except to mention the differences between the GET and POST methods.

The GET method has only a 1,024-character buffer. More importantly, browsers have different ways of sending the request URL. The GET method also suffers from the disadvantage that a malicious client can launch a denial-of-service attack by passing in a very large string. The GET method fills an HTTP environmental variable, QUERY_STRING, whereas the POST method sends data to standard input. POST, in general, is considered safer for this reason.

Complete details on the CGI specification can be found at

http://hoohoo.ncsa.uiuc.edu/cgi/

Before defining what makes up an extension, let's look at the output from printenv.dll, our CGI equivalent of a "Hello, World" application. This application retrieves the values of various environment variables, then creates HTML and sends it back to the user, displaying the values of these variables. Figure 7.1 shows a screen with sample output when this DLL is called with the GET method.

Figure 7.1 : Sample output from printenv.dll, called via the GET method.

To generate this output, printenv.dll was called with a URL in the following form:

http://127.0.0.1/msdev/printenv/Release/printenv.dll?
NOTE
If the directory in which the ISA resides is configured for both Execute and Read access, then the ? has to be there; otherwise, the server sees it as an unknown file type and pops up the Save As box. It is generally a good practice to put executables in a directory that is configured for Execute-only access. If the directory includes Read access, a user can download your ISA and "Save to Disk" by calling the URL without the ? or additional parameters.

Now try calling the DLL from a POST method form. For instance:

<TITLE>printenv.cpp</TITLE>

<FORM METHOD=POST ACTION="http://127.0.0.1/msdev/printenv/
Release/printenv.dll?">
<INPUT TYPE=TEXT NAME=FIELD1 VALUE="FIELD1">
<INPUT TYPE=TEXT NAME=FIELD2 VALUE="FIELD2">
<INPUT TYPE=TEXT NAME=FIELD3 VALUE="FIELD3">
<BR>
<INPUT TYPE=SUBMIT VALUE="Post">
</FORM>

You can see the differences between the GET and POST methods by looking at some of the field values, such as lpszMethod, lpszQueryString, and so on.

Listing 7.1 is the source code for printenv.dll.


Listing 7.1 printenv.cpp
// printenv.cpp
// non-MFC ISAPI example
// prints HTTP environment variables
//

#include <windows.h>
#include <stdio.h>
#include <httpext.h>

BOOL WINAPI DllMain (HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpv)
     {
     // Nothing to do here
     return (TRUE);
     }

DWORD PrintVars(CHAR * , EXTENSION_CONTROL_BLOCK *);

DWORD dwBytesWritten;
void WriteHtml(EXTENSION_CONTROL_BLOCK *pECB, LPSTR lpsz);
void WriteHtml(EXTENSION_CONTROL_BLOCK *pECB, DWORD dw);

BOOL WINAPI GetExtensionVersion (HSE_VERSION_INFO *version)
{
     version->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);
     strcpy(version->lpszExtensionDesc, "Print Environment variables");
     return TRUE;
}

DWORD WINAPI HttpExtensionProc (EXTENSION_CONTROL_BLOCK *pEcb)
{
     DWORD dwReturnCode;
     char szOutputBuffer[4096];

     pEcb->ServerSupportFunction (pEcb->ConnID,
          HSE_REQ_SEND_RESPONSE_HEADER,
          "200 OK",
          NULL, (LPDWORD)szOutputBuffer);

     dwReturnCode = PrintVars(szOutputBuffer, pEcb);
     return HSE_STATUS_SUCCESS;
}

DWORD PrintVars(CHAR * szVars, EXTENSION_CONTROL_BLOCK *pECB)
{
     
     WriteHtml(pECB, "<B>printenv.cpp</B><BR>");
     WriteHtml(pECB, "Fetch and print stuff from the ECB<HR>");

     WriteHtml(pECB, "cbSize=");
     WriteHtml(pECB, pECB->cbSize);

     WriteHtml(pECB, "<BR>dwVersion=");
     WriteHtml(pECB, pECB->dwVersion);
     
     WriteHtml(pECB, "<BR>cbTotalBytes=");
     WriteHtml(pECB, pECB->cbTotalBytes);
     
     WriteHtml(pECB, "<BR>cbAvailable=");
     WriteHtml(pECB, pECB->cbAvailable);
     
     WriteHtml(pECB, "<BR>lpszMethod=");
     WriteHtml(pECB, pECB->lpszMethod);

     WriteHtml(pECB, "<BR>lpszQueryString=");
     WriteHtml(pECB, pECB->lpszQueryString);
     
     WriteHtml(pECB, "<BR>lpszPathInfo=");
     WriteHtml(pECB, pECB->lpszPathInfo);

     WriteHtml(pECB, "<BR>lpszPathTranslated=");
     WriteHtml(pECB, pECB->lpszPathTranslated);

     WriteHtml(pECB, "<HR>");
     return HSE_STATUS_SUCCESS;
}

void WriteHtml (EXTENSION_CONTROL_BLOCK *pECB, LPSTR lpsz)
{
     dwBytesWritten = lstrlen (lpsz);
     pECB->WriteClient (pECB->ConnID, (PVOID) lpsz, \
          &dwBytesWritten, 0);
}
 

void WriteHtml(EXTENSION_CONTROL_BLOCK *pECB, DWORD dw)
{
     LPTSTR szdwString="";
     wsprintf(szdwString, "%lu", dw);
     dwBytesWritten = lstrlen(szdwString);
     pECB->WriteClient (pECB->ConnID, (PVOID)szdwString, \
          &dwBytesWritten, 0);
}

You can build and test this printenv.cpp extension in the Visual C++ IDE by creating a new project workspace for a DLL and inserting the file. Additionally, we did two things before compiling the ISA: (1) In the Build Settings dialog, under Link/Output, we defined the Entry Point symbol as "DllMain@12," which is what the IDE wanted from us. (2) We created a .def file that defines the two functions to export. Here's that file (printenv.def):

LIBRARY     printenv

DESCRIPTION     'Print Environment variables'

EXPORTS     GetExtensionVersion
          HttpExtensionProc

Now let's define each of the key elements in our simple ISA.

Extension Control Block

The first ingredient is the Extension Control Block (ECB), a data structure used by the HTTP server and the ISA to exchange information. The ECB consists of a number of variables and several support functions. From the ISAPI header file, the ECB is defined as follows:

typedef struct _EXTENSION_CONTROL_BLOCK {
     DWORD          cbSize;     
     DWORD          dwVersion;
     HCONN          ConnID;
     DWORD          dwHttpStatusCode;
     CHAR          lpszLogData[HSE_LOG_BUFFER_LEN];
     LPSTR          lpszMethod;
     LPSTR          lpszQueryString;
     LPSTR          lpszPathInfo;
     LPSTR          lpszPathTranslated;
     DWORD          cbTotalBytes;
     DWORD          cbAvailable;
     LPBYTE          lpbData;
     LPSTR          lpszContentType;

BOOL ( WINAPI * GetServerVariable )
     (HCONN          hConn,
      LPSTR          lpszVariableName,
     LPVOID          lpvBuffer,
     LPDWORD     lpdwSize );

BOOL ( WINAPI * WriteClient )
     (HCONN          ConnID,
     LPVOID          Buffer,
     LPDWORD     lpdwBytes,
     DWORD          dwReserved );

BOOL ( WINAPI * ReadClient )
     ( HCONN     ConnID,
     LPVOID          lpvBuffer,
     LPDWORD     lpdwSize );

BOOL ( WINAPI * ServerSupportFunction )
     ( HCONN     hConn,
     DWORD          dwHSERRequest,
     LPVOID          lpvBuffer,
     LPDWORD     lpdwSize,
     LPDWORD     lpdwDataType );

} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

The variables in the ECB are described in Table 7.1, and Table 7.2 lists the ECB functions.

Table 7.1 Extension Control Block Variables
VariableDefinition
cbSizeSize of this structure
dwVersionVersion number of this application
ConnIDReserved; server assigned number
dwHttpStatusCodeHTTP status code
lpszLogDataData that will be written to the IIS HTTP log
lpszMethodHTTP request method (GET, POST, etc.)
lpszQueryStringQuery string portion of a GET request
lpszPathInfoAdditional path info, if supplied in the URL request
lpszPathTranslatedPhysical path to the requested file
cbTotalBytesAmount of data returned to the client
cbAvailableTotal amount of data available
lpbDataData returned to the client
lpszContentTypeMIME type

Table 7.2 Extension Control Block Functions
FunctionDefinition
GetServerVariableRetrieves the value of an HTTP environment variable. See Table 7.7 for a listing of available variables.
WriteClientWrites data to the client.
ReadClientReads data from the body of the client request (that is, for Method=POST forms).
ServerSupportFunctionThe SSF provides support for redirection, adding reponse headers, and continuing processing on the server after finishing the client request.

Required Entry-Point Functions

Two entry points are required in the extension. The first entry point is GetExtensionVersion, which can be implemented as follows:

BOOL WINAPI GetExtensionVersion( HSE_VERSION_INFO  *pVer )

{
    pVer->dwExtensionVersion = MAKELONG(
     HSE_VERSION_MINOR,
     HSE_VERSION_MAJOR );

    lstrcpyn( pVer->lpszExtensionDesc,
     "Extension Description goes here",
     HSE_MAX_EXT_DLL_NAME_LEN );
    return TRUE;
}

The GetExtensionVersion entry point is called when IIS first loads the DLL. Once the DLL is loaded into the IIS process space, a client request causes the second entry-point function, HttpExtensionProc, to be called. HttpExtensionProc is declared as follows:

DWORD  HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK  *lpEcb );

with the implementation left up to the developer.

Typically, the first thing to do is send a proper HTTP response header back to the client, such as

HTTP/1.0 200 OK\r\n
Content-type:  text/html\r\n\r\n

We did this with the Server Support Function (SSF). Aside from that, you will need to parse any parameters supplied with the client request.

At this point, things get a bit messy when developing ISAPI extensions without using MFC. The bulk of the ISAPI examples in the Win32 SDK do not use MFC, and the interested reader should look to those examples for means of parsing the client request parameters. The MFC methods for parsing client requests are covered in an upcoming section.

Filters

Earlier we mentioned that filters are different from extensions. Loading filters requires an extra programming step, because a filter is loaded when the IIS starts up and remains active until the service is stopped (or crashes).

In order to use an ISAPI filter, you must add an entry for the filter to the System Registry. The key that tells IIS what filters to load is

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\W3Svc\Parameters\FilterDLLs

With IIS 2.0, a string value for the default filter, sspifilt.dll, will already be added. To add another filter to IIS, you add the new filter's full path to the existing string, separating the filenames with a comma. For example, later in this section we have code for a sample filter called nmfcfilt.dll, so the string associated with the value FilterDLLs becomes

c:\inetsrv\Server\sspifilt.dll,d:\msdev\nmfcfilt\Debug\nmfcfilt.dll

Of course, the pathnames will vary by system. After adding the string to the Registry, you must restart IIS if the service is already running, in order for the new filter to be loaded.

Methods of debugging ISAPI applications are discussed later in the chapter. For now, we will just point out that should you put a bad filename here in the Registry, or if something is wrong with your filter and it fails to load, an error message will show up in the Event Viewer. See Figure 7.2.

Figure 7.2 : Massive trouble loading an ISAPI filter, as witnessed by the Event Viewer.

Filter Context Structure

One way in which filters are similar to extensions is that there are a filter structure and two entry-point functions in the DLL. An ISAPI filter uses the structure HTTP_FILTER_CONTEXT, as defined in httpfilt.h:

typedef struct _HTTP_FILTER_CONTEXT
{
     DWORD          cbSize;
     DWORD          Revision;
     PVOID          ServerContext;
     DWORD          ulReserved;
     BOOL           fIsSecurePort;
     PVOID          pFilterContext;

      BOOL (WINAPI * GetServerVariable) (
     struct _HTTP_FILTER_CONTEXT * pfc,
     LPSTR          lpszVariableName,
     LPVOID         lpvBuffer,
     LPDWORD        lpdwSize
     );

         BOOL (WINAPI * AddResponseHeaders) (
     struct _HTTP_FILTER_CONTEXT * pfc,
     LPSTR          lpszHeaders,
     DWORD          dwReserved
     );

         BOOL (WINAPI * WriteClient)  (
     struct _HTTP_FILTER_CONTEXT * pfc,
     LPVOID         Buffer,
     LPDWORD        lpdwBytes,
     DWORD          dwReserved
     );

         VOID * (WINAPI * AllocMem) (
     struct _HTTP_FILTER_CONTEXT * pfc,
     DWORD          cbSize,
     DWORD          dwReserved
     );

         BOOL (WINAPI * ServerSupportFunction) (
     struct _HTTP_FILTER_CONTEXT * pfc,
     enum SF_REQ_TYPE sfReq,
     PVOID          pData,
     DWORD          ul1,
     DWORD          ul2
     );

} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;

The variables in the HTTP_FILTER_CONTEXT structure are defined in Table 7.3. Functions in the filter structure are in Table 7.4.

Table 7.3 HTTP_FILTER_CONTEXT Variables
VariableDefinition
cbSizeSize of this structure
RevisionRevision level of structure
ServerContextReserved; used by the server
ulReservedReserved; used by the server
fIsSecurePortTRUE if the request is over a secure port
pFilterContextA pointer to this filter context

Table 7.4 HTTP_FILTER_CONTEXT Functions
FunctionDefinition
GetServerVariableRetrieves the value of an HTTP environment variable. Table 7.7 lists available variables.
Add Response HeadersSends an HTTP header to the client.
WriteClientWrites data to the client.
AllocMemUsed to allocate a block of memory for use by the filter.
ServerSupportFunctionThe SSF provides support for redirection, adding reponse headers, and continuing server processing after the client's request.

ISAPI filters have essentially the same two entry points as extensions: GetFilterVersion and HttpFilterProc. When IIS is started, the first entry point (GetFilterVersion) is called as each filter is loaded.

GetFilterVersion could be implemented as follows:

BOOL WINAPI  __stdcall 
GetFilterVersion(HTTP_FILTER_VERSION * pVer)
{
     pVer->dwFlags =     (
     // These flags are samples only. Other flags can go here... SF_NOTIFY_NONSECURE_PORT |
     SF_NOTIFY_ORDER_LOW |
     SF_NOTIFY_READ_RAW_DATA |
     SF_NOTIFY_SEND_RAW_DATA
     );
    pVer->dwFilterVersion = HTTP_FILTER_REVISION;
    strcpy( pVer->lpszFilterDesc, "non-MFC filter example");
    return TRUE;
}
NOTE
As you can see, in addition to exchanging version information, GetFilterVersion also tells IIS what notifications it is interested in. Microsoft documents recommend that you only include flags for the notifications you are interested in, because some of the notifications are "expensive" in system resources. The documentation doesn't say which notifications are expensive, except to indicate that changing the priority of notifications to anything higher than the default ORDER_LOW is expensive.

The HttpFilterProc entry point is declared as follows:

DWORD WINAPI   __stdcall HttpFilterProc(HTTP_FILTER_CONTEXT * pfc, 
DWORD notificationType, VOID * pvNotification);

As with HttpExtensionProc, the implementation is left up to the developer. This is the callback function that acts as the DLL's "main" function.

A sample non-MFC filter is shown in Listing 7.2. This filter looks at the incoming request. If the request is for any file in a particular subdirectory, \isapi, then the HTML returned to the client will have an appended string of additional HTML indicating a copyright date. If the requested subdirectory contains the string "Cookie," the user is denied access and gets an HTML message that the directory is under construction.


Listing 7.2 nmfcfilt.cpp
// nmfcfilt.cpp
// ISAPI filter example w/o MFC
//

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <httpfilt.h>

void WriteHtml (HTTP_FILTER_CONTEXT * pfc, LPSTR lpsz);
BOOL bWriteFooter;

BOOL WINAPI  __stdcall 
GetFilterVersion(HTTP_FILTER_VERSION * pVer)
{
     pVer->dwFlags =     (SF_NOTIFY_NONSECURE_PORT |
               SF_NOTIFY_ORDER_LOW |
               SF_NOTIFY_READ_RAW_DATA |
               SF_NOTIFY_SEND_RAW_DATA |
               SF_NOTIFY_END_OF_NET_SESSION
               );
    pVer->dwFilterVersion = HTTP_FILTER_REVISION;
    strcpy( pVer->lpszFilterDesc, "non-MFC filter example");
    return TRUE;
}

DWORD WINAPI   __stdcall
HttpFilterProc(HTTP_FILTER_CONTEXT * pfc, DWORD notificationType,
                  VOID * pvNotification)
{
     CHAR *  chInData;
     CHAR *  chOutData;
     DWORD dwOutData;
     PHTTP_FILTER_RAW_DATA pRawData;
     CHAR * iCmpRet;
     CHAR * chTestDir;
     LPSTR lpCopyRight ="<CENTER><I>This document (C) 1998</I></CENTER>";
     LPSTR lpConstruction = 
     "HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n \
     <B>This Area is <I>Under Contruction!</I><B><HR>\r\n";

     OutputDebugString("HttpFilterProc:MAIN\n");     

    switch (notificationType)
    {

     case SF_NOTIFY_READ_RAW_DATA:
     pRawData = (PHTTP_FILTER_RAW_DATA)pvNotification;
     OutputDebugString("HttpFilterProc:ReadRawData\n");     
     
          chInData = (CHAR * )pRawData->pvInData;

          chTestDir = "isapi";
          iCmpRet = strstr(chInData, chTestDir);
          if(iCmpRet != NULL)
          {
               bWriteFooter = TRUE;
          };

          chTestDir = "Cookie";
          iCmpRet = strstr(chInData, chTestDir);
          if(iCmpRet != NULL)
          {
               WriteHtml(pfc, lpConstruction);
               return SF_STATUS_REQ_FINISHED;
          };
          
     break;

     case SF_NOTIFY_END_OF_NET_SESSION:
          {
     OutputDebugString("HttpFilterProc:EndOfNetSession\n");
               bWriteFooter = FALSE;
          }
     break;

     case SF_NOTIFY_SEND_RAW_DATA:
     OutputDebugString("HttpFilterProc:SendRawData\n");     
     pRawData = (PHTTP_FILTER_RAW_DATA)pvNotification;

     if(bWriteFooter==TRUE)
     {
     OutputDebugString("HttpFilterProc: \
     bWriteFooter==TRUE\n");     
          chOutData = (CHAR * )pRawData->pvInData;
          strcat(chOutData, lpCopyRight);
          dwOutData = strlen(lpCopyRight);
          pRawData->cbInData = pRawData->cbInData + \
          dwOutData;
          pRawData->pvInData = (PVOID)chOutData;
          bWriteFooter = FALSE;
     };
     break;
     
     default:
     OutputDebugString("HttpFilterProc:Default\n");     
     break;
     }
          return SF_STATUS_REQ_NEXT_NOTIFICATION;
}
     
void WriteHtml(HTTP_FILTER_CONTEXT * pfc, LPTSTR lpsz)
{
     DWORD dwBytesWritten = lstrlen (lpsz);
     LPDWORD lpwBytes = &dwBytesWritten;
     pfc->WriteClient(pfc, lpsz, lpwBytes, 0);
}

As you can see in this example, the keys to an ISAPI filter are the notification flags supplied in the GetFilterVersion function. Each of these flags has its own structure, detailed in the ISAPI specification. The flags allow the filter to respond to the various events occurring during an HTTP transaction. This is a considerable improvement over, for example, the early versions of UNIX HTTP servers, where filter-type functions could only be specified in the server configuration files.

ISAPI Foundation Classes

Visual C++ 4.1 introduces five new Microsoft Foundation Classes (MFCs) for the development of ISAPI extensions and filters, along with a sample application, wwwquote. VC++ 4.1 also includes an AppWizard for creating ISAPI applications, and three macros for debugging: ISAPITRACE, ISAPIVERIFY, and ISAPIASSERT.

The new MFCs encapsulate the structures and functions needed to create ISAs. They provide implementations of common functions and access to data members and objects that streamline the process of developing ISAs.

There is the usual trade-off, however, when using MFCs. In our test compilations, DLLs using MFCs were frequently ten times the file size of non-MFC constructed DLLs. This not only consumes more of the host system's memory, it also degrades performance. You will have to make a choice about how you develop ISAs. Using MFCs allows for quicker development, so you might find it beneficial to first develop the application using MFCs. Then, as resources demand, you can rework the application without the classes.

The next few sections provide an overview of these classes. We'll also round out the ISAPI picture by describing a few items mentioned earlier, such as the Server Support Function and the HTTP environment variables.

CHttpServer Object

The CHttpServer object is created (instantiated) when the server loads the DLL. Only one CHttpServer is created, when a DLL request hits the server. To handle the first request and each subsequent request, CHttpServer constructs a CHttpServerContext object. Each CHttpServerContext object runs in a separate thread, allowing multiple CHttpServerContext objects to be executing simultaneously and independently.

CHttpServerContext objects may complete their execution in any order, depending on what tasks they perform. Thus the order in which these objects are created does not determine the order in which they will execute and complete their tasks.

Constructor

This statement constructs a new CHttpServer object:

CHttpServer( TCHAR cDelimiter );

The cDelimiter parameter indicates what character will be used as a field delimiter, which the DLL will send back to the client in the response URL. The default field delimiter character is the ampersand (&).

Overridable Methods

This function:

virtual int CallFunction( CHttpServerContext* pCtxt, LPTSTR pszQuery, LPTSTR pszCommand );

is used by the Parse Map macros to find and execute the specified function. Parse Map macros are explained later in "ISAPI Extensions and Parse Maps."

This function:

virtual BOOL OnParseError( CHttpServerContext* pCtxt, int nCause );

returns an HTML error message to the client if the request is unsuccessful. The error message is determined by the value of the dwHttpStatusCode data member in the ECB. Defined error messages and corresponding HTTP status codes are listed in Table 7.5.

Table 7.5 HTTP Status Codes
Error Message
Code
HTTP_STATUS_BAD_REQUEST
400
HTTP_STATUS_AUTH_REQUIRED
401
HTTP_STATUS_FORBIDDEN
403
HTTP_STATUS_NOT_FOUND
404
HTTP_STATUS_SERVER_ERROR
500
HTTP_STATUS_NOT_IMPLEMENTED
501

This "main" function is now a part of CHttpServer:

virtual DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );

and is used to read the client request and determine which function within the extension to execute. The HttpExtensionProc function can return various success or error messages, but normally you will not see these messages unless you choose to override HttpExtensionProc.

This next function is one of the two required DLL entry-point functions:

virtual CHtmlStream* ConstructStream( );

The following function is called by default:

virtual BOOL GetExtensionVersion( HSE_VERSION_INFO *pVer );

It creates the HTML stream object, CHtmlStream, which is the stream that sends data back to the client. The function can be overridden if application performance needs to be fine-tuned.

Attributes

This function:

void StartContent( CHttpServerContext* pCtxt ) const;

inserts <BODY><HTML> tags at the start of the HTML stream sent back to the client.

This function:

void EndContent( CHttpServerContext* pCtxt ) const;

inserts corresponding </BODY></HTML> tags into the HTML stream.

This function:

void WriteTitle( CHttpServerContext* pCtxt ) const;

calls the GetTitle function and writes the text returned between <TITLE> and </TITLE> tags.

This function is called by WriteTitle:

virtual LPCTSTR GetTitle( ) const;

It sets the text between the <TITLE> and </TITLE> tags.

The AddHeader function:

void AddHeader( CHttpServerContext* pCtxt, LPCTSTR pszString ) const;

adds a header string to the HTML stream before the data is sent back by the client. Call this function before putting data into the HTML stream, with the << operator or with a call to either StartContent or WriteTitle. (The << operator is described in the upcoming section on CHttpServerContext.) Defined HTTP headers are listed in Table 7.6.

NOTE
About extension-header headers: It used to be standard practice to prefix user-defined headers with HTTP_, but this is no longer considered necessary. Also keep in mind that not all browsers or servers will necessarily recognize nonstandard headers. Unrecognized headers will simply be ignored by the server or browser (assuming the proper expected headers are in place).

This function:

virtual BOOL InitInstance(CHttpServerContext* pCtxt );

initializes the CHttpServer object.

Table 7.6 HTTP Headers
HeaderDefinition
Entity-HeaderCan contain meta-information about the body of request or return data.
Content-LengthIf the client request is from a POST method form, this is the length of the data sent in the request body.
Content-TypeIdentifies the MIME type of the data, such as text/html, application/octet-stream.
Content-EncodingIdentifies any additional encoding applied to the Content-Type.
ExpiresA possible value for Entity-Header.
Last-ModifiedModification date of the file.
extension-headerUser-defined headers.

CHttpServerContext Object

A CHttpServerContext object is created by the CHttpServer object to handle each client request.

Data Members

The member

m_pECB

contains a pointer to the Extension Control Block for this DLL. The member

m_pStream

contains a pointer to the stream object created by CHtmlStream. Data is written to this stream and sent to the client when the function called in the URL request has completed.

Constructor

The following constructor:

CHttpServerContext( EXTENSION_CONTROL_BLOCK* pECB );

initializes a new CHttpServerContext object for each new client request.

Operations

The WriteClient function

BOOL WriteClient( LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved );

writes data immediately to the HTML stream object, before the CHttpServerContext << operator. Take care how and where you use this function. If the proper HTTP headers have not already been sent, unexpected results will follow.

The ReadClient function

BOOL ReadClient( LPVOID lpvBuffer, LPDWORD lpdwSize );

reads data from the body of the client request. The body will usually only contain data when a POST method form is sent. If you are using the Parse Map macros (explained later in "ISAPI Extensions and Parse Maps"), then you don't need to use ReadClient to get at the form variables.

This function:

BOOL GetServerVariable( LPTSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSize );

returns one of the HTTP environment variables. Available variables are listed in Table 7.7.

Table 7.7 HTTP Environment Variables
Environment VariableDefinition
CONTENT_LENGTHThe length of data buffer sent by the client.
CONTENT_TYPEThe MIME type of the incoming or outgoing data, such as text/html, image/gif, etc.
GATEWAY_INTERFACEThe server CGI type and revision level, in the format CGI/revision.
PATH_INFOSet by a URL containing an extra trailing slash (/) character. PATH_INFO is the string following the extra slash.
PATH_TRANSLATEDThe server translates the virtual path represented by PATH_INFO into a physical path on the server file system.
QUERY_STRINGThis variable is filled by data, following a question mark (?) character, in a URL.
REMOTE_ADDRThe IP address of the client.
REMOTE_HOSTThe client hostname. If the server cannot determine this, it still has access to the REMOTE_ADDR variable and leaves this unset.
REQUEST_METHODThis can be a GET, a POST, or a HEAD.
SCRIPT_NAMEA virtual path to the script being executed.
SERVER_NAMEThe server's hostname, DNS alias, or IP address.
SERVER_PORTThe port number that received the client request; port 80 is standard.
SERVER_PROTOCOLThe protocol being used by the client request; in the format protocol/revision; for example, HTTP/1.0 or HTTP/1.1.
SERVER_SOFTWAREThe name and version of the server software, in the format name/version.
AUTH_TYPEIf request is authenticated, this indicates the type, such as basic, NTLM, etc.
REMOTE_USERIf the server supports user authentication, and an object on the server is protected, this is the authenticated username.
AUTH_PASSIf a successful authentication occurs, this contains the password.
HTTP_ACCEPTIndicates the MIME types the client browser supports or can accept.
ALL_HTTPIncludes any other environment variables, such as HTTP_CONNECTION, HTTP_HOST, HTTP_USER_AGENT, and HTTP_PRAGMA.

Here is the important ServerSupportFunction:

BOOL ServerSupportFunction( DWORD dwHSERRequest, LPVOID lpvBuffer, LPDWORD lpdwSize, LPDWORD lpdwDataType ); 

It provides a number of utility-type functions for handling a request, including the following:

  • HSE_REQ_SEND_URL_REDIRECT_RESP sends a redirect header to the client, which causes the client to then load the specified URL. You should use this function if you are redirecting the client to a location outside of the current Web site. Example:
void CCodeExtension::Redirect(CHttpServerContext* pCtxt, LPSTR lpDummy)
{
BOOL SSFResult;
DWORD dwsizeofURL = sizeof(lpDummy);

SSFResult = pCtxt->ServerSupportFunction(
               HSE_REQ_SEND_URL_REDIRECT_RESP, 
               lpDummy, &dwsizeofURL, NULL);	
}
  • HSE_REQ_SEND_URL loads a local file without sending the 302 message in the previous function.
  • HSE_REQ_SEND_RESPONSE_HEADER sends an HTTP response header to the client with the HTTP server version, status code, time of the response, and MIME type. It does not include the Content-type, however, so you need to send that (and any other HTTP headers) immediately after calling this function and prior to sending any data to the client.

NOTE
Remember, also, that the HTTP header block should end with an extra blank line between the headers and the data. For example, if the last header you send is Content-type, you might define that string as.

LPSTR lpContentType = "Content-type:  text/html\r\n\r\n";
  • HSE_REQ_DONE_WITH_SESSION notifies the server that the server has completed sending data to the client and can disconnect the TCP/IP connection. The DLL, however, may have additional work, which it can continue doing after the connection has closed.
  • HSE_REQ_MAP_URL_TO_PATH can be used to determine either the logical path when the client makes a request (such as the virtual path specified in the request URL); or the real, physical path on the server when the outgoing data is sent to the client.
  • HSE_REQ_GET_SSPI_INFO returns the context and credential handles sent by the client.
  • HSE_REQ_END_RESERVED indicates the upper boundary number of the "reserved" functions.

Operators

The << operator is used to add data to the HTML stream object for delivery back to the client. This stream data is not sent to the client until the requested function returns.

void operator<<(LPCTSTR psz);
void operator<<(short int w);
void operator<<(long int dw);
void operator<<(CHtmlStream& stream);
void operator<<(double d);
void operator<<(float f);

As mentioned previously, if you mix usage of this operator with functions that add headers, or with the WriteClient function, be careful about the order of the data.

CHttpFilter Object

The CHttpFilter object is created when the server loads a filter DLL. Only one CHttpFilter object is created for each filter DLL loaded when the IIS service starts. Each client request causes CHttpFilter to create a CHttpFilterContext object to handle the client request.

Constructor

This function:

CHttpFilter( );

is called when IIS starts up and loads the DLL filters specified in the registry.

Attributes

This is the entry point that the server calls when it loads the DLL filter:

extern "C" BOOL WINAPI GetFilterVersion( PHTTP_FILTER_VERSION pVer );

In addition to initializing the CHttpFilter object and exchanging version information with the server, GetFilterVersion also includes the notification and priority flags in which the filter is interested.

Overridables

The overridable methods are the functions called when an event occurs for which a notification flag exists in the GetFilterVersion function.

The default implementations of each of these functions do nothing. In our various tests, the order in which the flags are specified did not affect the order in which the functions get called. This makes sense; for instance, OnReadRawData will get called before OnSendRawData, OnUrlMap, or OnLog.

This function is CALLED when the client request is received by the server:

virtual DWORD OnReadRawData( CHttpFilterContext* pfc, PHTTP_FILTER_RAW_DATA pRawData ); 

It will contain the complete client request, including the incoming HTTP headers and the body of the request from a POST method form. The data structure is defined as follows:

typedef struct _HTTP_FILTER_RAW_DATA
{
PVOID          pvInData;
DWORD          cbInData;
DWORD          cbInBuffer;
DWORD          dwReserved;
} HTTP_FILTER_RAW_DATA, *PHTTP_FILTER_RAW_DATA;

where pvInData points to the actual data; cbIndata indicates the amount of data in the buffer; and cbInBuffer is the size of the buffer.

This function:

virtual DWORD OnPreprocHeaders( CHttpFilterContext* pfc, PHTTP_FILTER_PREPROC_HEADERS pHeaders ); 

is called when the server has finished reading and processing the client request headers. This is a good place to examine the client headers if your DLL expects to take some action based on the headers. Note that attempting to read the headers in OnReadRawData is not always reliable, as not all of the header data will be available at that time.

The definition of the structure for this function consists of three functions. These functions allow you to retrieve and set the values of the HTTP headers.

typedef struct _HTTP_FILTER_PREPROC_HEADERS
{
BOOL (WINAPI * GetHeader) (
STRUCT          _HTTP_FILTER_CONTEXT * PFC,
LPSTR          lpszName,
LPVOID          lpvBuffer,
LPDWORD     lpdwSize
);
BOOL (WINAPI * SetHeader) (
STRUCT          _HTTP_FILTER_CONTEXT * PFC,
LPSTR          lpszName,
LPSTR          lpszValue
);
BOOL(WINAPI * AddHeader) (
STRUCT          _HTTP_FILTER_CONTEXT * PFC,
LPSTR          lpszName,
LPSTR          lpszValue
);
DWORD          dwReserved;

A Note About Format: The VC++ reference material indicates that you should include the trailing colon for when specifying the header name. We found the opposite to be the case. For example, these lines:

     LPSTR szHeader = "url";
     CHAR lpBuffer[128];
     DWORD dwSize = sizeof(lpBuffer);
     BOOL bHeader = pHeaderInfo->GetHeader(pCtxt->m_pFC, szHeader, lpBuffer, &dwSize);

will return True. We also found that the order in which you attempt to get the headers can make a difference. For instance, if you retrieve the METHOD header before the URL header, the call to get the URL header will return False.

This next function:

virtual DWORD OnAuthentication( CHttpFilterContext* pfc, PHTTP_FILTER_AUTHENT pAuthent ); 

is called when client authentication occurs. It allows you to retrieve the UserID and Password when an authentication event occurs. The structure for this function is as follows:

typedef struct _HTTP_FILTER_AUTHENT{
CHAR*          pszUser;
DWORD          cbUserBuff;
CHAR*          pszPassword;
DWORD          cbPasswordBuff;
} HTTP_FILTER_AUTHENT, *PHTTP_FILTER_AUTHENT;

This event will occur twice for each authentication request. For example, consider the following TRACE statements:

DWORD CAuth1Filter::OnAuthentication(CHttpFilterContext* pCtxt, PHTTP_FILTER_AUTHENT pAuthent)
{
     TRACE("pszUser=%s\n", pAuthent->pszUser);
     TRACE("pszPassword=%s\n", pAuthent->pszPassword);
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

The TRACE output would look like this:

pszUser=
pszPassword=
pszUser=UserX
pszPassword=ZD_one

OnUrlMap is called when the filter maps the requested file's logical path to the real, physical path of the actual file on the server.

virtual DWORD OnUrlMap( CHttpFilterContext* pfc, PHTTP_FILTER_URL_MAP pUrlMap ); 

The pUrlMap parameter will contain this physical path value:

typedef struct _HTTP_FILTER_URL_MAP
{
const CHAR *     pszURL;
CHAR *          pszPhysicalPath;
DWORD          cbPathBuff;
} HTTP_FILTER_URL_MAP, *PHTTP_FILTER_URL_MAP;

Note that although you can retrieve and modify these values, simply modifying pszPhysicalPath will not redirect the client to the new value in pszPhysicalPath. Instead, use the ServerSupportFunction to send an HTTP Location header to the client to cause the redirect.

This function:

virtual DWORD OnSendRawData( CHttpFilterContext* pfc, PHTTP_FILTER_RAW_DATA pRawData ); 

is called when the filter is sending data back to the client. The data returned by the server will already contain HTTP headers, so adding headers by the time OnSendRawData is called can lead to unexpected results. The raw data structure is defined as follows:

typedef struct _HTTP_FILTER_RAW_DATA
{
PVOID          pvInData;
DWORD          cbInData;
DWORD          cbInBuffer;
DWORD          dwReserved;
} HTTP_FILTER_RAW_DATA, *PHTTP_FILTER_RAW_DATA;

The OnLog function

virtual DWORD OnLog( CHttpFilterContext* pfc, PHTTP_FILTER_LOG pLog ); 

is called when the server is about to log data to the file specified in the IIS Logging property sheet. You can use the OnLog function to alter the data that IIS writes to the log file. Here is the structure of the data that IIS logs:

typedef struct _HTTP_FILTER_LOG
{
const CHAR *    pszClientHostName;
const CHAR *    pszClientUserName;
const CHAR *    pszServerName;
const CHAR *    pszOperation;
const CHAR *    pszTarget;
const CHAR *    pszParameters;
DWORD        dwHttpStatus;
DWORD        dwWin32Status;
} HTTP_FILTER_LOG, *PHTTP_FILTER_LOG;

It's important to note here that you cannot change this structure; nor can you add data to what is logged. You can, however, alter the data that is logged-for example, by replacing the value of the pszTarget string. Another field to consider changing is the pszServerName: IIS does not do any lookup of the ClientHostName and so by default just logs the client's IP number. The ServerName field contains the IP number for the server host machine and is thus often disposable. This chapter's section of sample ISAPI programs contains code for changing the data that is written to the IIS log.

This function:

virtual DWORD OnEndOfNetSession( CHttpFilterContext* pfc );

is called when the server has finished processing the client request and is about to close the connection with the client.

Operations

HttpFilterProc is the entry point that the server will call when a client request is received.

extern "C" DWORD WINAPI HttpFilterProc( PHTTP_FILTER_CONTEXT pfc, 
DWORD NotificationType, LPVOID pvNotification );

This function looks at the flags specified in GetFilterVersion and calls the associated function (if it exists). There are six notification types to which HttpFilterProc will respond, as described in the earlier section on CHttpFilter Overridables.

CHttpFilterContext Object

CHttpFilterContext is the object created by CHttpFilter to handle a client request. One of these is created for each client request.

Data Members

The member

m_pFC

points to the HTTP_FILTER_CONTEXT structure.

Constructor

This function:

CHttpFilterContext( PHTTP_FILTER_CONTEXT pfc );

creates the CHttpFilterContext object.

Attributes

All of the following work the same as their CHttpExtension versions. See the corresponding descriptions under CHttpServerContext.

BOOL GetServerVariable( LPTSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSize ); 
BOOL AddResponseHeaders( LPTSTR lpszHeaders, DWORD dwReserved =0 );
BOOL WriteClient( LPVOID lpvBuffer, LPDWORD lpdwBytes, DWORD dwReserved );
LPVOID AllocMem( DWORD cbSize, DWORD dwReserved );
BOOL ServerSupportFunction(enum SF_REQ_TYPE sfReq, 
PVOID pvData, LPDWORD lpdwSize, LPDWORD lpdwDataType )

CHtmlStream Object

CHtmlStream is an object created by filters and extensions to hold HTML data. The ISA writes data to this stream object to send back to the client. Although you might have occasion to use the CHtmlStream member functions for fine-tuning the creation of the HTML stream object, in general CHtmlStream is used by the MFC framework and you will not need to do anything with it.

Data Members

The member

m_nStreamSize

is protected and contains the size of the HTML stream.

Constructor

The constructor function is called by either CHttpServer or CHttpFilter:

CHtmlStream( UINT nGrowBytes = 4096 );
CHtmlStream( BYTE* lpBuffer, UINT nBufferSize, UINT nGrowBytes = 0 );

The first version of CHtmlStream can be used to alter the size of the memory blocks that are added to the stream object. If substantial memory is available, increasing the size of nGrowBytes can improve performance.

Operations

This function:

void Attach( BYTE* lpBuffer, UINT nBufferSize, UINT nGrowBytes = 0 );

attaches the specified block of memory to the HTML stream.

This function:

DWORD GetStreamSize( ) const;

returns the size of the stream in bytes, for example:

DWORD dwStrmSize = pCtxt->m_pStream->GetStreamSize();

This function closes the HTML stream:

virtual void Close( );

and this one initializes a new HTML stream:

virtual void InitStream( );

Overridables

This function:

virtual void Abort( );

closes the HTML stream and the connection to the client.

This function:

virtual void Reset( );

clears and resets the HTML stream.

This function:

virtual BYTE* Alloc( DWORD nBytes );

allocates additional memory for the stream.

This function:

virtual BYTE* Realloc( BYTE* lpMem, DWORD nBytes );

changes the size of the memory block.

This function:

virtual BYTE* Memcpy( BYTE* lpMemTarget, const BYTE* lpMemSource, UINT nBytes );

allows you to copy one memory block to another.

This function:

virtual void Free( BYTE* lpMem );

unallocates the indicated memory block.

This function:

BYTE* Detach( );

closes the memory block without destroying it. To reuse it later, you must first call GetStreamSize().

This function:

virtual void GrowStream( DWORD dwNewLen );

increases the size of the memory block.

This function:

virtual void Write( const void* lpBuf, UINT nCount );

writes the indicated buffer to the HTML stream memory block. As with the WriteClient function, take care with the order in which you write data to the HTML stream.

Operators

The << operator in CHtmlStream works the same as in the other classes, adding data to the end of the HTML stream.

void operator <<( LPCTSTR sz );
void operator <<( WORD w );
void operator <<( DWORD dw );
void operator <<( CHtmlStream& stream )

Problems with the ISAPI AppWizard Files

Beginning with Visual C++ 4.1, an ISAPI AppWizard was included with the compiler. It was capable of generating either a skeleton ISAPI extension or a filter. The AppWizard properly generates the extension skeleton, and a bit further on, in the section on Parse Maps, we'll look at an extension application.

Note, however, that the AppWizard does not properly generate the skeleton for a filter application. This problem is also true of the 4.2 version of VC++. Let's take a look at how the extension skeleton should look, beginning with the .cpp file in Listing 7.3. Then in Listing 7.4 you'll see the corresponding header file.


Listing 7.3 A skeleton ISAPI filter.cpp file
// BROWSER.CPP - Implementation file for your Internet Server
//    browser Filter
#include "stdafx.h"
#include "browser.h"

///////////////////////////////////////////////////////////////////////
// The one and only CWinApp object
// NOTE: You may remove this object if you alter your project to no
// longer use MFC in a DLL.
CWinApp theApp;

///////////////////////////////////////////////////////////////////////
// The one and only CBrowserFilter object
CBrowserFilter theFilter;

///////////////////////////////////////////////////////////////////////
// CBrowserFilter implementation

CBrowserFilter::CBrowserFilter()
{
}

CBrowserFilter::~CBrowserFilter()
{
}

BOOL CBrowserFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
     // Call default implementation for initialization
     CHttpFilter::GetFilterVersion(pVer);

     // Clear the flags set by base class
     pVer->dwFlags &= ~SF_NOTIFY_ORDER_MASK;

     // Set the flags we are interested in
     pVer->dwFlags |=     SF_NOTIFY_ORDER_LOW 
          | SF_NOTIFY_SECURE_PORT 
          | SF_NOTIFY_NONSECURE_PORT 
          | SF_NOTIFY_LOG 
          | SF_NOTIFY_AUTHENTICATION 
          | SF_NOTIFY_PREPROC_HEADERS 
          | SF_NOTIFY_READ_RAW_DATA 
          | SF_NOTIFY_SEND_RAW_DATA 
          | SF_NOTIFY_URL_MAP 
          | SF_NOTIFY_END_OF_NET_SESSION;

     // Load description string
     TCHAR sz[SF_MAX_FILTER_DESC_LEN+1];
     ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
               IDS_FILTER, sz, SF_MAX_FILTER_DESC_LEN));
     _tcscpy(pVer->lpszFilterDesc, sz);
     return TRUE;
}

DWORD CBrowserFilter::OnPreprocHeaders(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_PREPROC_HEADERS pHeaderInfo)
{
     // TODO: React to this notification accordingly and
     // return the appropriate status code
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CBrowserFilter::OnAuthentication(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_AUTHENT pAuthent)
{
     // TODO: React to this notification accordingly and
     // return the appropriate status code
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CBrowserFilter::OnUrlMap(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_URL_MAP pMapInfo)
{
     // TODO: React to this notification accordingly and
     // return the appropriate status code
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CBrowserFilter::OnSendRawData(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_RAW_DATA pRawData)
{
     // TODO: React to this notification accordingly and
     // return the appropriate status code
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CBrowserFilter::OnReadRawData(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_RAW_DATA pRawData)
{
     // TODO: React to this notification accordingly and
     // return the appropriate status code
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CBrowserFilter::OnEndOfNetSession(CHttpFilterContext* pCtxt)
{
     // TODO: React to this notification accordingly and
     // return the appropriate status code
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CBrowserFilter::OnLog(CHttpFilterContext* pCtxt, PHTTP_FILTER_LOG pLog) 
{
     // This is the function not created by the App Wizard
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

// Do not edit the following lines, which are needed by ClassWizard.
#if 0
BEGIN_MESSAGE_MAP(CBrowserFilter, CHttpFilter)
     //{{AFX_MSG_MAP(CBrowserFilter)
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()
#endif     // 0

///////////////////////////////////////////////////////////////////////
// If your extension will not use MFC, you'll need this code to make
// sure the extension objects can find the resource handle for the
// module.  If you convert your extension to not be dependent on MFC,
// remove the comments arounn the following AfxGetResourceHandle()
// and DllMain() functions, as well as the g_hInstance global.

/****

static HINSTANCE g_hInstance;

HINSTANCE AFXISAPI AfxGetResourceHandle()
{
     return g_hInstance;
}

BOOL WINAPI DllMain(HINSTANCE hInst, ULONG ulReason,
                         LPVOID lpReserved)
{
     if (ulReason == DLL_PROCESS_ATTACH)
     {
          g_hInstance = hInst;
     }

     return TRUE;
}

****/


Listing 7.4 A skeleton ISAPI filter .h file
// BROWSER.H - Header file for your Internet Server
//    browser Filter
#include "resource.h"

class CBrowserFilter : public CHttpFilter
{
public:
     CBrowserFilter();
     ~CBrowserFilter();

// Overrides
     // ClassWizard generated virtual function overrides
          // NOTE - the ClassWizard will add and remove member functions here.
          //    DO NOT EDIT what you see in these blocks of generated code !
     //{{AFX_VIRTUAL(CBrowserFilter)
     public:
     virtual BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer);
     virtual DWORD OnPreprocHeaders(CHttpFilterContext* pCtxt, PHTTP_FILTER_PREPROC_HEADERS pHeaderInfo);
     virtual DWORD OnAuthentication(CHttpFilterContext* pCtxt, PHTTP_FILTER_AUTHENT pAuthent);
     virtual DWORD OnUrlMap(CHttpFilterContext* pCtxt, PHTTP_FILTER_URL_MAP pMapInfo);
     virtual DWORD OnSendRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
     virtual DWORD OnReadRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
     virtual DWORD OnEndOfNetSession(CHttpFilterContext* pCtxt);
     virtual     DWORD OnLog(CHttpFilterContext* pCtxt, PHTTP_FILTER_LOG pLog);
     //}}AFX_VIRTUAL

     //{{AFX_MSG(CBrowserFilter)
     //}}AFX_MSG
};

You can either edit the two files manually, as we have in Listings 7.3 and 7.4, or you can use the ClassWizard to add a function for the OnLog event. However, if you use ClassWizard to do this, be advised that it creates a new variable to point to the CHttpFilterContext, which just adds to the confusion.

NOTE
Another alternative you might want to try is to create your own custom AppWizard. We started to do this but were amused to find that AppWizards created in VC++ 4.2 won't run in the 4.2 IDE without some workaround. So we abandoned that idea for the time being.

ISAPI Extensions and Parse Maps

When creating ISAPI extensions, the AppWizard generates a skeleton extension with the GetExensionVersion function, a Default function (in place of the HttpExtensionProc we saw with the non-MFC example), and a message map should you choose to add functions with the ClassWizard. In addition, the AppWizard generates a skeleton Parse Map. These ISAPI Parse Maps are macros that read the body of a GET or POST method form, placing the values associated with the HTML form input into variables that you specify in the Parse Map. Using the Parse Maps to read form data greatly simplifies this task.

In this section, we'll show you the basics of using the ISAPI Parse Maps.

Adding a Parse Map to Source Code

To use the Parse Maps, place the following in your header file:

DECLARE_PARSE_MAP()

In the source code, you will use the following five macros to specify the Parse Map:

     BEGIN_PARSE_MAP(derived_class, base_class)
     ON_PARSE_COMMAND(function, derived_class,args)
     ON_PARSE_COMMAND_PARAMS(parameters)
     DEFAULT_PARSE_COMMAND
     END_PARSE_MAP(derived_class)

For example, let's say we have an HTML page with the tagged items as follows:

<FORM METHOD=GET ACTION="/sample.dll?FunctionOne">
<INPUT TYPE=TEXT NAME="question">
<INPUT TYPE=SUBMIT VALUE="Ask">
</FORM>

If our extension is named Question, and our derived_class is CQuestionExtension, then our Parse Map would look like this:

     BEGIN_PARSE_MAP(CQuestionExtension, CHttpServer)
     ON_PARSE_COMMAND(FunctionOne, CQuestionExtension, ITS_PSTR)
     ON_PARSE_COMMAND_PARAMS("question")
     DEFAULT_PARSE_COMMAND
     END_PARSE_MAP(CQuestionExtension)

The parameter types you can use in a Parse Map are shown in Table 7.8.

Table 7.8 Parse Map Parameter Types
Parse Map DeclarationData Type
ITS_EMPTYNo arguments
ITS_PSTRString
ITS_I2Short
ITS_I4Long
ITS_R4Float
ITS_R8Double

The argument types listed in the ON_PARSE_COMMAND_PARAMS function must be in the same order as the parameter names supplied in ON_PARSE_COMMAND. Otherwise, the macro will not expand properly at build time. For example, let's say we add an input box to the above "Question" example. We want our ISAPI program to receive the first value as a string, and the second value as a short number. So the two corresponding Parse Map lines could look like this:

     ON_PARSE_COMMAND(FunctionOne, CQuestionExtension, ITS_PSTR ITS_I2)
     ON_PARSE_COMMAND_PARAMS("question number")

In the two lines just above, the Parse Map is set up to require that the user supply something for both <INPUT> variables in the HTML form. If the user clicked on the Submit button without typing something in both of the input boxes, an error window would appear. To avoid this, you can supply default values for the optional HTML input elements. You'd specify the parameter with =[value] following the parameter name. For instance, you can change the preceding example to set a default value for the question parameter, like this:

     ON_PARSE_COMMAND(FunctionOne, CQuestionExtension, ITS_PSTR ITS_I2)
     ON_PARSE_COMMAND_PARAMS("question='What is the point?' number")

The ISAPI documents suggest setting the default value of a parameter to the tilde character (~), which is a reasonable solution as it is not a character commonly required in fill-out forms (such as a form requesting an address). Although supplying default values allows for missing input values, a user can still send a request with too many parameters (by typing the request in the URL box in the browser). This will result in a "Bad Request" type of error.

The required parameters-that is, parameters with no default values set-must appear first in the argument and parameter lists. These lists do not, however, have to follow the same order as the appearance of the fields in an HTML form. For example, consider an HTML form that has three input boxes named One, Two, and Three. The input boxes appear in that order in the HTML form. We only want to require a value for Two, though, so in our Parse Map the code could look like this:

     ON_PARSE_COMMAND(FunctionOne, CQuestionExtension, ITS_PSTR ITS_PSTR ITS_PSTR)
     ON_PARSE_COMMAND_PARAMS("two one=~ three=~")

A Parse Map Example

The next example program, form.cpp, shows what happens when various HTML form elements are passed using the Parse Map macros. Our HTML form (Figure 7.3) incorporates some elements prevalent in fill-out forms. Listing 7.5 shows the source code for form.cpp, which simply reads in the values from the form and sends them back to the client.

Figure 7.3 : HTML form used to investigate the Parse Map macros.


Listing 7.5 form.cpp
// FORM.CPP - Implementation file for your Internet Server
//    form Extension

#include "stdafx.h"
#include "form.h"

///////////////////////////////////////////////////////////////////////
// command-parsing map

BEGIN_PARSE_MAP(CFormExtension, CHttpServer)
     ON_PARSE_COMMAND(Readform, CFormExtension, ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR)
     ON_PARSE_COMMAND_PARAMS("Textbox=~ Checkbox=~ Radio=~ Hidden=~ Select=~ Textarea=~")
     ON_PARSE_COMMAND(Default, CFormExtension, ITS_PSTR)
     DEFAULT_PARSE_COMMAND(Default, CFormExtension)
END_PARSE_MAP(CFormExtension)

///////////////////////////////////////////////////////////////////////
// The one and only CFormExtension object
CFormExtension theExtension;

///////////////////////////////////////////////////////////////////////
// CFormExtension implementation

CFormExtension::CFormExtension()
{
}

CFormExtension::~CFormExtension()
{
}

BOOL CFormExtension::GetExtensionVersion(HSE_VERSION_INFO* pVer)
{
     // Call default implementation for initialization
     CHttpServer::GetExtensionVersion(pVer);

     // Load description string
     TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];
     ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
               IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));
     _tcscpy(pVer->lpszExtensionDesc, sz);
     return TRUE;
}

///////////////////////////////////////////////////////////////////////
// CFormExtension command handlers

void CFormExtension::Default(CHttpServerContext* pCtxt)
{
     StartContent(pCtxt);
     WriteTitle(pCtxt);

     *pCtxt << _T("This default message was produced by the Internet");
     *pCtxt << _T(" Server DLL Wizard. Edit your CFormExtension::Default()");
     *pCtxt << _T(" implementation to change it.\r\n");

     EndContent(pCtxt);
}

void CFormExtension::Readform(CHttpServerContext* pCtxt,
LPSTR Textbox, LPSTR Checkbox, LPSTR Radio, LPSTR Hidden, 
LPSTR Select, LPSTR Textarea)
{

     StartContent(pCtxt);
     WriteTitle(pCtxt);

     *pCtxt << _T("<HR>");
     *pCtxt << _T("Textbox: ") << Textbox << _T("<BR><BR>");
     *pCtxt << _T("Checkbox: ") << Checkbox << _T("<BR><BR>");
     *pCtxt << _T("Radio: ") << Radio << _T("<BR><BR>");
     *pCtxt << _T("Hidden: ") << Hidden << _T("<BR><BR>");
     *pCtxt << _T("Select: ") << Select << _T("<BR><BR>");
     *pCtxt << _T("Textarea: ") << Textarea << _T("<BR><BR>");
     
     LONG cbTotalBytes = (LONG)pCtxt->m_pECB->cbTotalBytes;
     LONG cbAvailable = (LONG)pCtxt->m_pECB->cbAvailable;
     LPBYTE lpbData = pCtxt->m_pECB->lpbData;
     UCHAR * chData = lpbData;

     TCHAR lpszData[512];
     lstrcpyn(lpszData, (LPCTSTR)chData, cbTotalBytes);

     *pCtxt << _T("cbTotalBytes = ") << cbTotalBytes << _T("<BR>");
     *pCtxt << _T("cbAvailable = ") << cbAvailable << _T("<BR>");
     *pCtxt << _T("lpbData = ") << lpszData << _T("<HR>");

     EndContent(pCtxt);

}

// Do not edit the following lines, which are needed by ClassWizard. 
// And don't put anything after them either.  We warned you!
#if 0
BEGIN_MESSAGE_MAP(CFormExtension, CHttpServer)
     //{{AFX_MSG_MAP(CFormExtension)
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()
#endif     // 0


Listing 7.6 form.h
// FORM.H - Header file for your Internet Server
//    form Extension

#include "resource.h"

class CFormExtension : public CHttpServer
{
public:
     CFormExtension();
     ~CFormExtension();
// Overrides
     // ClassWizard generated virtual function overrides
          // NOTE - the ClassWizard will add and remove member functions here.
          //    DO NOT EDIT what you see in these blocks of generated code !
     //{{AFX_VIRTUAL(CFormExtension)
     public:
     virtual BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);
     //}}AFX_VIRTUAL

     // TODO: Add handlers for your commands here.
     // For example:

     void Default(CHttpServerContext* pCtxt);
//     void Readform(CHttpServerContext* pCtxt,LPSTR Textbox);

     void Readform(CHttpServerContext* pCtxt,
LPSTR Textbox, LPSTR Checkbox, LPSTR Radio, LPSTR Hidden, 
LPSTR Select, LPSTR Textarea);



     DECLARE_PARSE_MAP()

     //{{AFX_MSG(CFormExtension)
     //}}AFX_MSG
};

Listing 7.6 shows the header file for this application, and Figure 7.4 illustrates typical output. Both the header file and the .cpp source were created with the ISAPI AppWizard. All we added to the .cpp file were the Readform function; the two additional lines to the Parse Map indicating the name of our new function; and the argument types, followed by the Params line indicating the argument names.

Figure 7.4 : Output from the ReadForm function in form.cpp.

Parse Map Limits

Note that for the Checkbox element we only specified one parameter in the Parse Map. This is an example of when the Parse Maps start to become unwieldy-particularly if the HTML is dynamically created and there may be varying occurrences of some element (such as a check box) appearing on the page. In a situation like this you have to code into the Parse Maps some maximum number of potential check-box occurrences, specifiying the values as optional.

The alternative is to parse the form output manually. In the above "Form" example, we show at the end of the ReadForm function how to get at the data from the form, by displaying the raw data at the bottom of the output screen. To get an idea of how to parse this raw data, see the Formdump example on the ActiveX SDK.

Debugging ISAPI Programs

Since ISAPI applications are DLLs, additional steps are needed to debug the program in Developer Studio. In general, we found that the steps described here made debugging and tracing through an ISA a relatively painless process. (Occasionally, however, a misbehaving DLL can still get hung up in memory, and the IIS executable, inetinfo.exe, can't be stopped by InetMgr or Pview. The only option is to reboot the machine.)

IIS Extension Caching

Normally, when IIS loads a DLL, it will stay in memory until the server decides to unload it (for extensions), or until the ISS service is stopped (for filters). So the first thing to do when developing ISAPI applications is to turn off extension caching.

In the Registry, find the line

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters

and change the value of CacheExtensions to 0. Setting the value to 0 causes the service to unload extension DLLs after use.

Debugging in Developer Studio

To debug an ISA in the IDE, you need to start up the IIS service as the executable for your DLL. Technical Note #63 in the VC++ IDE describes how to load the IIS executable, Inetinfo.exe, in the Developer Studio debugger.

  1. Stop the WWW and other internet services with Inet Manager. You must stop all WWW services.

TIP
You may want to use the Services option in Control Panel to change the startup of these services to manual, just to avoid this step while developing applications.

  1. Open the workspace file for the DLL being debugged. Select Build | Settings | Debug from the menu.
  2. For the "Executable for debug sessions" line, add the following:
    [your pathname]\inetinfo.exe 
  1. For Program arguments, enter the following:
    -e W3Svc

and click on OK.

  1. Make sure you are logged on as an NT user with an appropriate level of system privileges, including "Act as part of the operating system" and "Generate security audits."
  2. Now, to run the DLL under the debugger, select Build | Debug | Go (or press F5). This loads the W3Svc service in the debugger.

The DLL can be tested in a separate window running Internet Explorer or another browser. You can step and trace through the ISAPI application just as you can any normal executable; the client browser will simply wait while you do this. In addition, if you have one filter DLL entered in the Registry and are debugging another ISA, either an extension or a filter, you will still see any trace statements from the first filter DLL while debugging the current ISA.

Upon unloading the ISA, an "access violation" message is usually generated, which can be ignored.

ISAPITRACE, ISAPIVERIFY, and ISAPIASSERT

Now that there's no misbehaving DLL crashing the environment, it's time to add some statements to help with the debugging process.

The ISAPITRACE statement works much like the TRACE macro, allowing the developer to dump information to the Output Window. ISAPITRACE employs a sprintf syntax for printing the values of variables.

The ISAPIVERIFY macro corresponds to the VERIFY macro. An expression is provided; if it fails, a diagnostic message is printed and the program halts. Similiarly, ISAPIASSERT corresponds to the ASSERT macro. ISAPIVERIFY statements can be left in the source code when preparing a release version. The verify statement will be evaluated, but failure will not print any messages on the user's screen or stop the program.

Each of these ISAPI versions is provided in VC++, if you are developing ISAPI applications that do not use MFC. If you are using MFC, then the ISAPI version will call the non-MFC version...well, almost. We found that the ISAPITRACE statement, for instance, does not correspond exactly to TRACE. With TRACE, you can supply parameters to the statement to be printed. But with the ISAPI versions, you must call the appropriate ISAPITRACE1, ISAPITRACE2, ISAPITRACE3... statements, depending on the number of parameters. For example, this code:

CString x = "sample statement";
TRACE1("%s\n", x);
ISAPITRACE("%s\n", x);
ISAPITRACE1("%s\n", x); 

generates a warning during compilation and produces output like this:

sample statement
(null)
sample statement

Example ISAPI Programs

In this section we will look at a few typical tasks for which you might use ISAPI. These are all straightforward examples. Beyond the basics that we've outlined in this chapter, ISAPI applications get site specific.

Customized Logging

The Internet Information Server can log access data to either a flat, comma-delimited ASCII file, or to an ODBC datasource. There is no built-in method for changing what data is logged, but with an ISAPI filter script you can change the default IIS logging behavior.

In our log filter, we have altered the data that is written to two of the fields: ServerIP, which typically contains the IP number of the IIS server host; and the Operation field, which typically contains the Request Method (GET, POST, etc.).

The ServerIP field is replaced with the remote host name (the host name of the client). IIS-designed with better response time in mind-won't do DNS lookups. By itself, an individual DNS lookup may only take a few seconds. However, looking up the remote host name can add substantial overhead to a Web server if that server is responding to many simultaneous requests. The alternative, then, is to do this lookup later. In our case, our Web server doesn't have much traffic anyway, so doing the DNS lookups at request time is of no consequence.

In the Operation field, we added some data from the ALL_HTTP environment variable. We added HTTP_REFERER data (the user's URL that provided the link to the current page); and the HTTP_USER_AGENT data (the client browser type). We separated the values with an & character for later parsing.

The ALL_HTTP environment variable is a catch-all variable specific to IIS; it contains variable fields with data. These fields are defined within ALL_HTTP by [variable-name]: [data][0x13]. The 0x13 hex value makes it easy to parse ALL_HTTP into separate fields. Among the interesting things that we've seen show up in ALL_HTTP are the processer type of the client machine, and the display type ("800x600x16," with the 16 representing 16 million colors). Presumably, someone could write an ISAPI application that takes advantage of this value, customizing the HTML returned to the user to fit their display size and number of colors.

The log filter program is in Listings 7.7 and 7.8.


Listing 7.7 logf.cpp
// LOGF.CPP - Implementation file for your Internet Server
//    logf Filter

#include "stdafx.h"
#include "logf.h"

///////////////////////////////////////////////////////////////////////
// The one and only CLogfFilter object

CLogfFilter theFilter;

///////////////////////////////////////////////////////////////////////
// CLogfFilter implementation

CLogfFilter::CLogfFilter()
{
}

CLogfFilter::~CLogfFilter()
{
}

BOOL CLogfFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
     // Call default implementation for initialization
     CHttpFilter::GetFilterVersion(pVer);

     if (!AfxSocketInit()) // for gethostbyname() calls
     {
          TRACE("AfxSocketInit FAILED\n");
          return FALSE;
     }

     // Clear the flags set by base class
     pVer->dwFlags &= ~SF_NOTIFY_ORDER_MASK;

     // Set the flags we are interested in
     pVer->dwFlags |=     SF_NOTIFY_ORDER_LOW | 
                              SF_NOTIFY_SECURE_PORT | 
                              SF_NOTIFY_NONSECURE_PORT | 
                              SF_NOTIFY_LOG | 
                              SF_NOTIFY_READ_RAW_DATA | 
                              SF_NOTIFY_SEND_RAW_DATA | 
                              SF_NOTIFY_END_OF_NET_SESSION;

     // Load description string
     TCHAR sz[SF_MAX_FILTER_DESC_LEN+1];
     ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
               IDS_FILTER, sz, SF_MAX_FILTER_DESC_LEN));
     _tcscpy(pVer->lpszFilterDesc, sz);
     return TRUE;
}


DWORD CLogfFilter::OnSendRawData(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_RAW_DATA pRawData)
{
     
     csHostName = GetHostName(csIPNumber);
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CLogfFilter::OnReadRawData(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_RAW_DATA pRawData)
{
     dwSize =sizeof(szAllHttpBuffer);
     bGetVar = pCtxt->GetServerVariable("REMOTE_HOST", 
     szAllHttpBuffer, &dwSize);
     csIPNumber = szAllHttpBuffer;
     csIPNumber.TrimRight();

     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CLogfFilter::OnEndOfNetSession(CHttpFilterContext* pCtxt)
{
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

// Do not edit the following lines, which are needed by ClassWizard. huh?
#if 0
BEGIN_MESSAGE_MAP(CLogfFilter, CHttpFilter)
     //{{AFX_MSG_MAP(CLogfFilter)
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()
#endif     // 0


DWORD CLogfFilter::OnLog(CHttpFilterContext* pfc, PHTTP_FILTER_LOG pLog) 
{
     dwSize =sizeof(szAllHttpBuffer);
     bGetVar = pfc->GetServerVariable("ALL_HTTP", 
     szAllHttpBuffer, &dwSize);
     if(!bGetVar) 
     {      //Try Again with new size
          bGetVar = pfc->GetServerVariable("ALL_HTTP", 
          szAllHttpBuffer, &dwSize);
     }
     csAllHttp = szAllHttpBuffer;

     csReferer = FindHttpVar("HTTP_REFERER");
     csAgent = FindHttpVar("HTTP_USER_AGENT");

     bGetVar = pfc->GetServerVariable("REQUEST_METHOD", 
     szAllHttpBuffer, &dwSize);
     csMethod = szAllHttpBuffer;
     csMethod.TrimRight();

     csOperation = csMethod + "&" + csReferer + "&" + csAgent;
     pLog->pszOperation = csOperation;
     pLog->pszServerName = csHostName;
     
     return CHttpFilter::OnLog(pfc, pLog);
}


CString CLogfFilter::FindHttpVar(CString csVariable)
{
     CString csRefTemp = csVariable;
     INT iFind = csAllHttp.Find(csVariable);
     INT iVarLen = csVariable.GetLength();
     if(iFind > 0)
     {     CString csRefTemp = csAllHttp.Mid(iFind+iVarLen+1);
          int iEnd = csRefTemp.Find(0x0A);
          csRefTemp = csRefTemp.Left(iEnd);
          return csRefTemp;
     }else
     {
          csRefTemp = csVariable + " not found";
          return csRefTemp;
     }

}

CString CLogfFilter::GetHostName(CString csIP)
{

     CString csLocalName;
     CString csDot;
     WORD dotarray[4];
     int iFind;

     for(int r=0; r<4; r++)
     {
     csDot = csIP.SpanExcluding(".");
     dotarray[r] = atoi(csDot);
     iFind = csIP.Find(".");
     csIP = csIP.Mid(iFind+1);
     }

     hostent FAR * ptrHost;
     WORD wX = MAKEWORD(dotarray[0], dotarray[1]);
     WORD wY = MAKEWORD(dotarray[2], dotarray[3]);
     DWORD dwNetAddr = MAKELONG(wX, wY);
     CHAR * ptrAddress;
     ptrAddress = (CHAR *)&dwNetAddr;
     
     ptrHost = gethostbyaddr(ptrAddress, 4, PF_INET);
     if(ptrHost==NULL)
     { 
          int iError = WSAGetLastError();
          CHAR szError[10];
          _itoa(iError, szError, 10);
          CString csNull=csNull + "Failure. Error is in your head (" + szError + ")";
          return csNull;
     }     else
     {
     csLocalName = ptrHost->h_name;
     return csLocalName;
     }
}


Listing 7.8 logf.h
// LOGF.H - Header file for your Internet Server
//    logf Filter

#include "resource.h"


class CLogfFilter : public CHttpFilter
{
public:
     CLogfFilter();
     ~CLogfFilter();
// user added
     CString csHostName;
     CString GetHostName(CString csIP);
     CString FindHttpVar(CString csVariable);
     CString csIPNumber;
     CString csAllHttp;
     CString csReferer;
     CString csAgent;
     CString csMethod;
     CString csOperation;     

     char szAllHttpBuffer[1024];

     DWORD dwSize;
     BOOL bGetVar;

// Overrides
     //{{AFX_VIRTUAL(CLogfFilter)
     public:
     virtual BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer);
     virtual DWORD OnSendRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
     virtual DWORD OnReadRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
     virtual DWORD OnEndOfNetSession(CHttpFilterContext* pCtxt);
     virtual DWORD OnLog(CHttpFilterContext* pfc, PHTTP_FILTER_LOG pLog);
     //}}AFX_VIRTUAL

     //{{AFX_MSG(CLogfFilter)
     //}}AFX_MSG
};

This code will work equally well with either the flat ASCII or the SQL log. The following SQL script (Listing 7.9) can be used to create the SQL table.


Listing 7.9 wwwlogcr.sql
/* Microsoft SQL Server - Scripting               */
/* Server: BASEMENT                         */
/* Database: www_db                         */
/* Creation Date 5/8/96 1:35:43 PM                */

/****** Object:  Table dbo.weblog    Script Date: 5/8/96 1:35:44 PM ******/
if exists (select * from sysobjects where id = object_id('dbo.weblog') and sysstat & 0xf = 3)
     drop table dbo.weblog
GO

/****** Object:  Table dbo.weblog    Script Date: 5/8/96 1:35:44 PM ******/
CREATE TABLE dbo.weblog (
     ClientHost char (50),
     Username char (50),
     LogTime datetime,
     Service char (20),
     Machine char (20),
     ServerIP char (50),
     ProcessingTime int,
     BytesRecvd int,
     BytesSent int,
     ServiceStatus int,
     Win32Status int,
     Operation char (200),
     Target char (200),
     Parameters text  
)
GO

As mentioned earlier in the description of the OnLog notification, the fields included in Listing 7.9 are fixed as far as IIS is concerned. Changing the field names, types, or lengths will result in missing or, at best, unpredictable data.

Cookies and Redirection

In this section you'll see an example of how to allow or deny access to a file directory. This application demonstrates several things: reading and writing Cookies; redirection of a user to a different HTTP location if they don't have the right Cookie; and the logging of data entered via a fill-out form.

HTTP Cookies are another invention from Netscape. A Cookie file is a file sent by the server to the client and stored by the client in a particular directory (such as \windows\Cookies). Provided the client is willing to accept a Cookie file, it can be used by the server to store virtually any sort of data. When the user starts up the browser application, the Cookies are read into memory. Then, if the user visits a Web site or domain for which there is a valid Cookie, the Cookie data is sent to the server in the HTTP header.

Typically, a Cookie can be used to maintain the state of a transaction or dialog between a server and a client, as for a catalog application, perhaps. In our enter.dll application,we use the Cookie file to deny or allow access to a particular directory on our Web server. We do this by looking in the HTTP header sent by the client for the string "ZD_AUTH_USER". This is not necessarily the most secure way to accomplish this control; obviously, any user could create their own Cookie file containing this string. To make it safer, you could implement a function that assigns a value to the authentication string-maybe an encrypted password that the server then decrypts and checks against a database.

First let's look at the ISAPI filter, enter.dll, which checks for our Cookie header. Listings 7.10 and 7.11 are the source and header files for the filter.


Listing 7.10 enter.cpp
// ENTER.CPP - Implementation file for your Internet Server
//    enter Filter

#include <afx.h>
#include <afxwin.h>
#include <afxisapi.h>
#include "resource.h"
#include "enter.h"

///////////////////////////////////////////////////////////////////////
// The one and only CEnterFilter object
CEnterFilter theFilter;

///////////////////////////////////////////////////////////////////////
// CEnterFilter implementation

CEnterFilter::CEnterFilter()
{
}

CEnterFilter::~CEnterFilter()
{
}

BOOL CEnterFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
     CHttpFilter::GetFilterVersion(pVer);
     pVer->dwFlags &= ~SF_NOTIFY_ORDER_MASK;
     pVer->dwFlags |= 
          SF_NOTIFY_ORDER_LOW | 
          SF_NOTIFY_READ_RAW_DATA | 
          SF_NOTIFY_SEND_RAW_DATA | 
          SF_NOTIFY_URL_MAP | 
          SF_NOTIFY_END_OF_NET_SESSION;

     // Load description string
     TCHAR sz[SF_MAX_FILTER_DESC_LEN+1];
     ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
               IDS_FILTER, sz, SF_MAX_FILTER_DESC_LEN));
     _tcscpy(pVer->lpszFilterDesc, sz);

     return TRUE;
}

DWORD CEnterFilter::OnReadRawData(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_RAW_DATA pRawData)
{
     LPSTR lpRawData = (LPSTR)pRawData->pvInData;
     DWORD dwRawSize = pRawData->cbInData;
     csRawData = lpRawData;
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CEnterFilter::OnUrlMap(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_URL_MAP pMapInfo)
{
// Check the directory the user is accessing
// If it's our protected dir, check for a Cookie
     CString csMapInfo = pMapInfo->pszURL;
     FindReturn = csMapInfo.Find("/Cookie/protected");
     if(FindReturn > -1 )
     {
          FindReturn = csRawData.Find("ZD_AUTH_USER");
          if(FindReturn > -1)
          { 
               // user is okay, so do nothing
               bRedirectFlag = FALSE;
          } else
          {     
          // user is not okay, so send to register.htm
          // this will occur in OnSendRawData method
          bRedirectFlag = TRUE;     
          }
     }
return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CEnterFilter::OnEndOfNetSession(CHttpFilterContext* pCtxt)
{
     return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

DWORD CEnterFilter::OnSendRawData(CHttpFilterContext* pCtxt,
     PHTTP_FILTER_RAW_DATA pRawData)
{
     if(bRedirectFlag)
     {     
          char chHeader[] = "HTTP/1.0 302 Redirect\r\nContent-Type:  \
          text/html\r\nLocation: /Cookie/addr.htm\r\n\r\n";

          DWORD dwchHeader = sizeof(chHeader);          
          pCtxt->WriteClient(chHeader, &dwchHeader, NULL);
          bRedirectFlag=FALSE;
     }
return SF_STATUS_REQ_NEXT_NOTIFICATION;
}


BOOL CEnterFilter::WriteClientData(CHttpFilterContext* pCtxt, LPSTR lpClientData)
{
     lpdwLen = strlen(lpClientData);
     bWrite = pCtxt->WriteClient(lpClientData, &lpdwLen, 0);
     bWrite=TRUE;     

     if(!bWrite) 
     {return FALSE;}
     else
     {return TRUE;}
}


Listing 7.11 enter.h
// ENTER.CPP - Implementation file for your Internet Server
//    enter Filter

class CEnterFilter : public CHttpFilter
{
public:
     CEnterFilter();
     ~CEnterFilter();
     LPSTR temp;
     DWORD lpdwLen;
     BOOL bWrite;
     CString csRawData;
     int FindReturn;
     BOOL bRedirectFlag;

     BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer);
     DWORD OnReadRawData(CHttpFilterContext* pCtxt,
          PHTTP_FILTER_RAW_DATA pRawData);
     DWORD OnUrlMap(CHttpFilterContext* pCtxt,
          PHTTP_FILTER_URL_MAP pMapInfo);
     DWORD OnSendRawData(CHttpFilterContext* pCtxt,
          PHTTP_FILTER_RAW_DATA pRawData);
     DWORD OnEndOfNetSession(CHttpFilterContext* pCtxt);
};

Upon determining that the user does not have one of our Cookies, we send the user back up the directory tree one level to our registration form. Figure 7.5 shows the registration form in Explorer.

Figure 7.5 : Registration form for a Cookie.

Once the user completes the form and clicks on the Submit button, our registration application is executed: addr.dll (Listings 7.12 and 7.13). We did two versions of the functions that write the data collected. The first version simply writes the data to a flat ASCII file, separating the data fields with an & character. The second version of the functions (Listings 7.14 and 7.15) shows some nominal code for adding the data to a SQL table (Listing 7.16).

In Figure 7.6, you see the "Accept Cookie?" inquiry window that Explorer pops up when the Cookie is sent.

Figure 7.6 : The user is asked to confirm acceptance of the Cookie.


Listing 7.12 addr.cpp
// ADDR.CPP - Implementation file for your Internet Server
//    addr Extension

#include "stdafx.h"
#include "addr.h"
#include "SqlTab.h"
#include "time.h"

///////////////////////////////////////////////////////////////////////
// command-parsing map

// Form fields:  Email, Name, Address, City, State, Zip
BEGIN_PARSE_MAP(CAddrExtension, CHttpServer)
     
     ON_PARSE_COMMAND(Add, CAddrExtension, ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR)
     ON_PARSE_COMMAND_PARAMS("Email Name Address City State Zip")

     ON_PARSE_COMMAND(SQLAdd, CAddrExtension, ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR ITS_PSTR)
     ON_PARSE_COMMAND_PARAMS("Email Name Address City State Zip")

     ON_PARSE_COMMAND(Default, CAddrExtension, ITS_EMPTY)
     DEFAULT_PARSE_COMMAND(Default, CAddrExtension)

END_PARSE_MAP(CAddrExtension)

///////////////////////////////////////////////////////////////////////
// The one and only CAddrExtension object

CAddrExtension theExtension;
CSqlTab cSqlTab;

///////////////////////////////////////////////////////////////////////
// CAddrExtension implementation

CAddrExtension::CAddrExtension()
{
}

CAddrExtension::~CAddrExtension()
{
}

BOOL CAddrExtension::GetExtensionVersion(HSE_VERSION_INFO* pVer)
{
     // Call default implementation for initialization
     CHttpServer::GetExtensionVersion(pVer);

     // Load description string
     TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1];
     ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
               IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN));
     _tcscpy(pVer->lpszExtensionDesc, sz);

// ADJUST the following for your machine.
     pAuthFile = "c:\\windows\\system32\\inetsrv\\wwwroot\\zd\\address.dat";
     if( !cAuthFile.Open( pAuthFile, CFile::modeRead | CFile::typeText) )
     { 
          TRACE("unable to open the Authorization file in READ mode\n"); 
          cAuthFile.Open( pAuthFile, CFile::modeCreate | CFile::typeText);
          TRACE("created a new file...\n");
     }
     cAuthFile.Close();
     return TRUE;
}

///////////////////////////////////////////////////////////////////////
// CAddrExtension command handlers

void CAddrExtension::Default(CHttpServerContext* pCtxt)
{
     StartContent(pCtxt);
     WriteTitle(pCtxt);
     *pCtxt << _T("This function doesn't do anything.<BR>");
     EndContent(pCtxt);
}

void CAddrExtension::Add(CHttpServerContext* pCtxt, LPSTR Email, LPSTR Name,
LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip)
{
     if( FindUser(Email) )
     {
          StartContent(pCtxt);
          WriteTitle(pCtxt);
          *pCtxt << _T("User ") << Email << _T(" already on file<BR>");
          *pCtxt << _T("Try a different account, or phone 555-sorry<BR>");
     }
     else
     {
          WriteUser(Email,Name,Address,City,State,Zip);
               StartContent(pCtxt);
     time_t ltime;
     struct tm *gmt;
     time(&ltime); ltime = ltime+(6*100000);
     gmt = gmtime(&ltime);
     char cbCookieTime[34];          
     strftime(cbCookieTime,34,"%A, %d-%b-%y %H:%M:%S GMT",gmt); 

     LPSTR lpTime = (LPSTR)cbCookieTime;
     CString csTime = cbCookieTime;
     CString csCookie = "";
     csCookie = csCookie + 
          "Set-Cookie: " +
          "Name=ZD_AUTH_USER; " +
          "Expires=" + csTime + "; " +
          "path=/Cookie/prot; " +
          "domain=166.84.253.193; " +
          "\r\n" ;
     
     AddHeader(pCtxt, (LPCTSTR)csCookie);
     AddHeader(pCtxt, "Content-type = text/html\r\n\r\n");
     *pCtxt << _T("<TITLE>Cookie Accepted</TITLE>");
     *pCtxt << _T("Welcome to the Party, pal.<BR>");
     *pCtxt << _T("You should now be able to access the protected directory at:<BR>");
     *pCtxt << _T("<A HREF=/Cookie/protected/protected.htm>/Cookie/protected/protected.htm</A>");
     }

EndContent(pCtxt);
}

BOOL CAddrExtension::FindUser(LPSTR Email)
{
     if( !cAuthFile.Open( pAuthFile, CFile::modeRead | CFile::typeText) )
     { 
          TRACE("unable to open the Authorization file in READ mode\n");
          return FALSE;
      }
     
     cAuthFile.SeekToBegin();
     CString csEmail = Email;
     CString csReadString;
     while( cAuthFile.ReadString(csReadString) == TRUE )
     { 
          if(csReadString.Find(csEmail) > -1)
          { 
               cAuthFile.Close();
               return TRUE;
          }
          else
          { 
               // do nothing, keep going
          }
     }

     cAuthFile.Close();
     return FALSE;
}

// Reopens the data file and writes the user data to it.
void CAddrExtension::WriteUser(LPSTR Email, LPSTR Name, LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip)
{
     CString csLogString;
     // In our single machine tests, with 2 copies of Explorer running,
     // and both trying to open the file, 1 browser waited till the other
     // was finished before getting past this line.  It would probably
     // be useful to implement some sort of file-locking here.
     if( !cAuthFile.Open( pAuthFile, CFile::modeWrite | CFile::typeText) )
     { TRACE("unable to open file in modeWrite\n"); }
 
     csEmail = Email;     
     csName = Name;      csAddress = Address;
     csCity = City; csState = State;
     csZip = Zip;
     CString csEOL( 0x0A );
     
     csLogString =     csEmail + "&" + csName + "&" + 
     csAddress + "&" + csCity + "&" + csState + "&" +
     csZip + csEOL;
     cAuthFile.SeekToEnd();
     cAuthFile.WriteString(csLogString);
     cAuthFile.Close();

} // end ascii file stuff

///////////////// begin SQL section ////////////////////////

void CAddrExtension::SQLAdd(CHttpServerContext* pCtxt, LPSTR Email, LPSTR Name,
LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip)
{
     if( SQLFindUser(pCtxt,Email,Name,Address,City,State,Zip) )
     {
          StartContent(pCtxt);
          WriteTitle(pCtxt);
          *pCtxt << _T("User ") << Email << _T(" already on file<BR>");
          *pCtxt << _T("Try a different account, or phone 555-sorry<BR>");
          EndContent(pCtxt);
     }
     else
     {
          StartContent(pCtxt);
          WriteTitle(pCtxt);
          *pCtxt << _T("Welcome to the Party, pal.<BR>");
          EndContent(pCtxt);
     }
     EndContent(pCtxt);
}

BOOL CAddrExtension::SQLFindUser(CHttpServerContext* pCtxt, LPSTR Email, LPSTR Name,
LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip)
{
     
     szDefaultConnect = cSqlTab.GetDefaultConnect();
     CString csStart="Email = '"; 

     cSqlTab.m_strFilter = csStart + Email + "'";
     BOOL bOpen = cSqlTab.Open(CRecordset::dynaset,NULL,CRecordset::none);
     if(!bOpen) { TRACE("bOpen returned FALSE\n"); }

     LONG lRecCount = cSqlTab.GetRecordCount();
     if(lRecCount>0) 
     { 
          cSqlTab.Close();
          return TRUE; 
     }
     else 
     { 
          cSqlTab.AddNew();
          cSqlTab.m_Email=Email;
          cSqlTab.m_Name=Name;
          cSqlTab.m_Address=Address;
          cSqlTab.m_City=City;
          cSqlTab.m_State=State;
          cSqlTab.m_Zip=Zip;
          cSqlTab.Update();
          cSqlTab.Close();
          return FALSE; 
     }
}


// Do not edit the following lines, which are needed by ClassWizard.
#if 0
BEGIN_MESSAGE_MAP(CAddrExtension, CHttpServer)
     //{{AFX_MSG_MAP(CAddrExtension)
     //}}AFX_MSG_MAP
END_MESSAGE_MAP()
#endif     // 0

Listing 7.13 addr.h
// ADDR.H - Header file for your Internet Server
//    addr Extension

#include "resource.h"

class CAddrExtension : public CHttpServer
{
public:
     CAddrExtension();
     ~CAddrExtension();

     void Add(CHttpServerContext* pCtxt, LPSTR Email, LPSTR Name,
LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip);
     BOOL FindUser(LPSTR Email);
     void WriteUser(LPSTR Email, LPSTR Name, LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip);
     
     void SQLAdd(CHttpServerContext* pCtxt, LPSTR Email, LPSTR Name,
LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip);
     BOOL SQLFindUser(CHttpServerContext* pCtxt, LPSTR Email, LPSTR Name,
LPSTR Address, LPSTR City, LPSTR State, LPSTR Zip);

     PSTR pAuthFile; // string holding name of our flat file containing registration data
     CStdioFile cAuthFile; 
     CString csEmail;     
     CString csName;      
     CString csAddress;
     CString csCity; 
     CString csState;
     CString csZip; 
     CString szDefaultConnect;
     LPCTSTR lpNewCookie;

// Overrides
     
     //{{AFX_VIRTUAL(CAddrExtension)
     public:
     virtual BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);
     //}}AFX_VIRTUAL

     // TODO: Add handlers for your commands here.
     // For example:

     void Default(CHttpServerContext* pCtxt);

     DECLARE_PARSE_MAP()

     //{{AFX_MSG(CAddrExtension)
     //}}AFX_MSG
};

Following are Listings 7.14 and 7.15 for the .cpp and .h files for the CRecordSet object used to access our SQL table. These are just the default files generated by ClassWizard.


Listing 7.14 SqlTab.cpp
// SqlTab.cpp : implementation file
//
#include "stdafx.h"
#include "addr.h"
#include "SqlTab.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CSqlTab

IMPLEMENT_DYNAMIC(CSqlTab, CRecordset)
CSqlTab::CSqlTab(CDatabase* pdb)
     : CRecordset(pdb)
{
     //{{AFX_FIELD_INIT(CSqlTab)
     m_Email = _T("");
     m_Name = _T("");
     m_Address = _T("");
     m_City = _T("");
     m_State = _T("");
     m_Zip = _T("");
     m_nFields = 6;
     //}}AFX_FIELD_INIT
     m_nDefaultType = dynaset;
}

CString CSqlTab::GetDefaultConnect()
{
     return _T("ODBC;DSN=address;UID=sa;PWD=");
}

CString CSqlTab::GetDefaultSQL()
{
     return _T("[dbo].[AddrTab]");
}

void CSqlTab::DoFieldExchange(CFieldExchange* pFX)
{
     //{{AFX_FIELD_MAP(CSqlTab)
     pFX->SetFieldType(CFieldExchange::outputColumn);
     RFX_Text(pFX, _T("[Email]"), m_Email);
     RFX_Text(pFX, _T("[Name]"), m_Name);
     RFX_Text(pFX, _T("[Address]"), m_Address);
     RFX_Text(pFX, _T("[City]"), m_City);
     RFX_Text(pFX, _T("[State]"), m_State);
     RFX_Text(pFX, _T("[Zip]"), m_Zip);
     //}}AFX_FIELD_MAP
}

/////////////////////////////////////////////////////////////////////////////
// CSqlTab diagnostics
#ifdef _DEBUG
void CSqlTab::AssertValid() const
{
     CRecordset::AssertValid();
}

void CSqlTab::Dump(CDumpContext& dc) const
{
     CRecordset::Dump(dc);
}
#endif //_DEBUG

Listing 7.15 SqlTab.h
// SqlTab.h : header file
//
#include "afxdb.h"
/////////////////////////////////////////////////////////////////////////////
// CSqlTab recordset
class CSqlTab : public CRecordset
{
public:
     CSqlTab(CDatabase* pDatabase = NULL);
     DECLARE_DYNAMIC(CSqlTab)

// Field/Param Data
     //{{AFX_FIELD(CSqlTab, CRecordset)
     CString     m_Email;
     CString     m_Name;
     CString     m_Address;
     CString     m_City;
     CString     m_State;
     CString     m_Zip;
     //}}AFX_FIELD

// Overrides
     // ClassWizard generated virtual function overrides
     //{{AFX_VIRTUAL(CSqlTab)
     public:
     virtual CString GetDefaultConnect();    // Default connection string
     virtual CString GetDefaultSQL();    // Default SQL for Recordset
     virtual void DoFieldExchange(CFieldExchange* pFX);  // RFX support
     //}}AFX_VIRTUAL

// Implementation
#ifdef _DEBUG
     virtual void AssertValid() const;
     virtual void Dump(CDumpContext& dc) const;
#endif
};

The SQL table was created with the script in Listing 7.16.


Listing 7.16 addrcr.sql
/* Microsoft SQL Server - Scripting               */
/* Server: BASEMENT                         */
/* Database: address                         */
/* Creation Date 8/10/96 8:33:42 PM                */
/****** Object:  Table dbo.AddrTab    Script Date: 8/10/96 8:33:44 PM ******/
if exists (select * from sysobjects where id = object_id('dbo.AddrTab') and sysstat & 0xf = 3)
     drop table dbo.AddrTab
GO
/****** Object:  Table dbo.AddrTab    Script Date: 8/10/96 8:33:44 PM ******/
CREATE TABLE dbo.AddrTab (
     Email char (40) NOT NULL ,
     Name char (40) NOT NULL ,
     Address char (40) NOT NULL ,
     City char (20) NOT NULL ,
     State char (2) NOT NULL ,
     Zip char (11) NOT NULL
)
GO
CREATE UNIQUE INDEX EIndex on AddrTab (Email)
GO

Server Push

Server Push is a technique invented by Netscape that tells the client browser to keep a connection open, allowing the server to update a section of the document. The document may be HTML, or an image, or audio; in fact, any valid MIME type is replaceable.

Server Push does, indeed, work. But be forewarned: It has always been somewhat unreliable because response times depend heavily on network loads, and the strain on the server is immense. On a UNIX host where we tried it for a "live" event, it consumed a great deal of the machine's CPU capacity and memory, causing a rapid server crash. Nonetheless, Server Push remains a topic of interest, so we'll look at an example of how to use it in an ISAPI extension.

Server Push works by sending a different Content-type header, namely:

Content-type: multipart/x-mixed-replace;boundary=----------\n\n

Netscape invented this multipart/x-mixed-replace Content-type. It informs the browser that the server will be sending multiple document sections, each with its own MIME type/subtype header.

The boundary= elements delimit document sections. So in the document we send to the browser, we will frame the section to update with a line of data containing our boundary. (The actual boundary data is arbitrary, as long as you use the same boundary throughout.) You can only update one section of the document sent to the client, and the current multipart/x-mixed-replace experimental specification does not support mixing MIME types.

With this technique, a server push can send a series of HTML text sections, each one replacing the last; or it can push a series of GIFs, or JPEGs, or audio files. As it stands currently, however, the server push cannot replace multiple MIME types on the client side.

In our example program that follows (nph.cpp in Listing 7.17), we simply print out the current time, wait a few seconds, and then update the time. This HTML (text) replacement occurs five times; then the program exits. Here's what the raw HTML looks like that we are sending to the client:

> telnet 166.84.253.193 80
Trying 166.84.253.193...
Connected to 166.84.253.193.
Escape character is '^]'.
get /msdev/ax/isapi/nph/debug/nph.dll?
HTTP/1.0 200 OK
Server: Microsoft-IIS/2.0
Date: Sat, 10 Aug 1996 19:40:10 GMT
Content-type: multipart/x-mixed-replace;boundary=----------

----------
Content-type: text/html

15:40:10<HR>
----------
Content-type: text/html

15:40:14<HR>
----------
Content-type: text/html

15:40:17<HR>
----------
Content-type: text/html

15:40:20<HR>
----------
Content-type: text/html

15:40:24<HR>
----------
Connection closed by foreign host.
>

Note the following things about our experiment with Server Push:

  • We got our raw HTML output by Telnetting to the Web server's port 80, and manually issuing a GET request. This proved to be a useful method for debugging our Server Push example.
  • The two headers sandwiched between the HTTP Status and Content-type headers were added by ISAPI when the ServerSupportFunction call was made.
  • Speaking of additional headers, this example does not use MFC. We started trying to build this application with the ISAPI foundation classes and found that an extra set of headers were being sent to the client. In the extra set, the Content-type was changed back to "text/html." According to the MFC source file, "isapi.cpp," this behavior is part of the MFC design. We didn't want to have to build custom MFC DLLs, so we did without MFC for the example.
  • Our nph.cpp program works with the Netscape browser. When we tried it with Internet Explorer, however, it simply printed the "replace" sections one after another, rather than replacing the HTML in place as expected.

Here's our Server Push example, which displays the current time on the server and updates the display four times, with an interval of 3.333 seconds between updates.


Listing 7.17 nph.cpp
// nph.cpp
// server push example
// prints out the time 5 times and quits.
// works with Netscape browser.  Doesn't seem to work with Explorer...

#include <windows.h>
#include <stdio.h>
#include <time.h>
#include <httpext.h>

_stdcall DllMain (HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpv)
    {
      return (TRUE);
    }

DWORD dwBytesWritten;
DWORD Push(EXTENSION_CONTROL_BLOCK *);

BOOL WINAPI GetExtensionVersion (HSE_VERSION_INFO *version)
{
     version->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR,
          HSE_VERSION_MAJOR);
     strcpy(version->lpszExtensionDesc, "Print Environment variables");

     return TRUE;
}

DWORD WINAPI HttpExtensionProc (EXTENSION_CONTROL_BLOCK *pEcb)
{
     DWORD dwReturnCode = Push(pEcb);
     return HSE_STATUS_SUCCESS;
}

DWORD Push(EXTENSION_CONTROL_BLOCK *pEcb)
{

     char szXHeader[] = "Content-type: multipart/x-mixed-replace;boundary=----------\n\n";
     DWORD dwSize = sizeof(szXHeader);
     DWORD dwReq = HSE_REQ_SEND_RESPONSE_HEADER;
     char szOK[] = "200 OK";
     pEcb->ServerSupportFunction( pEcb->ConnID, 
          dwReq,
          szOK, 
          &dwSize, 
          (ULONG *)szXHeader); 

     char tbuffer [9];
     char szContent[] = "Content-type: text/html\n\n";
     char szSep[] = "----------\n";
     char szRuler[] = "<HR>\n";

     dwBytesWritten = lstrlen (szSep);
     pEcb->WriteClient (pEcb->ConnID, (PVOID) szSep, 
          &dwBytesWritten, 0);

     for(int i = 0; i < 5; i++)
     {
     
     dwBytesWritten = lstrlen (szContent);
     pEcb->WriteClient (pEcb->ConnID, (PVOID) szContent, 
          &dwBytesWritten, 0);

     _strtime( tbuffer );
     dwBytesWritten = lstrlen (tbuffer);
     pEcb->WriteClient (pEcb->ConnID, (PVOID) tbuffer, 
          &dwBytesWritten, 0);

     dwBytesWritten = lstrlen (szRuler);
     pEcb->WriteClient (pEcb->ConnID, (PVOID) szRuler, 
          &dwBytesWritten, 0);

     dwBytesWritten = lstrlen (szSep);
     pEcb->WriteClient (pEcb->ConnID, (PVOID) szSep, 
          &dwBytesWritten, 0);

     Sleep(3333); // 3.333 seconds to rest
     }
return HSE_STATUS_SUCCESS;
}

Industrial-Strength Extensions

Using C++ provides the best way to take advantage of the Internet Server API. The resulting DLLs are smaller in size, so they require less memory and perform faster.

There are alternatives to C++ if you wish to develop applications in another language, but these alternatives only apply to ISAPI extensions. For filters, you will have to use C++ (at least at this time). In this section we'll take a look at one such alternative-ISAPI Perl.

First, however, let's look at an ISAPI extension from Microsoft that is installed along with IIS: the Internet Database Connector.

The Internet Database Connector

The Internet Database Connector (IDC) is an ISAPI extension application that Microsoft ships with IIS. The IDC makes it easy to query ODBC-compliant databases such as MS-SQL, Access, and FoxPro, and then pass the results back to the client in HTML format.

Database interface tools such as IDC are almost as old as the Web itself, and Microsoft's version is not the first to hook into ODBC-compliant data. Similar tools worth investigating are offered by Nomad, Cold Fusion, and others. IDC is a good example of an industrial-strength filter.

When IIS is installed, files with the filename extension of .idc are mapped to the file HTTPODBC.DLL (in whatever directory holds IIS server). This DLL is built so that URL requests referencing a file with an .idc extension will trigger functions in HTTPODBC.DLL.

The extension does three things: (1) Opens and reads the .idc file; (2) executes the ODBC query in the .idc file; and (3) formats and returns the resulting data to the client. In this third step, the filter looks for a file specified in the .idc file, which will have an extension of .htx. This file specifies how the data returned to the client is to be formatted.

To use IDC, then, we need four components:

  • An ODBC database table with populated data records
  • An .idc file that points to this table, and that specifies the SQL query to execute
  • An HTML form that acts as the front-end to start up the SQL contained in the .idc file
  • An .htx file that formats the output of the .idc SQL statements.

As an example, suppose we have a Microsoft Access database called sampledb, containing a table called sampletable. This table contains the fields lname, fname, addr, and tel. The next thing we need is the glue that must exist between a Web front-end (an HTML form) and the ODBC-compliant database-namely, the .idc file. This file points to the data source (sampletable) and specifies the embedded SQL query. This .idc file must reside in a directory that is executable.

Let's examine a sample .idc file that references a sample table in a sample database:

Datasource: sampledb
Username: I_USR_MYNTBOX	
Template: sample.htx
SQLStatement: Select * from sampletable;

The first line specifies the data source, which in this case is our Access database named sampledb. The database must be registered on the IIS server as a System DSN. (You can do this with the Control Panel | ODBC command.)

The second line specifies an NT Username with the appropriate privileges to access the database. Here we are using an IIS default account, but the implementation will be site specific.

The third line specifies the .htx file that will contain the template for formatting the data returned by the query. Following that is the SQLStatement line, which can be one line as it is here or can span multiple lines. If there are additional lines, each begins with a + character. In this example, however, we want everything in the database.

If this were all that IDC could do, it wouldn't amount to much-we'd just be sending back an ugly blob of text to the user. In Listing 7.18 is a sample .htx file that allows you to format the data returned to the client, placing and inserting the data fields within the HTML specified.


Listing 7.18 The SQL output, formatted in an *.htx file
<h1> Greetings </h1>
<FORM>
<%begindetail%>

<table border=2 cellpadding=1 valign=center>
<tr>
<td><%lname%></td><td><%fname%></td><td><%addr%></td>
</tr>
</table>
<hr>

<%enddetail%>
</FORM>
Thank you for choosing our sample IDC HTX thingamabob.

In the .htx file, formatting instructions are specified within <%[something]%> tags. The [something] can refer to a field name from the data source, an IDC formatting instruction, or some other piece of variable data such as an HTTP environment variable. In Listing 7.18, the <%begindetail%> and <%enddetail%> placeholders serve as bookends to flag the region where we reference fields from the sampletable. There are references to the lname, fname, and addr fields from our sampledb. The begindetail and enddetail instructions tell IDC to put the data into a list-style format. Each record returned from the SQL query goes into a separate <%begindetail%> and <%enddetail%> block, which is repeated until the records have been exhausted. Following that, the rest of the HTML in the .htx file is formatted and output. The output from our simple query is shown in Figure 7.7.

Figure 7.7 : Output from a simple SQL query.

The Internet Database Connector has way more power than we've shown here, including the ability to use conditionals in the .htx file, execute stored SQL procedures, and pass parameters from a POST method form to the .idc file. This is a bit outside our ActiveX mission, so for details we'll just direct you to the on-line documentation that ships with IIS.


Listing 7.19 A front-end to kick off an SQL query, specifying the .idc file
<title> Sample ODBC IDC-HTX </title>
<h1> Sample ODBC IDC-HTX </h1>
Hello.
<form method=post action="http://127.0.0.1/sample.idc">
<input type=submit name="submit">
</form>

ISAPI Perl

The folks at Hip Communications, Inc., at

http://www.perl.hip.com

have succeeded in porting most of Perl's useful features to the Win32 platform. (Perl, for Practical Extraction and Report Language, is the popular UNIX CGI scripting language written by Larry Wall.) Not satisfied with a simple NT port, Hip Communications recently extended Perl to work under Win95, as well.

For true ISAPI fans, the best news yet is the dynamic link library, PerlIS.DLL, which is an ISAPI extension. A user can therefore associate the extension .pl with the Perl interpreter perl.exe. To test the new PerlIS.DLL, the user can invent a new extension (say, *.plx) and set up programs ending in .plx to run the PerlIS.DLL. Naturally, scripts using the PerlIS.DLL will run much faster because, as mentioned, ISAPI extensions run in the server process space.

Actually, the user need not worry about manually setting up the association. The WinZipped distribution kit, when downloaded and unzipped from

ftp://ftp.perl.hip.com/

contains a batch file, INSTALL.BAT. Assuming we're starting from the directory c:\perl5, for example, the INSTALL.BAT will unpack the distribution and put the perl.exe interpreter in c:\perl5\bin, and the PerlIS.DLL in c:\perl5\ntt. INSTALL also prompts the user for an extension to associate with the DLL. We recommend a test extension such as .plx, which is different from interpreted Perl's (.pl, by convention). The batch file then creates the necessary Registry entries in the file HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\Parameters\ScriptMap.

At this point, it is merely necessary to use the IIS service manager to stop and restart the IIS service to force a reread of the new copy of the Registry, and all should be well.

Our Notes on the Beta Release

We did encounter some quirks and problems with Hip's beta release and had to ask the newsgroup for advice. The right way to call the script test.plx, which is in the standard \Scripts directory, from an Internet Web client is with this URL:

http://server-machine/Scripts/test.plx

and most certainly not with this URL:

http://server-machine/Scripts/PerlIS.DLL?test.plx

The latter is technically valid (using the script name as a QUERY_STRING environmental variable), but it opens up mammoth holes in security. It can allow an anonymous client to interact (maliciously) with the PerlIS.DLL, which is unacceptably dangerous. This is true of the Perl interpreter, perl.exe, as well: Do not place it in any of the server's script directories. Tom Christiansen warns against this at

http://mox.perl.com/.

Here are a few other important points about PerlIS.DLL:

  • Web scripts should be placed in a directory that is executable only and not readable. Run the IIS Service Manager to configure a new script area, or use the default \Scripts virtual directory. This undocumented fact was pointed out in the popular newsgroup.
  • Web scripts associated with PerlIS.DLL run in the context of the IIS; hence, threads in the server process space share the context of the server. This means a misbehaving thread could alter the context of all other active threads in the IIS process space-a potential security concern.
  • Certain Perl functions, including changing the current working directory, are not supported by scripts running with the PerlIS.DLL. Why not? For the same reason elaborated above: All threads in the IIS process space are essentially one big, indivisible family. Similarly, shell invocations-back-tick syntax, execs, and system calls-are disabled.

Other ISAPI Tools

Although CGI is almost as old as the Web, the field of server extensions and filters is still relatively new. Aside from developing new applications in VC++, you might check out these additional tools and applications.

OLEISAPI is one of the samples in the Win32 SDK. This is an extension that allows you to execute Visual Basic applications as extensions. A number of people are using OLEISAPI successfully, but we found it cumbersome compared to VC++. We prefer to wait for the birth of Denali: the next item in our list.

Denali is the code name for a beta product from Microsoft; as we are writing this book, it isn't yet available. Denali is the promised server-side version of VBScript. We expect that Denali will have more power than the client-side version of VBScript, which for security reasons can't do certain things like reading and writing files on the server.

An existing server-side version of Basic is ISAPI Basic from Houston Prose, available at

http://gene.fwi.uva.nl/~spoelstr/ISAPIBasic/

ISAPI Basic is interesting in that it comes with built-in hooks for sending
e-mail, making ODBC queries, and setting Cookies.

Tripoli is the code name for the beta version of Microsoft's ActiveX Search engine. This is an ISAPI extension that will index specified files on a Web site. What sets Tripoli apart from traditional Web indexing programs is that it will index ActiveX documents such as Microsoft Word documents and Excel spreadsheets, in addition to HTML documents. This allows for searching not only the contents of a document, but also the data that is in the document's header information (Subject, Creator, Date Modified, and so forth).

By the way, watch out: The indexes generated by Tripoli take substantial disk space.

The beta version of Tripoli can be found at

http://www.microsoft.com/ntserver/search/

Other Resources

The ISAPI mailing list is a busy and informative place and is frequented by Microsoft developers. To find out how to join this list, navigate to

http://www.microsoft.com/workshop/

Stephen Genusa maintains an ISAPI FAQ at

http://rampages.onramp.net/~steveg/isapi.html

and an IIS FAQ at

http://rampages.onramp.net/~steveg/iis.html

Both are comprehensive and valuable resources.

In addition, Stephen's EyeSAPI ISAPI Debugger is available at

http://rampages.onramp.net/~steveg/eyesapi.html

This application allows you to debug ISAPI extensions under both Win95 and NT.

Another useful Web site is

http://www.klv.com/english/iis/index.htm

which contains a number of interesting articles and tips for developing ISAPI and IDC applications.



HomeAbout UsSearchSubscribeAdvertising InfoContact UsFAQs
Use of this site is subject to certain Terms & Conditions.
Copyright (c) 1996-1998 EarthWeb, Inc.. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of EarthWeb is prohibited.
Please read the Acceptable Usage Statement.
Contact reference@developer.com with questions or comments.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.

Click here for more info

Click here for more info