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.
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:
- The client sends a request to the server via an HTTP GET or
POST method.
- The connection persists while the server forks a CGI process
to handle the request.
- That CGI process builds output and passes it back to the server.
- 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.
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.
|
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.
|
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
Variable | Definition
|
cbSize | Size of this structure
|
dwVersion | Version number of this application
|
ConnID | Reserved; server assigned number
|
dwHttpStatusCode | HTTP status code
|
lpszLogData | Data that will be written to the IIS HTTP log
|
lpszMethod | HTTP request method (GET, POST, etc.)
|
lpszQueryString | Query string portion of a GET request
|
lpszPathInfo | Additional path info, if supplied in the URL request
|
lpszPathTranslated | Physical path to the requested file
|
cbTotalBytes | Amount of data returned to the client
|
cbAvailable | Total amount of data available
|
lpbData | Data returned to the client
|
lpszContentType | MIME type
|
Table 7.2 Extension Control Block Functions
Function | Definition
|
GetServerVariable | Retrieves the value of an HTTP environment variable. See Table 7.7 for a listing of available variables.
|
WriteClient | Writes data to the client.
|
ReadClient | Reads data from the body of the client request (that is, for Method=POST forms).
|
ServerSupportFunction | The 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.
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
Variable | Definition
|
cbSize | Size of this structure
|
Revision | Revision level of structure
|
ServerContext | Reserved; used by the server
|
ulReserved | Reserved; used by the server
|
fIsSecurePort | TRUE if the request is over a secure port
|
pFilterContext | A pointer to this filter context
|
Table 7.4 HTTP_FILTER_CONTEXT Functions
Function | Definition
|
GetServerVariable | Retrieves the value of an HTTP environment variable. Table 7.7 lists available variables.
|
Add Response Headers | Sends an HTTP header to the client.
|
WriteClient | Writes data to the client.
|
AllocMem | Used to allocate a block of memory for use by the filter.
|
ServerSupportFunction | The 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.
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.
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
Header | Definition
|
Entity-Header | Can contain meta-information about the body of request or return data.
|
Content-Length | If the client request is from a POST method form, this is the length of the data sent in the request body.
|
Content-Type | Identifies the MIME type of the data, such as text/html, application/octet-stream.
|
Content-Encoding | Identifies any additional encoding applied to the Content-Type.
|
Expires | A possible value for Entity-Header.
|
Last-Modified | Modification date of the file.
|
extension-header | User-defined headers.
|
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 Variable | Definition
|
CONTENT_LENGTH | The length of data buffer sent by the client.
|
CONTENT_TYPE | The MIME type of the incoming or outgoing data, such as text/html, image/gif, etc.
|
GATEWAY_INTERFACE | The server CGI type and revision level, in the format CGI/revision.
|
PATH_INFO | Set by a URL containing an extra trailing slash (/) character. PATH_INFO is the string following the extra slash.
|
PATH_TRANSLATED | The server translates the virtual path represented by PATH_INFO into a physical path on the server file system.
|
QUERY_STRING | This variable is filled by data, following a question mark (?) character, in a URL.
|
REMOTE_ADDR | The IP address of the client.
|
REMOTE_HOST | The client hostname. If the server cannot determine this, it still has access to the REMOTE_ADDR variable and leaves this unset.
|
REQUEST_METHOD | This can be a GET, a POST, or a HEAD.
|
SCRIPT_NAME | A virtual path to the script being executed.
|
SERVER_NAME | The server's hostname, DNS alias, or IP address.
|
SERVER_PORT | The port number that received the client request; port 80 is standard.
|
SERVER_PROTOCOL | The protocol being used by the client request; in the format protocol/revision; for example, HTTP/1.0 or HTTP/1.1.
|
SERVER_SOFTWARE | The name and version of the server software, in the format name/version.
|
AUTH_TYPE | If request is authenticated, this indicates the type, such as basic, NTLM, etc.
|
REMOTE_USER | If the server supports user authentication, and an object on the server is protected, this is the authenticated username.
|
AUTH_PASS | If a successful authentication occurs, this contains the password.
|
HTTP_ACCEPT | Indicates the MIME types the client browser supports or can accept.
|
ALL_HTTP | Includes 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.
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 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 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 )
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.
|
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.
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 Declaration | Data Type
|
ITS_EMPTY | No arguments |
ITS_PSTR | String |
ITS_I2 | Short |
ITS_I4 | Long |
ITS_R4 | Float |
ITS_R8 | Double |
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=~")
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.
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.
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.)
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.
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.
- 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.
|
- Open the workspace file for the DLL being debugged. Select
Build | Settings | Debug from the menu.
- For the "Executable for debug sessions" line, add
the following:
[your pathname]\inetinfo.exe
- For Program arguments, enter the following:
-e W3Svc
and click on OK.
- 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."
- 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.
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
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.
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.
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(<ime); ltime = ltime+(6*100000);
gmt = gmtime(<ime);
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 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;
}
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 (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>
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.
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/
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.