Writing Interactive Web Apps is a Piece of Cake with the New ISAPI Classes in MFC 4.1

Mike Blaszczak

Mike Blaszczak is a software design engineer in Microsoft's Developer Studio Business Unit working on MFC. He is currently updating his book, The Revolutionary Guide to Win32 Programming with the Microsoft Foundation Classes (Wrox Press). He can be reached at mikeblas@msn.com or 76360.157@compuserve.com.

Click to open or copy the MFCTALK project files.

Click to open or copy the OLDBROW project files.

Many Web pages today provide interactive forms users can fill out to access databases, get information dynamically, request mail, order products, or just run another program on the server. Let's explore ways to make your Web pages contain something more interesting than static pictures. I'll show you how the new WindowsÒ ISAPI architecture and MFC 4.1 make writing interactive web apps a piece of cake.

The common language of the Web is HTML. Web browsers all support some form of HTML, while the underlying communication software uses TCP/IP-based sockets to accept requests and return responses using Hypertext Transfer Protocol (HTTP). So whether you want to write an interactive sports page that lets users request the latest hockey scores by typing in the names of their favorite teams, or you simply want to run another program without any user interface, all you have to work with is HTML. All clients do is request HTML documents and all servers do is ship them back. To create an interactive Web page, you have to be creative.

Several methods already exist for writing these Web extensions. Traditionally, developers have used a protocol calledCommon Gateway Interface (CGI). In CGI, theserver uses environment variables to communicate with the extension. The extension dynamically generates HTML documents, which the server pumps back to the client. Most UNIX Web servers support some form of CGI. Some CGI extensions are written in C, but most are written in a language called Perl. Since Perl can access environment variables easily, talks naturally with standard input and output, and supports simple control structures, it's easier to use than C, but it's slower. The main advantage of Perl scripts is that they run on any platform that has a Perl interpreter.

A Better Way: ISAPI

One problem with CGI is that the server must spawn a separate copy of the extension program for every user who uses it. Spawning an application, tracking it while it runs, and terminating it cleanly incurs considerable expense. This expense adds up quickly on busy machines that serve dozens or even hundreds of connections. If you're running a Web server on Windows NTÔ, there is a much better way. Instead of launching a new process for every user, you can use threads, which are much more lightweight. By writing your extension as a dynamic-link library (DLL) instead of an executable program (EXE), the server can call entry points within your library from threads the server owns. Since the server uses multiple threads to service I/O requests from different clients, no extra overhead is incurred by running your extension code inside the same process as the server. This approach is faster than the old spawn-a-process technique thought up by UNIX hackers.

This architecture—using a DLL to implement Web server extensions—was jointly proposed by Progress Software Corporation and Microsoft. To make it rhyme with technologies like MAPI and TAPI, the architecture was named ISAPI, which stands for Internet Server API. Microsoft adopted ISAPI in its Windows NT Server-based Internet Information Server (the software formerly known as Gibraltar). Internet Information Server provides Web, FTP, and Gopher publishers, all implemented as secure, high-performance Win32Ò services.

Release 4.1 of the Microsoft Foundation Classes, which will ship with Visual C++ 4.1 (you'll need to order the Visual C++ subscription to get it), includes a handful of new classes that make writing ISAPI extensions as easy as overriding a few virtual functions. The new development environment even has a custom AppWizard that produces ISAPI extension projects that use the new classes. Using ISAPI and MFC, you can write two types of server extensions: filters and server applications.

Filters

Filters aren't as visually glamorous as server applications, but they're surprisingly powerful. As the name suggests, a filter sits between the client and server apps and modifies data as it goes by (see Figure 1). An ISAPI filter is a DLL that provides two special entries: GetFilterVersion and HttpFilterProc.

Figure 1 Where a Filter Sits

To install a new filter, you must bring the server down to doctor some registry entries, then restart the server. As it starts up, the server loads your filter DLL and calls GetFilterVersion with a pointer to an HTTP_FILTER_VERSION structure.


 BOOL GetFilterVersion( HTTP_FILTER_VERSION * pVer ); 
// entry point in your DLL

// defined in HTTPFILT.H
struct HTTP_FILTER_VERSION {
    DWORD  dwServerFilterVersion;
    DWORD  dwFilterVersion;
    CHAR      lpszFilterDesc[SF_MAX_FILTER_DESC_LEN];
    DWORD  dwFlags;
};

It's your job to fill this structure with information about your filter. dwFilterVersion tells the server what level of the ISAPI specification you use. dwServerFilterVersion is the corresponding number for the server (which the server fills in before calling your GetFilterVersion function). By comparing dwServerFilterVersion and dwFilterVersion, servers and filters of different vintages can coexist peacefully. You can identify your filter by copying a string into the lpszFilterDesc. The server never shows this string to the client, but only to various server-side administration tools. The most important item in HTTP_FILTER_VERSION is dwFlags. By setting the appropriate flags, you tell the server exactly what events you are interested in. Figure 2 shows the various flags/events.

Figure 2 Server Notifications and Corresponding CHttpFilter Members and their Parameters

Notification

Meaning

CHttpFilter Member

Structure

SF_NOTIFY_PREPROC_HEADERS

The server is about to begin processing infor mation provided in the HTTP headers of the client request.

OnPreprocHeaders

HTTP_FILTER_PREPROC_HEADERS

SF_NOTIFY_SEND_RAW_DATA

The server is about to send data to the client.

OnSendRawData

HTTP_FILTER_RAW_DATA

SF_NOTIFY_READ_RAW_DATA

The server has received data from the client and is about to process it.

OnReadRawData

HTTP_FILTER_RAW_DATA

SF_NOTIFY_AUTHENTICATION

The server is authenti cating a user.

OnAuthentication

HTTP_FILTER_AUTHENT

SF_NOTIFY_URL_MAP

The server is mapping a URL from the client's request for a logical URL to a physical path on the server.

OnUrlMap

HTTP_FILTER_URL_MAP

SF_NOTIFY_LOG

The server is terminating the connection to the client.

OnLog

HTTP_FILTER_LOG

SF_NOTIFY_END_OF_NET_SESSION

The server is writing data to the log about the current session.

OnEndOfNetSession

(none)


You can view the work a filter does along two lines: filtering data and processing requests. When the server sends or receives data from the client, your filter gets a chance to process the data. Normally, the server fulfills user requests by retrieving the appropriate Web page or executing a script and then sending the HTML back to the client. If you specify SF_NOTIFY_SEND_RAW_DATA, the server will call your filter with a buffer containing the return data—before the data is actually sent, but after the server has completely retrieved it and processed it. Likewise, if you specify SF_NOTIFY_READ_RAW_DATA, then as soon as the server receives new information from a client, it passes the information to your filter before doing any processing of its own. Your filter can translate, massage, or completely mutilate the data to your heart's content, before the server ever sees it.

How does the server "notify" your DLL with new data? By calling the other entry point, HttpFilterProc.


 DWORD HttpFilterProc(
   HTTP_FILTER_CONTEXT * pfc,
   DWORD NotificationType,
   VOID * pvNotification);

The filter proc is similar to an ordinary window proc. The server calls HttpFilterProc whenever something interesting happens. pfc is a pointer to an HTTP_FILTER_CONTEXT structure (see Figure 3) that contains information about the connection, such as whether it has been authenticated or not. The context provides a generic VOID pointer, pFilterContext, that you can use to hold your own connection-specific information. It also holds several useful server-supplied callback functions, some of which I'll describe shortly. NotificationType is a code such as SF_NOTIFY_SEND_RAW_DATA that identifies the event, and pvNotification is a pointer to event-specific information, like LPARAM for a window procedure (see Figure 2).

Figure 3 ISAPI Filter Context


 //////////////////
// Filter context structuure
// The server calls your HttpFilterProc with a pointer to one of these.
//
struct HTTP_FILTER_CONTEXT {
   DWORD          cbSize;           // size in bytes
   DWORD          Revision;         // struct revision level
   PVOID          ServerContext;    // server-private info
   DWORD          ulReserved;       // reserved for server: do not use!
   BOOL           fIsSecurePort;    // port is authenticated ?
   PVOID          pFilterContext;   // (your) filter-private info

   //  Server callbacks
   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);
};


////////////////
// MFC wrapper for HTTP_FILTER_CONTEXT
//
class CHttpFilter {
public:
   CHttpFilterContext(PHTTP_FILTER_CONTEXT pfc);
   ~CHttpFilterContext() { }

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

   // pointer to the actual HTTP_FILTER_CONTEXT
   PHTTP_FILTER_CONTEXT const m_pFC;
};

Data filters are immensely powerful because you can alter the data any way you like. You might compress it, expand it, encrypt it, decrypt it, or map it all to lowercase. The main drawback is that raw data notifications are rather expensive, since the server must notify you frequently with great wads of data.

The READ/SEND_RAW_DATA notifications include only the actual content data sent to and from the server, not the HTTP header information that wraps it. The HTTP headers describe the format, age, last modification date, size, and language of the content data, among other things. There are times when you may want to examine this information. For example, you might want to look at the "Referer:" field in client requests so you can see how users are finding out about your server, or you might want to change something in the reply header before it's sent to the client. Either way, to see header information, you must specify SF_NOTIFY_PREPROC_HEADERS in your version flags. Another common use for SF_NOTIFY_PREPROC_HEADERS is to implement "cookies." Cookies are HTML extensions that allow clients to hold persistent data for servers, such as context information between two parts of a search or merchandise order. Cookies aren't part of the HTML standard, but most browsers can handle them.

Another useful filter event is SF_NOTIFY_URL_MAP. Clients download pages from your Web server by requesting a specific universal resource locator (URL). The URL identifies the type of resource (which is always "http:" for web servers) and its location: server name, directory path, and filename. For example, if you work at Microsoft, you might find a file loaded with hockey statistics for a team in New England on a server named MOOSEBOY using this URL:


 http://mooseboy/hockey/whalers/team_stats.html

When the server receives this request, it notifies any filters registered with the SF_NOTIFY_URL_MAP flag. Your filter can alter the URL if it likes; this is a great way to make your site respond as if it implements virtual directories. If you use SF_NOTIFY_URL_MAP to change the logical look of your Web site, you might also want to use SF_NOTIFY_PREPROC_HEADERS to fiddle with the headers sent back from your server to the client, since the HTTP header may itself contain URLs and other information about the page returned, and many browsers rely on this information to make decisions about how to process the page once it's returned to the client.

One of the most interesting notifications is SF_NOTIFY_AUTHENTICATION. The server sends this notification when a client browser tries to authenticate its connection. If you want to perform your own authentication—for example, by looking up the user's account in a database to make sure he has prepaid for this session—you can do so by handling SF_NOTIFY_AUTHENTICATION.

SF_NOTIFY_END_OF_NET_SESSION isn't very interesting, but it's important. The server uses it to notify your DLL when a connection is about to end. This is your chance to clean up any per-connection state information you may have allocated. If you don't find it amusing to write your own clean-up code in response to SF_NOTIFY_END_OF_NET_SESSION, you can use the HTTP_FILTER_CONTEXT::AllocMem callback to allocate all your per-connection storage. The server will automatically free it when the connection terminates.

Two other flags, SF_NOTIFY_SECURE_PORT and SF_NOTIFY_NONSECURE_PORT, tell the server you are interested in connections involving either secured or non-secured ports. If you're writing a filter that performs authentication, you'll want to talk with non-secured ports only. If you're writing a filter that performs encryption, you might want it to run only on a secured port that's already been authenticated. If your filter simply maps resource strings or watches raw data go by, it's up to you what ports you watch. You can watch both non-secured and secured ports from the same filter by using both flags. SF_NOTIFY_SECURE_PORT and SF_NOTIFY_NONSECURE_PORT are different from the other flags in that they don't correspond to any particular events; there is no "SF_NOTIFY_SECURE_PORT" event.

If you are able to initialize your filter properly, you should return a non-zero value from GetFilterVersion. If you have registered your filter correctly, the server will load it whenever the server starts up, and begins notifying you of whatever events you requested in your version flags. If several filters are registered for the same notifications, the server is allowed to decide which it will notify first. Microsoft's server uses the same order as the registry entries listing the DLLs.

If you're using MFC to write a filter, you don't have to write GetFilterVersion and HttpFilterProc. Instead, you derive your own filter class from CHttpFilter and override the virtual function CHttpFilter::GetFilterVersion and whichever specific event handlers you are interested in (for example, CHttpFilter::OnReadRawData to translate incoming data or CHttpFilter::OnUrlMap to mess with the URL mapping). You then create a single static instance of your class, just like you create a single instance of your CWinApp-derived class, usually called the App. Internally, MFC provides its own ::GetFilterVersion and ::HttpFilterProc, which get linked with your code. The MFC versions route calls to the appropriate CHttpFilter virtual functions, just like MFC routes window messages to your handler functions like OnGetFocus or OnCreate. The major difference is that with CHttpFilter, you don't need message maps, you just implement the function. There is a virtual function CHttpFilter::HttpFilterProc you can override, but normally you shouldn't have to since the default implementation routes notifications to the appropriate event handler. MFC even converts the generic VOID* pvNotification argument into the appropriate type-safe event-specific structure pointer. Figure 4 shows the definition of CHttpFilter and Figure 2 shows which member functions correspond to which filter events.

Figure 4 MFC Filer Class


 //////////////////
// MFC class for ISAPI filters
// To implement a filter, you override the appropriate virtual functions.
//
// You should instantiate one of these in your filter module, 
// just like "theApp".
//
class CHttpFilter {
public:
   CHttpFilter();
   ~CHttpFilter();

   // Virtual analogs for DLL entry points
   virtual DWORD HttpFilterProc(PHTTP_FILTER_CONTEXT pfc,   
       DWORD dwNotificationType, LPVOID pvNotification);
   virtual BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer);

   // Specific virtual notifcation handlers
   virtual DWORD OnReadRawData(CHttpFilterContext* pfc, 
      PHTTP_FILTER_RAW_DATA pRawData);
   virtual DWORD OnPreprocHeaders(CHttpFilterContext* pfc, 
      PHTTP_FILTER_PREPROC_HEADERS pHeaders);
   virtual DWORD OnAuthentication(CHttpFilterContext* pfc, 
      PHTTP_FILTER_AUTHENT pAuthent);
   virtual DWORD OnUrlMap(CHttpFilterContext* pfc, 
      PHTTP_FILTER_URL_MAP pUrlMap);
   virtual DWORD OnSendRawData(CHttpFilterContext* pfc, 
      PHTTP_FILTER_RAW_DATA pRawData);
   virtual DWORD OnLog(CHttpFilterContext* pfc, PHTTP_FILTER_LOG pLog);
   virtual DWORD OnEndOfNetSession(CHttpFilterContext* pfc);
};

OLDBROW

To put the discussion in the context of a real live working program, I wrote a simple filter, OLDBROW (see Figure 5), that you can use to exclude users from your Web server if they're not running a particular browser. Not very friendly, but it's the simplest example I could think of. COldBrowFilter overrides just two virtual functions: GetFilterVersion and OnUrlMap. GetFilterVersion is easy; it calls the base class implementation to set dwFilterVersion and then sets SF_NOTIFY_URL_MAP to tell the server I'm interested in URL mapping events. It also copies the resource string "OldBrow Filter" into lpszFilterDesc.

Figure 5 OLDBROW

OLDBROW.RC


 //Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//

#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

#ifdef APSTUDIO_INVOKED

/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE
BEGIN
       "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE
BEGIN
       "#include ""afxres.h""\r\n"
       "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#define _AFX_NO_SPLITTER_RESOURCES\r\n"
    "#define _AFX_NO_OLE_RESOURCES\r\n"
    "#define _AFX_NO_TRACKER_RESOURCES\r\n"
    "#define _AFX_NO_PROPERTY_RESOURCES\r\n"
       "\r\n"
       "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\n"
       "#include ""OldBrow.rc2""  // non-MS Visual C++ edited resources\r\n"
       "#include ""afxres.rc""         // Standard components\r\n"
       "#include ""afxisapi.rc""       // Internet Support resources\r\n"
       "#endif"
       "\0"
END
#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Version
//

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,1
 PRODUCTVERSION 1,0,0,1
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x4L
 FILETYPE 0x2L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904B0"
        BEGIN
            VALUE "CompanyName", "Microsoft\0"
            VALUE"FileDescription","OldBrowInternetServerExtensionModule\0"
            VALUE "FileVersion", "1, 0, 0, 1\0"
            VALUE "InternalName", "OLDBROW\0"
            VALUE "LegalCopyright", "Copyright © 1996 Microsoft\0"
            VALUE "LegalTrademarks", "\0"
            VALUE "OriginalFilename", "OLDBROW.DLL\0"
            VALUE "ProductName", "OldBrow Internet Server Extension\0"
            VALUE "ProductVersion", "1, 0, 0, 1\0"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1200
    END
END

/////////////////////////////////////////////////////////////////////////////
//
// String Table
//

STRINGTABLE DISCARDABLE
BEGIN
       IDS_FILTER        "OldBrow Filter"
END

#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//

#define _AFX_NO_SPLITTER_RESOURCES
#define _AFX_NO_OLE_RESOURCES
#define _AFX_NO_TRACKER_RESOURCES
#define _AFX_NO_PROPERTY_RESOURCES

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#include "OldBrow.rc2"  // non-Microsoft Visual C++ edited resources
#include "afxres.rc"         // Standard components
#include "afxisapi.rc"       // Internet Support resources
#endif

#endif    // not APSTUDIO_INVOKED

OLDBROW.RC2


 //
// OldBrow.RC2 - resources Microsoft Visual C++ does not edit directly
//

#ifdef APSTUDIO_INVOKED
       #error this file is not editable by Microsoft Visual C++
#endif //APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
// Add manually edited resources here...

/////////////////////////////////////////////////////////////////////////////

OLDBROW.H


 // OLDBROW.H - Implementation file for your Internet Server
//    OldBrow Filter


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

       BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer);

       DWORD OnUrlMap(CHttpFilterContext* pCtxt,
              PHTTP_FILTER_URL_MAP pMapInfo);

       // TODO: Add your own overrides here
};

OLDBROW.CPP


 // OLDBROW.CPP - Implementation file for your Internet Server
//    OldBrow Filter

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

// NOTE!
// You will (probably) need to change this #define to point at the
// right file as installed on your system.  You'll get "Not found"
// errors from your browser if it isn't working.

#define TOO_OLD "C:\\INETSRV\\WWWROOT\\OLDBROW.HTM"

///////////////////////////////////////////////////////////////////////
// The one and only COldBrowFilter object

COldBrowFilter theFilter;


///////////////////////////////////////////////////////////////////////
// COldBrowFilter implementation

COldBrowFilter::COldBrowFilter()
{
}

COldBrowFilter::~COldBrowFilter()
{
}

BOOL COldBrowFilter::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_URL_MAP;

       // 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 COldBrowFilter::OnUrlMap(CHttpFilterContext* pCtxt,
       PHTTP_FILTER_URL_MAP pMapInfo)
{
       char szUserAgent[1024];

       DWORD dwSize = sizeof(szUserAgent);
       if (pCtxt->GetServerVariable("HTTP_USER_AGENT", szUserAgent, &dwSize))
       {
              // Old versions of Microsoft Internet Explorer say "Microsoft
              // Internet Explorer" in their User-Agent: header.  We'll not
              // allow access from those browsers because the user should
              // upgrade to Microsoft Internet Explorer 2.0, which uses
              // the abbreviation "MSIE" instead.

              szUserAgent[dwSize] = '\0';
              if (strncmp(szUserAgent,"Microsoft Internet Explorer", 25) == 0)
          strncpy(pMapInfo->pszPhysicalPath, TOO_OLD, pMapInfo->cbPathBuff-1);
       }

       return SF_STATUS_REQ_NEXT_NOTIFICATION;
}

///////////////////////////////////////////////////////////////////////
// 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 around 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;
}

****/

OnUrlMap is a little more complex, but not much. Most of the filter notification handlers receive a CHttpFilterContext* and a pointer to an event-specific structure like HTTP_FILTER_URL_MAP, which is specific to URL mappings. CHttpFilterContext is an MFC wrapper for HTTP_FILTER_CONTEXT that lets you invoke the HTTP_FILTER_CONTEXT callback functions using normal C++ notation. In OLDBROW, I use GetServerVariable to get the value of the HTTP_USER_AGENT variable. This is just the value of the "User-Agent" field in the HTTP request header. Browsers identify themselves to servers by sending their name and version information in the User-Agent header field of any request they make. For OLDBROW, if the User Agent is "Microsoft Internet Explorer" (as opposed to "Microsoft Internet Explorer 2.0"), I change whatever URL the browser requested to a hardwired value "c:\...\oldbrow.htm" which is an HTML page that basically says, "Sorry, Charlie" (see Figure 6). Otherwise the request goes through unaltered. Either way, OnUrlMap returns SF_STATUS_REQ_NEXT_NOTIFICATION to tell the server to pass the event along to the next filter, if any (see Figure 5).

Figure 6 Sorry, Charlie.

While OLDBROW doesn't use it, WriteClient is easily the most interesting CHttpFilterContext member function. It writes data directly back to the client. You can use this function to send anything you'd like back to the client, be it HTML data or raw data in some other format you've announced to the client. You can use it in conjunction with AddResponseHeaders to formulate complete replies to any request. You should call AddResponseHeaders first, then WriteClient to actually send the data. If you're not using C++ and MFC, these functions are still available as callback members of HTTP_FILTER_CONTEXT.

As I mentioned at the outset, the major benefit of ISAPI is that it lets servers execute a Web extension from within multiple threads within its process space instead of launching a new process for every connection. One consequence of this is that your code must be thread-safe! The server may call your DLL's HttpFilterProc function (or virtual CHttpFilter::HttpFilterProc override) concurrently from many different threads on behalf of many ongoing client connections. In practice, all this means is that you must be careful not to use globals so your code will be reentrant. In particular, if you need to maintain per-connection information, you can't just store a pointer to the CHttpFilterContext object MFC sends you. Fortunately, HTTP_FILTER_CONTEXT contains a VOID* member, pFilterContext, that's designed for just this purpose. You can use it to store a pointer to your own connection-specific data. You can create the structure the first time you get a notification from a particular connection, then delete it in OnEndOfNetSession. (If you use CHttpFilterContext::AllocMem, the system will delete it for you.)

Aside from thread considerations, the whole trick to programming a filter is efficiency. You don't want to do anything that isn't absolutely necessary because time is of the essence. Wasting time in a filter is bad! It steals cycles from the server, so the server can't work as quickly, so users get bored staring at the hourglass. Don't forget that whatever inefficiencies you introduce will be multiplied as you service many clients concurrently.

Server Applications

I told you that there are two basic kinds of ISAPI server extensions: filters and server apps. We just finished filters; now it's time to turn our attention to server applications. ISAPI server applications are a great way to implement interactive Web apps. You can use server applications (also called extensions) to process forms, generate log files, or respond to user queries. Server applications are a lot like filters. They are just DLLs with two special entry points, GetExtensionVersion and HttpExtensionProc.


 BOOL    GetExtensionVersion( HSE_VERSION_INFO  *pVer );
DWORD HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);

You can even implement a filter and an extension in the same DLL! (But at most only one of each.) Figure 7 shows the structures these functions use. As with HTTP_FILTER_CONTEXT, EXTENSION_CONTROL_BLOCK contains a number of useful callbacks like WriteClient and GetServerVariable. MFC wraps it all up the way you would expect. MFC provides its own DLL entry points; instead of writing GetExtensionVersion and HttpExtensionProc, you derive your own class from CHttpServer, override the appropriate virtual functions, and instantiate exactly one static instance. MFC also provides CHttpServerContext to wrap EXTENSION_CONTROL_BLOCK. (MFC uses more consistent naming conventions.) Figure 8 shows the definitions for CHttpServer and CHttpServerContext.

Figure 7 Basic Structures for Server Apps


 ////////////////
// Version info structure for GetExtensionVersion 
//
struct HSE_VERSION_INFO {
    DWORD  dwExtensionVersion;
    CHAR   lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];
};

////////////////
// The server calls your HttpExtensionProc with one of these
//
struct EXTENSION_CONTROL_BLOCK {

    DWORD     cbSize;                 // size of this struct.
    DWORD     dwVersion;              // version info of this spec
    HCONN     ConnID;                 // Context number not to be modified!
    DWORD     dwHttpStatusCode;       // HTTP Status code

    // log info specific to this Extension DLL
    CHAR      lpszLogData[HSE_LOG_BUFFER_LEN];

    LPSTR     lpszMethod;             // REQUEST_METHOD
    LPSTR     lpszQueryString;        // QUERY_STRING
    LPSTR     lpszPathInfo;           // PATH_INFO
    LPSTR     lpszPathTranslated;     // PATH_TRANSLATED

    DWORD     cbTotalBytes;           // Total bytes indicated from client
    DWORD     cbAvailable;            // Available number of bytes
    LPBYTE    lpbData;                // pointer to cbAvailable bytes

    LPSTR     lpszContentType;        // Content type of client data

    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 );

};

Figure 8 MFC Classes for Server Apps


 ///////////////////////////////////////////////////////////////////////
// Internet Information Server Extension Support
//
class CHttpServer {
public:
   CHttpServer(TCHAR cDelimiter = '&');
   ~CHttpServer();

   enum errors {
      callOK = 0,          // everything is fine
      callParamRequired,   // a required parameter was missing
      callBadParamCount,   // there were too many or too few parameters
      callBadCommand,      // the command name was not found
      callNoStackSpace,    // no stack space was available
      callNoStream,        // no CHtmlStream was available
      callMissingQuote,    // a parameter had a bad format
      callMissingParams,   // no parameters were available
      callBadParam,        // a paremeter had a bad format (ie, only one quote)
   };

// overridables
   virtual int CallFunction(CHttpServerContext* pCtxt,
      LPTSTR pszQuery, LPTSTR pszCommand);
   virtual  BOOL OnParseError(CHttpServerContext* pCtxt, int nCause);

// operations
   virtual void EndContent(CHttpServerContext* pCtxt) const;
   virtual void StartContent(CHttpServerContext* pCtxt) const;
   virtual void WriteTitle(CHttpServerContext* pCtxt) const;
   virtual LPCTSTR GetTitle() const;
   void AddHeader(CHttpServerContext* pCtxt, LPCTSTR pszString) const;

   virtual DWORD HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
   virtual BOOL GetExtensionVersion(HSE_VERSION_INFO *pVer);
   virtual CHtmlStream* ConstructStream();

   virtual BOOL InitInstance(CHttpServerContext* pCtxt);

// implementation

protected:
   UINT PASCAL GetStackSize(const BYTE* pbParams);
   int CallMemberFunc(CHttpServerContext* pCtxt,
      const AFX_PARSEMAP_ENTRY* pEntry,
      AFX_PARSEMAP_ENTRY* pParams, LPTSTR szParams);
   LPTSTR GetQuery(CHttpServerContext* pCtxt,
      LPTSTR lpszQuery, DWORD cbQuery);
    const AFX_PARSEMAP_ENTRY* LookUp(LPCTSTR szMethod,
      const AFX_PARSEMAP*& pMap, AFX_PARSEMAP_ENTRY*& pParams,
      AFX_PISAPICMD pCmdDefault = NULL);
   int CountParams(LPCTSTR pszCommandLine, int& nCount);
   int ParseDefaultParams(AFX_PARSEMAP_ENTRY* pParams,
      int nParams, AFX_PARSEMAP_ENTRY_PARAMS*& pBlock,
      const BYTE* pbTypes);
   LPVOID PreprocessString(LPTSTR psz);
   void BuildStatusCode(LPTSTR szResponse, DWORD dwCode);

#if defined(_PPC_) || defined(_MPPC_)
   int PushDefaultStackArgs(BYTE* pStack,
      CHttpServerContext* pCtxt, const BYTE* pbParams,
      LPTSTR lpszParams, AFX_PARSEMAP_ENTRY_PARAMS* pDefParams,
      int nSizeArgs);
   int PushStackArgs(BYTE* pStack, CHttpServerContext* pCtxt,
      const BYTE* pbParams, LPTSTR lpszParams, UINT nSizeArgs);
   BYTE* StoreStackParameter(BYTE* pStack, BYTE nType,
      LPTSTR pszCurParam, UINT nSizeArgs, BOOL bDoShadow);
   BYTE* StoreRawStackParameter(BYTE* pStack, BYTE nType,
      BYTE* pRawParam, int nSizeArgs);
#else
   int PushDefaultStackArgs(BYTE* pStack,
      CHttpServerContext* pCtxt, const BYTE* pbParams,
      LPTSTR lpszParams, AFX_PARSEMAP_ENTRY_PARAMS* pDefParams);
   int PushStackArgs(BYTE* pStack, CHttpServerContext* pCtxt,
      const BYTE* pbParams, LPTSTR lpszParams);
   BYTE* StoreStackParameter(BYTE* pStack, BYTE nType, LPTSTR pszParam);
   BYTE* StoreRawStackParameter(BYTE* pStack, BYTE nType, BYTE* pRawParam);
#endif

   LPCRITICAL_SECTION m_pCritSec;
   const TCHAR m_cTokenDelimiter;   // can't EVER change

   DECLARE_PARSE_MAP()
};

//////////////////
// MFC wrapper for server context (EXTENSION_CONTROL_BLOCK)
//
class CHttpServerContext {
public:
   CHttpServerContext(EXTENSION_CONTROL_BLOCK* pECB);
   virtual ~CHttpServerContext();

// Operations
    BOOL GetServerVariable(LPTSTR lpszVariableName,
      LPVOID lpvBuffer, LPDWORD lpdwSize);
    BOOL WriteClient(LPVOID lpvBuffer,LPDWORD lpdwBytes,DWORD dwReserved = 0);
    BOOL ReadClient(LPVOID lpvBuffer, LPDWORD lpdwSize);
    BOOL ServerSupportFunction(DWORD dwHSERRequest,
      LPVOID lpvBuffer, LPDWORD lpdwSize, LPDWORD lpdwDataType);

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

   void Reset();

// Attributes
   EXTENSION_CONTROL_BLOCK* const m_pECB;
   CHtmlStream* m_pStream;
   DWORD m_dwEndOfHeaders;
};

There are a lot of details about server applications I'm going to skip in order to focus on the really interesting thing, which is how clients interact with your server extension. As I mentioned at the outset, the key to having an interactive Web app is that you need some way to pass arguments to the server app, invoke its functions, and get a result back. To see how argument-passing works in practice with ISAPI, let's consider the MOOSEBOY server I mentioned earlier, the one that reports the latest hockey scores. A typical MOOSEBOY client begins by requesting a URL to get the score for the Hartford-Pittsburgh game:


 http://mooseboy/hockey/hockey_stats.dll?GameScore&HFD&PIT

You may be wondering what all that funny stuff at the end of the URL is? Don't get excited, it's just a convention for passing argument values. When the server processes this request, it understands that the portion of the URL preceding the ? identifies an executable file (DLL). The rest says: call the GameScore function with "HFD" and "PIT" as arguments.

If the user's connection allows executable privileges for the directory and file in question (hockey\hockey_stats.DLL), the server loads hockey_stats.dll and looks for an entry point named GetExtensionVersion. GetExtensionVersion accepts a pointer to an HSE_VERSION_INFO structure. The filter fills the structure and, assuming it initializes OK, is ready for action.

Unlike filters, which are loaded when the server first starts up, an extension isn't loaded until a client requests it. Once loaded, however, the server will keep the extension in memory as long as possible—which on a well-endowed server darn-near means forever. In particular, this means GetExtensionVersion is called only once.

HttpExtensionProc, on the other hand, is called each and every time a client asks (through the server) the extension to do some work. If six people looking for hockey scores connect to MOOSEBOY concurrently, hockey_stats.dll loads only once, but the server calls HttpExtensionProc six times, once for each connection. Like filters, your server application must be reentrant and thread-safe.

Once the DLL is loaded, the server calls your HttpExtensionProc, passing it the command string ("GameScore?HFD&PIT") inside the EXTENSION_CONTROL_BLOCK structure. If you're using C, it's up to you to parse the command and arguments and determine what function(s) within the DLL to invoke. In this case, you would invoke the GameScore function with "HFD" and "PIT" as arguments.

If you're using MFC, things are even easier. The default implementation for CHttpServer::HttpExtensionProc parses the command string for you and then looks for an appropriate entry in your parse command map. A parse command map is like the ordinary message maps we all know and love. Instead of writing a lot of ugly code to parse command strings and dispatch to functions in your extension DLL, all you do is write a handler for each operation you support, and then add an entry for it in the parse command map.

My hockey server has a parse map that looks something like this:


 BEGIN_PARSE_MAP(CHockeyServer, CHttpServer)
   ON_PARSE_COMMAND(GameScore, CHockeyServer, 
                    ITS_PSTR, ITS_PSTR, ITS_PSTR)
END_PARSE_MAP(CHockeyServer)

This map tells MFC I have a command "GameScore" that has three string arguments. GameScore is also the name of the member function—I'll have to run off and implement it to examine the parameters it receives and generate a reply. Usually, information is returned as HTML, but by tweaking the HTTP headers I could return an image or sound file instead. If the parameters the client sends to my GameScore function are bogus, I can return an error message like "Say what?" formatted as HTML. On the other hand, if the user requested a game actually played on a given date, I might return a JPEG image of the winning goal along with a link to a URL for the box scores.

The parse map entry for GameScore describes three string parameters (the three ITS_PSTR symbols), but the URL showed only two parameters: HFD and PIT. You can imagine that a puckhead like me might forget to specify the date if the game was today. In this case, the server should do the right thing. In other words, the Date parameter should be optional, with today's date as the default value. The parse map above makes all three strings mandatory, but I can alter it slightly to name the parameters and provide defaults.


 BEGIN_PARSE_MAP(CHockeyServer, CHttpServer)
   ON_PARSE_COMMAND(GameScore, CHockeyServer, 
                    ITS_PSTR, ITS_PSTR, ITS_PSTR)
   ON_PARSE_COMMAND_PARAMS("HomeTeam AwayTeam
                            Date='~'")
END_PARSE_MAP(CHockeyServer)

Now HomeTeam and AwayTeam are still required, but Date has a default value of tilde (~). When my GameScore function gets a date of "~", it substitutes the current date. I chose a tilde just because it's a nifty out-of-bounds character, and it has a funny name. With this new map, all of the following URLs would call my GameScore function with HomeTeam="HFD", AwayTeam="PIT", and Date="~".


 http://mooseboy/hockey/
       hockey_stats.dll?GameScore&HFD&PIT
http://mooseboy/hockey/
    hockey_stats.dll?GameScore&AwayTeam=PIT&HomeTeam=HFD

The following URLs request information about the Vancouver-Anaheim game on my birthday.


 http://mooseboy/hockey/
       hockey_stats.dll?GameScore&VAN&ANA&1/25/96
http://mooseboy/hockey/
       hockey_stats.dll?GameScore&Date=
       1/25/96&AwayTeam=ANA&HomeTeam=VAN

Since the parameters are named, their order doesn't matter. If I don't supply names, I must follow the order in which they appear in the parse map entry. Of course, my C++ member function CHockeyServer::GameScore always gets the parameters in the declared order.


 void CHockeyServer::GameScore(CHttpServerContext*pCtxt,  
LPCSTR pszHomeTeam,
LPCSTR pszAwayTeam,
LPCSTR psxDate);

The types of the parameters must match the types defined in ON_PARSE_COMMAND. MFC uses manifest constants like ITS_PSTR to identify the parameter types. Since the URLs aren't data-intensive, only a handful of types are supported (see Figure 9).

Figure 9 MFC Arg Types

ITS_EMPTY

no parameters

ITS_I2

short

ITS_I4

long

ITS_R4

float

ITS_R8

double

ITS_PSTR

LPCTSTR


If the URL syntax seems cumbersome, it is. No one ever intended it to be used by humans. You don't just walk up to your browser and pound out a long URL with a bunch of variables and values at the end. In most cases, you use a nice user-friendly form. It's pretty easy to design an HTML form with dropdown lists to select teams and an edit box to enter the date. The form would have a button called "Send" or maybe something colorful like "Drop the puck!" to send the request to the server. When the user clicks Send, the browser constructs the nasty URL according to instructions contained in the form's HTML source code. Since the source code identifies the first dropdown as "HomeTeam" and the second dropdown as "AwayTeam", the browser generates a string with those parameters coded in the URL. Once the browser has done this, it ships the URL off to the server, which loads hockey_server.dll (if it isn't loaded already) and calls the HttpExtensionProc entry (supplied by MFC), which parses the arguments and grovels through your parse maps to find the right function to call—namely, GameScore. GameScore generates whatever kind of reply it wants—the score formatted as an HTML file, a video or audio clip, or whatever, and sends it back to the client. Amazing!

You should be able to see how general this is. The same basic mechanism lets clients invoke any function in your DLL, passing any number of string or numeric arguments.

A Dip in the Stream

The obvious goal of most extensions is to return information in the form of HTML text to the client, even if all it says is "Congratulations, your name is now on our eternal mailing list, we'll send you overpriced offers for things you don't want, starting now and until you die." But how exactly do you send the reply back?

CHttpServerContext has a member function named WriteClient that writes data back to the client. The problem with WriteClient is that it sends data immediately, which in most cases is not good because you need to assemble your response piecemeal and each call to WriteClient causes a network hit since the server does no buffering. To help relieve your network load, MFC provides a simple class, CHtmlStream, that does buffered output to the client. CHttpServerContext contains an initialized CHtmlStream object prepared exclusively for the call in progress. Your handler function (GameScore) can use CHttpServerContext's streaming operators to build the response incrementally before sending it off.


 void CHockeyServer::GameScore(CHttpServerContext*   
                              pCtxt, LPTSTR pstrHome,
                              LPTSTR pstrAway, 
                              LPTSTR pstrDate)
{
      StartContent(pCtxt);
      WriteTitle(pCtxt);
      *pCtxt << "Game on: " << pstrDate;
      *pCtxt << "\r\n";

      // get scores from database
      // into nHomeScore and nAwayScore
      *pCtxt << pstrHome << " score is " << nHomeScore;
      *pCtxt << " and " << pstrAway << " score is ";
      *pCtxt << nAwayScore;
      EndContent(pCtxt);
}

When GameScore returns, it gives control back to MFC. If there were no errors, MFC sends out the HTML header to indicate success and then dumps, all at once, the contents of the CHtmlStream owned by pCtxt. Note that GameScore calls StartContent, WriteTitle, and EndContent. These are virtual CHttpServer functions you can override. StartContent writes (to the stream) a simple HTTP header, then opens an HTML page; EndContent closes that page. WriteTitle grabs the title by calling yet another virtual CHttpServer function, GetTitle, and wraps it in the appropriate HTML tags. Most browsers show the title in the window caption (and in history and favorite site lists), so choose a title that's brief but unique and descriptive. (You can skip WriteTitle if your handler doesn't return HTML.) These functions work nicely if you're sending HTML, but if you want to return data in a different format—a JPEG picture or an AU sound file—you'll have to override StartContent to emit the appropriate HTTP header. Another alternative is to not call StartContent at all, but write the headers manually from your handler function.

This simplified hockey example only touches on the power and flexibility afforded by dynamically generated HTML. The possibilities are limitless. Anything you can implement in C++ can be plopped on a web site for everyone in the world to use. For now, the real bottleneck is HTML and HTTP.

MFCTALK

Just so you don't think I'm waving my hands with the hockey example, I wrote a sample Internet server application, MFCTALK, that implements a "chat room" (see Figures 10 and 11). Web clients can execute the server DLL with no parameters to get a form that tells them how to join the conversation, and also shows the last twenty comments in the stack. When the user presses a button, the form invokes a function that adds their comment and returns the current pool of comments from other users. To make MFCTALK more interesting, I implemented a function that generates a random comment from a pool of dead celebrities.

Figure 10 MFCTALK

MFCTALK.RC


 //Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//

#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

#ifdef APSTUDIO_INVOKED

/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE
BEGIN
       "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE
BEGIN
       "#include ""afxres.h""\r\n"
       "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#define _AFX_NO_SPLITTER_RESOURCES\r\n"
    "#define _AFX_NO_OLE_RESOURCES\r\n"
    "#define _AFX_NO_TRACKER_RESOURCES\r\n"
    "#define _AFX_NO_PROPERTY_RESOURCES\r\n"
       "\r\n"
       "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\n"
       "#include ""MFCTalk.rc2""  // non-MS Visual C++ edited resources\r\n"
       "#include ""afxres.rc""         // Standard components\r\n"
       "#include ""afxisapi.rc""       // Internet Support resources\r\n"
       "#endif"
       "\0"
END
#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Version
//

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,1
 PRODUCTVERSION 1,0,0,1
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x4L
 FILETYPE 0x2L
 FILESUBTYPE 0x0L
BEGIN
    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904B0"
        BEGIN
            VALUE "CompanyName", "Microsoft\0"
            VALUE"FileDescription","MFCTalkInternetServerExtensionModule\0"
            VALUE "FileVersion", "1, 0, 0, 1\0"
            VALUE "InternalName", "MFCTALK\0"
            VALUE "LegalCopyright", "Copyright © 1996 Microsoft\0"
            VALUE "LegalTrademarks", "\0"
            VALUE "OriginalFilename", "MFCTALK.DLL\0"
            VALUE "ProductName", "MFCTalk Internet Server Extension\0"
            VALUE "ProductVersion", "1, 0, 0, 1\0"
        END
    END
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x409, 1200
    END
END

/////////////////////////////////////////////////////////////////////////////
//
// String Table
//

STRINGTABLE DISCARDABLE
BEGIN
       IDS_SERVER        "MFCTalk Extension"
END

#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//

#define _AFX_NO_SPLITTER_RESOURCES
#define _AFX_NO_OLE_RESOURCES
#define _AFX_NO_TRACKER_RESOURCES
#define _AFX_NO_PROPERTY_RESOURCES

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#include "MFCTalk.rc2"       // non-Microsoft Visual C++ edited resources
#include "afxres.rc"         // Standard components
#include "afxisapi.rc"       // Internet Support resources
#endif

#endif    // not APSTUDIO_INVOKED

MFCTALK.RC2


 //
// MFCTalk.RC2 - resources Microsoft Visual C++ does not edit directly
//

#ifdef APSTUDIO_INVOKED
       #error this file is not editable by Microsoft Visual C++
#endif //APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
// Add manually edited resources here...

/////////////////////////////////////////////////////////////////////////////

MFCTALK.H


 // MFCTALK.H - Implementation file for your Internet Server
//    MFCTalk Extension

class CMFCTalkExtension : public CHttpServer
{
public:

CMFCTalkExtension();
       ~CMFCTalkExtension();

       BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer);

protected:
       CCriticalSection m_ListCritical;
       CStringList m_ListContent;

protected:
       void GetCharacter();
       void WriteChatForm(CHttpServerContext* pCtxt);
       void WriteRoomContent(CHttpServerContext* pCtxt, int nTimes);

       void AddComment(LPCTSTR pstrName, LPCTSTR pstrSays);

public:
       void StartContent(CHttpServerContext* pCtxt);

       void Default(CHttpServerContext* pCtxt, int nTimes);
       void Comment(CHttpServerContext* pCtxt, LPCTSTR pstr);
       void Refresh(CHttpServerContext* pCtxt, int nTimes);

       DECLARE_PARSE_MAP()
};

MFCTALK.CPP


 // MFCTALK.CPP - Implementation file for your Internet Server
//    MFCTalk Extension

#include <afx.h>
#include <afxwin.h>
#include <afxisapi.h>
#include <afxmt.h>              // for locking

#include <winsock.h>       // for address translation

#include "resource.h"
#include "MFCTalk.h"

#pragma comment(lib, "wsock32.lib")       // for winsock

#define MAX_LIST_SIZE 20

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

BEGIN_PARSE_MAP(CMFCTalkExtension, CHttpServer)

       ON_PARSE_COMMAND(Default, CMFCTalkExtension, ITS_I4)
       ON_PARSE_COMMAND_PARAMS("Times=0")
       DEFAULT_PARSE_COMMAND(Default, CMFCTalkExtension)
       ON_PARSE_COMMAND(Refresh, CMFCTalkExtension, ITS_I4)
       ON_PARSE_COMMAND(Comment, CMFCTalkExtension, ITS_PSTR)
        ON_PARSE_COMMAND_PARAMS("Says")
END_PARSE_MAP(CMFCTalkExtension)

///////////////////////////////////////////////////////////////////////
// The one and only CMFCTalkExtension object

CMFCTalkExtension theExtension;


///////////////////////////////////////////////////////////////////////
// CMFCTalkExtension implementation

CMFCTalkExtension::CMFCTalkExtension()
{
}

CMFCTalkExtension::~CMFCTalkExtension()
{
       m_ListContent.RemoveAll();
}

BOOL CMFCTalkExtension::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);

       srand(1234);       // how random!

       return TRUE;
}

///////////////////////////////////////////////////////////////////////
// CMFCTalkExtension command handlers

void CMFCTalkExtension::GetCharacter()
{
       // make a person who isn't here say something funny

       int nPerson = rand() % 7;
       LPTSTR strPerson;
       LPTSTR strSays;
       
       switch (nPerson)
       {
       case 0:
              strPerson = "Marilyn M.";
              strSays = "Hello, Sailor!";
              break;

       case 1:
              strPerson = "Richard N.";
              strSays = "People have gotta know if their president is a crook.";
              break;

       case 2:
              strPerson = "Bill";
              strSays = "Does anybody know what we closed at?";
              break;

       case 3:
              strPerson = "Mike B.";
              strSays = "I'm the man!";
               break;

       case 4:
              strPerson = "John K.";
              strSays = "What can I do for my country?";
              break;

       case 5:
              strPerson = "Charles B.";
              strSays="Somedaysyou'reSuperman,andsomedaysyou'reClarkKent.";
              break;

       case 6:
              strPerson = "Flea";
              strSays = "You've got to play music like you really mean it.";
              break;
        }

       AddComment(strPerson, strSays);
       return;
} 

void CMFCTalkExtension::WriteChatForm(CHttpServerContext* pCtxt)
{
       // dump HTML to make the form

       *pCtxt << "<h2>Welcome to MFCTalk!</h2><hr>";
       *pCtxt << "<FORM ACTION=\"MFCTalk.dll?Comment\" \"METHOD=\"POST\">";
       *pCtxt << "Type something you'd like to share:<p>";
       *pCtxt << "<INPUT TYPE=\"text\" NAME=\"Says\" SIZE=60><p>";
       *pCtxt << "<INPUT TYPE=\"submit\" VALUE=\"Send\">";
       *pCtxt << "</FORM><FORM ACTION=\"MFCTalk.dll?Refresh\" 
                 \"METHOD=\"POST\">";
       *pCtxt << "<INPUT TYPE=\"submit\" VALUE=\"Refresh\">";
       *pCtxt << "Show Message Times<INPUT TYPE=\"checkbox\" NAME=\"Times\" 
                 CHECKED=\"1\">";
       *pCtxt << "</FORM><hr>";
} 

void CMFCTalkExtension::WriteRoomContent(CHttpServerContext* pCtxt, int nTimes)
{
       // through the list backwards (because that's time order)

       m_ListCritical.Lock();

       POSITION pos;
       pos = m_ListContent.GetTailPosition();

       // print something nice if there's no list

       if (pos == NULL)
              *pCtxt << "<i>Nobody has said anything yet.</i><p>";
       else
       {
              CString strThisOne;
              while (pos != NULL)
              {
                     LPCTSTR strThisOne = m_ListContent.GetPrev(pos);
                     if (nTimes == 0)
                     {
                            strThisOne = _tcschr(strThisOne, ' ');
                            if (strThisOne == NULL)
                                   continue;
                            strThisOne++;
                     }
                     *pCtxt << strThisOne;
              }
       }

       m_ListCritical.Unlock();
}

void CMFCTalkExtension::Default(CHttpServerContext* pCtxt, int nTimes)
{
       // by default, show the form and show the content

       StartContent(pCtxt);
       WriteTitle(pCtxt);

       WriteChatForm(pCtxt);
       WriteRoomContent(pCtxt, nTimes);

       EndContent(pCtxt);
}

void CMFCTalkExtension::AddComment(LPCTSTR pstrName, LPCTSTR pstrSays)
{
       // someone said something... add it to the list

       m_ListCritical.Lock();

       CString str;
       CTime timing = CTime::GetCurrentTime();

       str = timing.Format("%H:%M:%S ");

       str += "<b>";
       str += pstrName;
       str += "</b>:";
       str += pstrSays;
       str += "<p>";

       m_ListContent.AddTail(str);

       // is the list too big?

       if (m_ListContent.GetCount() > MAX_LIST_SIZE)
              m_ListContent.RemoveHead();

       m_ListCritical.Unlock();
} 

void CMFCTalkExtension::Refresh(CHttpServerContext* pCtxt, int nTimes)
{
       // if we're refreshing, just have a character say something
       // and dump the form and the list again

       if (nTimes)

       GetCharacter();
       Default(pCtxt, nTimes);
}

void CMFCTalkExtension::StartContent(CHttpServerContext* pCtxt)
{
       // remember to say taht the content always expires!

       AddHeader(pCtxt, "Pragma: no-cache\r\n");
       CHttpServer::StartContent(pCtxt);
}

void CMFCTalkExtension::Comment(CHttpServerContext* pCtxt, LPCTSTR pstr)
{
       StartContent(pCtxt);
       WriteTitle(pCtxt);

       // l-trim what they said

       while (_istspace(*pstr))
              pstr++;

       if (*pstr != NULL)
       {
              TCHAR sz[80];
               DWORD dwSize = 80;

              // query the user's IP address
              if (!pCtxt->GetServerVariable("REMOTE_HOST", sz, &dwSize))
                     _tcscpy(sz, "<i>unknown</i>");
              else
              {
                     // convert to binary address
                     char nAddr[4];
                     nAddr[0] = nAddr[1] = nAddr[2] = nAddr[3] = 0;
                     int nIndex = 0;
                     char *pstr = sz;

                     while (*pstr != '\0' && nIndex < 4)
                     {
                            if (*pstr == '.')
                                   nIndex++;
                            else
                            {
                                   nAddr[nIndex] *= 10;
                                   nAddr[nIndex] += (*pstr - '0');
                            }
                            pstr++;
                     }

                     // ask WinSock for host name
                     HOSTENT* pResult;
                     pResult = gethostbyaddr((const char*) &nAddr, 4,PF_INET);
                     if (pResult == NULL)
                            _tcscat(sz, " <i>(unresolved)</i>");
                     else
                            _tcscpy(sz, pResult->h_name);
              }

              // finally, add it!
              AddComment(sz, pstr);
       }

       // rewrite everything
        
       WriteChatForm(pCtxt);
       WriteRoomContent(pCtxt, 1);

       EndContent(pCtxt);
}

///////////////////////////////////////////////////////////////////////
// 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 around 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;
}

****/

Figure 11 MFCTALK in action.

MFCTALK implements several features worth noting. First, it overrides StartContent to emit an extra header line on every page the program produces. The content generated by MFCTALK is volatile: as soon as another user adds a comment, the previously generated pages become obsolete. This is something of a problem, since most Web browsers try to cache pages as a way of minimizing data transfer. That approach doesn't wear well with MFCTALK, so StartContent emits a header line


 Pragma: no-cache

that tells compliant browsers the page in question should not be cached. Since the page is never cached, the browser downloads it again each time the user requests it.

Second, MFCTALK provides a name to identify each participant in the chat. The user name comes from the IP address of the requester. I call CHttpServerContext::GetServerVariable("REMOTE_HOST") to get this name—the system asks the Windows Sockets API to retrieve the machine name for the IP address. This adds a nice touch to my program, but it's probably not a great idea for serious applications. The user could have arrived at the server through some route that masked their IP address—for example, they bounced through a couple of extra proxies or firewall servers, or they didn't use TCP/IP to log in. (In general, you shouldn't depend on TCP/IP or Winsock being available in your extension.) More importantly, the translation of an IP address to a machine name is an expensive operation. If you need to do this translation, you should do it on another machine away from your server, and do it offline. The way MFCTALK is written, it will certainly bring a powerful machine to its knees if a larger number of users connect.

Implementing Your ISAPI App

All of the classes I've talked about here are part of MFC 4.1 and declared in afxisapi.h, which you must include in your application source. You can use the Internet Extension Wizard to kick out a new project that implements minimal server and/or filter overrides. This new project type allows you to quickly and easily create ISAPI projects with MFC without the tedium of getting the project hooked up yourself. The first screen of the wizard (see Figure 12) asks for the essential information about your project—it allows you to specify whether or not you want a filter object or a server extension object, or both. By putting both a filter and a server in your DLL, you can save a few bytes of overhead on your server machine. It's particularly convenient if the extension uses filter functionality, or vice versa.

Figure 12 ISAPI Extension Wizard Sample One

If you request a filter, the wizard displays a second page (see Figure 13) that allows you to hook up overrides for the various notification functions automatically. This step is as straightforward as can be—just pick the notifications you'd like to receive and the wizard hooks up the appropriate flags in GetServerVersion and adds the corresponding overrides to the generated header file and implementation file!

Figure 13 ISAPI Extension Wizard Sample Two

The beauty of the MFC classes, though, lies in their flexibility. You can write a slim filter or server application module that doesn't use the MFC classes because the ISAPI classes live in a separate, statically-linkable library. On the other hand, if you're going to be pounding on databases or using OLE objects, you may want to use the support provided in the other MFC classes. You can bake the MFC ISAPI library into a module with the rest of the MFC code and write the coolest extension on the block in less time than it takes to download the Microsoft home page.

If C++ isn't your bailiwick, you can always use the ISAPI SDK directly. ISAPI is a public standard and is available in lots of places. Even if you're not using the Microsoft server, you can still use ISAPI and the MFC classes that support it, as long as your server platform is targeted by Visual C++.

End of Session

Even if you're not a Web-surfer type dude, someone in your organization can undoubtedly benefit from the information shared by a Web server extension. Forms provide a great way to collect information, and using a server extension to tuck the information away inside a database is a great way to store it. You can use other extensions to get that same information back out to clients who request it through the Web. By embracing Windows NT for your server platform, you'll have a scalable place for your server to grow. And by using MFC, you'll be able to grow quickly. Before you know it, the Web can become the most convenient way for your organization to share data—both internally and externally. As services offered by Web servers become more and more mature (as the result of enhancements to HTTP, for example), you'll find that applications you write for the Internet, or for your own organization's intranet, will be more robust and powerful.

What's New in Microsoft(r) Visual C++(r)4.1

Microsoft Visual C++ version 4.1 is a subscription update to Visual C++ version 4.0. The focus of Visual C++ 4.0 was delivering features that supported faster development through improved code reuse. In Visual C++ 4.1, these same features—custom AppWizards, OLE controls, and new MFC encapsulation—are used to deliver new Internet-oriented functionality that you can incorporate into new and existing applications rapidly.

The release includes new MFC and Wizard support for Microsoft's Internet Server API (ISAPI). Five new MFC classes are included for creating interactive Web applications using Microsoft Internet Information Server (see Figure A). These classes let you create dynamic Web-based applications by adding functionality to Internet servers and Web pages.

Figure A MFC Classes for ISAPI

New MFC Class

Description

CHttpServer

Creates an Internet server extension

CHttpFilter

Creates an Internet server filter to screen messages sent to and from an Internet server

CHttpServerContext

CHttpServer uses this to handle concurrent multiple requests

CHttpFilterContext

CHttpFilter uses this to handle concurrent multiple requests

CHtmlStream

Used by CHttpServer to send an HTML stream back to the client


The ISAPI Extension Wizard makes it easy to create Internet server extensions and filters. Built using the custom AppWizard technology, the ISAPI Extension Wizard is available via the New Project Workspace dialog box.

Microsoft has teamed up with Template Graphics Software to support VRML with a custom AppWizard, an MFC extension, and an OLE control. Using these components you can create Internet applications capable of real-time, three-dimensional walk-throughs on the Web.

In addition, developers can access their favorite Web sites from within Developer Studio. Using the Web Favorites command on the Help menu, you can go to one of the preloaded Microsoft sites for Visual C++ developer support or to one of your own custom sites.

Several of the leading OLE control vendors have partnered with the Visual C++ team to deliver fully functional, fully redistributable controls as part of the Visual C++ 4.1 release. These OLE controls offer a wide range of reusable functionality that helps simplify application development (see Figure B).

Figure B Sampling of Third-party Controls that Ship with Visual C++

OLE Control

Company

Description

Drag-it/OCX

Kelro Software

Lets your users draw a model of their data using symbols you provide on a customer toolbox.

InterAct

ProtoView Development Corp.

InterAct is a versatile programming object that allows users to create a wide variety of diagrams.

LEADTOOLS OCX

LEAD Technologies Inc.

Comprehensive support for raster images with 150+ functions in seven imaging categories.

Mh3dGauge

MicroHelp, Inc.

Creates user-defined gauges with a choice of linear (filled) or needle styles.

MList2

Desaware, Inc.

List box control with support for bitmaps, icons, multiple colors, and more.

Non-Rectangle Arrow Button

ASP Corp.

Provides a three-dimensional arrow and buttons of other shapes.

PICS Date Edit

ProtoView Development Corp.

The PICS Date Edit control allows date values to be entered and formatted. It provides a drop down calendar to choose dates from and a selection of display styles.

Sax Basic Engine

Sax Software Corp.

Allows the addition of macro language support to an application.

SmartHelp

Blue Sky Software

Simply drag the Help button into an application's dialog boxes and screens and visually select any Help file.

VideoPlay/OCX

Media Architects, Inc.

An easy way to integrate AVI, Quicktime, or MPEG video playback without sacrificing critical functionality.

Visual 3Space Browser Control

Template Graphics Software

A control that lets you integrate 3D and VRML into your apps.

Visual Voice for TAPI Solo

Stylus Innovation, Inc.

Supports the building of telephony applications such as touch-tone data access and voice mail.

VSFLEX

VideoSoft

FlexArray is similar to a spreadsheet, but allows total flexibility to manage tables containing strings and pictures. FlexString enables addition of regular-expression text matching into an application.


The project management and build systems have been tuned to manage large projects better. Enhancements include improved efficiency in project load time, settings management, debugger start time, and overall responsiveness.

Visual C++ 4.1 also includes the Game Software Development Kit. This SDK provides the tools necessary to develop high-performance games for Windows 95. Visual C++ 4.1 also includes new support in the compiler backend and debugger to take advantage of the recently announced Intel MMX architecture.

Finally, Visual C++ 4.1 includes several new Tech Notes and many new MFC samples covering a wide range of topics, from real-time database access over the Web (WWWQUOTE) to creating an OLE document object (BINDSCRB) (see Figure C).

Figure C New MFC Sample Programs

ACDUAL

Shows how to add dual-interface support to an MFC-based OLE Automation server

BINDSCRB

Shows how to write an OLE document object for use with the Binder tool in Microsoft Office 95 applications

DAOTABLE

Shows how to use MFC Data Access Object classes to create databases, tables, queries, fields, and indexes

DLGTEMPL

Shows how to dynamically create and use a dialog template

HTTPSVR

Shows how to use MFC and the Windows Sockets classes to implement a simple HTTP server

MFCUCASE

Shows how to use MFC classes to create Internet filter DLLs

ODBCINFO

Shows how to determine ODBC driver capabilities at run time by opening a selected ODBC data source and displaying information using property pages

ROWLIST

Shows how to implement full-row selection in the report mode of the CListView control class

WWWQUOTE

Shows how to write a World Wide Web application for retrieving information (in this case, stock quotes)


This article is reproduced from Microsoft Systems Journal. Copyright © 1995 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.