Web servers need the capability to expand their horizons. They occupy a unique niche in technology, allowing people to view information you want them to see. As such, each situation is unique, and it would be near impossible to pick any one set of functions or methods and say, "This is it. This is how everyone will deal with information." That would be like making cars in only one color. People might try to sell you on the idea that generic is good, but individuality (on either a personal or a corporate level) needs to find a way to express itself.
CGI programs are great at what they do-gather information, process it, and generate output. But they also have disadvantages, such as needing to create a new instance of themselves every time someone runs the script. If five people are using the function, there are five copies of that process in memory. If your site gets thousands of hits, and most of them are going to be starting CGI processes you get the picture. Lots of wasted memory space and processing time, all to do some simple (or maybe not so simple) functions.
The Internet Server API (ISAPI) is a different method of dealing with informational functions. It applies high-level programming to give you the most efficient combination of power and flexibility, including information that is almost impossible to get through CGI. By developing a program in a certain manner, and by meeting certain requirements, you gain access to this hidden world of additional power, information, and speed. Be warned, however, that prog-ramming ISAPI functions is not something to be approached lightly, or by the faint of programming-heart. It can be a jungle out there.
This chapter does not assume much background in the realm of C or C++ programming. The primary focus here is not the actual writing of ISAPI code, but understanding the concepts behind it. Among these concepts are
With these concepts firmly in hand, and some examples of code sneaked in here and there, you'll have a place to start planning your own functions.
When you're working with a Web server, it is useful to gain more information and be able to deal with it faster. You want ways of getting at details that normal CGI can't give you, as well as ways to modify those bits of information. You want a method of doing it faster and more efficiently, so that the only lag time that exists is the user sorting through the cool stuff you can do for them in an almost instantaneous manner. You want an API, and you want it now.
An Application Programming Interface (API) exists so that you can do fun things with it. You gain access to the inner workings of the program itself, giving you more freedom and power to do things with that information. In the case of a Web server, there are all sorts of hidden things you might want to get hold of, such as user authorization information, the ability to manipulate how errors are handled, and how information is logged on the system. In addition, APIs are faster than normal CGI programs during execution, and take up less resources while running. This means more power, more users and fewer problems.
Note |
In theory, API functions are supposed to be faster, and thus better in general for use. Some people even say that CGI will become obsolete. Later in the chapter we'll cover some of the problems that API functions can present later on in this chapter during "Implementation Complications," and you'll get a sense of why API programming isn't for everyone, no matter what benefits it may have. |
To take advantage of all this freedom and power, Microsoft and Process Software teamed up to create an API standard for their Web servers (and for anyone who wants to adopt it). The aptly-named Internet Server API (ISAPI) is a whole collection of functions that allow you to extend the capability of your web server in a nearly unlimited number of ways. There are actually two very distinct components present in the ISAPI standard that will be discussed separately in the next section, "ISAPI Background and Functionality." I will later discuss ISAPI as a whole in the "Implementation Complications" section later in the chapter.
The ISAPI standard is a very recent, but natural, invention. Microsoft has long been providing Windows developers with access to Windows' inner workings through the Windows Software Development Kit (SDK), while Process software has been providing people with Web servers. When Microsoft began development of its new Internet Information Server (IIS), it was expected that they would allow developers the opportunity to get down and dirty with IIS' functionality: they didn't disappoint anyone with the release of the ISAPI.
The two branches of the ISAPI, Internet Server Applications (ISAs) and ISAPI Filters, comprise two different schools of thought on how programmers can approach additional functionality. ISAs are the more traditional of the two, leading programmers to develop something that's more of an external component with special links back into the server's workings. ISAPI Filters are closer to building blocks, which can be attached directly to the server, providing a seamless component that carefully monitors the HTTP requests being directed at the server. Since each has its own particular way of being dealt with by the server, I'll look at them as separate entities, and tie together the common points where they conveniently overlap.
Internet Server Applications (ISAs), which can also be called ISAPI DLLs, are the first step in extending a server's functionality. Much like a traditional CGI program, an ISA might find itself referenced in a form entry like the following:
<form method=POST action=/scripts/function.dll>
Note |
See Chapter 8, "Forms and How to Handle Them," for more details on CGI used with form elements. |
An ISA performs the same task of gathering the POSTed form data, parsing it out, and doing something with it, but there the similarities stop. Although the surface elements look exactly the same, what occurs once the form in question gets submitted (or whatever other action triggers the ISA to execute) is a completely different matter. Figure 25.1 shows the typical path of processes in ISAPI and CGI requests.
Figure 25.1 : Request processes for ISAPI in CGI.
Figure 25.1 shows an example of how communication works between various entities in the land of the server. Requests are routed to the main HTTP server. When the server receives instructions to start a typical CGI program, it needs to make a separate process for that request. It sends the data out to the CGI program through the environment variables and Standard Input (STDIN). The CGI program, in turn, processes that information from the environment variables and STDIN, then sends output (normally through Standard Output (STDOUT)) back to the server, which responds to the request. This action takes place far from home so there's going to be some delay. In addition, there's some information that the server can't export past its own boundaries.
Requests that go to an ISA, on the other hand, stay within the boundaries of the server's process territory. The data is handled using Extension Control Blocks (ECBs). There's much less work involved in getting the data to the ISAs. Also, because it's closer to home, it also allows for more detailed exchanges of information, even changes to the server based on that information. There's a lot more going on than might meet the eye.
What happens when an ISA function is called? There are a number of internal steps:
When the server receives a request to start the ISA, one of the first things it does is check to see if the ISA is already in the memory. This is called Run-Time Dynamic Linking. While the program is running, it hooks up with other components that it needs and recognizes that it already has them onboard when other requests come in for those components' functions. These components are commonly referred to as Dynamic Linked Libraries, or DLLs. Just as the name might imply, DLLs are libraries of functions that an application can dynamically link to and use during its normal execution. Anyone who uses the Windows operating system, in any version, has encountered DLLs before-Windows is a whole compilation of mutually cooperative DLL functions. Each function can call out to another to do whatever needs to be done. When the server needs to load the DLL, it calls into a special entry point that defines an ISAPI function, as opposed to some other DLL that might not be safe to use.
The primary entry point that the server looks for in an ISA is the GetExtensionVersion() function. If the server calls out to that function, and nobody answers, it knows that it's not a usable function. Therefore, the attempt to load the DLL into memory will fail. If, on the other hand, the function is there, then it will let the server know what version of the API it conforms to. Microsoft's recommended implementation of a GetExtensionVersion() definition is
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *version )
{
version->dwExtensionVersion = MAKELONG(HSE_VERSION_MAJOR, HSE_VERSION_MINOR);
lstrcpyn( version->lpszExtensionDesc, "This is a sample Extension",
HSE_MAX_EXT_DLL_NAME_LEN);
return TRUE;
}
The GetExtensionVersion() entry point is really just a way for the server to ensure that the DLL is conforming to the specification that the server itself conforms to. It could be that the server or function is too old (or too new), and so they wouldn't work well together. It's also possible that future changes will need to know past versions to accommodate for special changes, or use it for some other compatibility purpose.
The actual startup of the function occurs at the HttpExtensionProc() entry point. Similar to the main() function declaration in a standard C program, it accepts data inside an Extension Control Block. This block is made available to the function to figure out what to do with the incoming data before composing a response. Here is the declaration for the HttpExtensionProc() entry point:
DWORD WINAPI HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK *lpEcb);
Whatever happens, you can't keep the client waiting; you have to tell them something. In addition, it has to be something that the server understands and can properly deal with. To properly create a response, the ISA can call on either the ServerSupportFunction() or the WriteClient() function (These functions are defined and explained in "Callback Functions."). Within that response, it will want to return one of the valid return values shown in Table 25.1.
Return Value | Meaning |
HSE_STATUS_SUccESS | The ISA successfully completed its task, and the server can disconnect and clean up. |
HSE_STATUS_SUccESS_ AND_KEEP_CONN | The ISA successfully completed its task, but the server shouldn't disconnect just yet, if it supports persistent connections. The application hopes it will wait for another HTTP request. |
HSE_STATUS_PENDING | The ISA is still working and will let the server know when it's done by sending an HSE_REQ_DONE_WITH_SESSION message through the ServerSupportFunction call. |
HSE_STATUS_ERROR | Whoops, something has gone wrong in the ISA. The server should end the connection and free up space. |
All of these extension processes have to interact with something
to get their data, and, as shown before, there's a group of intermediaries
called Extension Control Blocks (ECBs) that handle that particular
duty. They're nothing more than a C structure that is designed
to hold specific blocks of data and allow a few functions to make
use of that data. Here is the setup of an ECB:
Listing 25.1. Extension Control Block structure.
// To be passed to extension procedure on a new request
//
typedef struct _EXTENSION_CONTROL_BLOCK {
DWORD cbSize; //Size of this struct
DWORD dwVersion; //Version info for this spec
HCONN ConnID; //Connection Handle/ContextNumber(don't modify!)
DWORD dwHttpStatusCode; //Http Status code for request
chAR lpszLogData[HSE_LOG_BUFFER_LEN];
//Log info for this specific request (null terminated)
LPSTR lpszMethod; // REQUEST_METHOD
LPSTR lpszQueryString; // QUERY_STRING
LPSTR lpszPathInfo; // PATH_INFO
LPSTR lpszPathTranslated; // PATH_TRANSLATED
DWORD cbTotalBytes; // Total Bytes from client
DWORD cbAvailable; // Available Bytes
LPBYTE lpbData; // Pointer to client Data (cbAvailable bytes worth)
LPSTR lpszContentType; // Client Data Content Type
BOOL ( WINAPI * GetServerVariable)
( HCONN ConnID,
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 ConnID,
DWORD dwHSERRequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType);
}
Table 25.2 goes into detail about each individual component of an Extension Control Block.
Field | Comments | |
cbSize | The size of the structure itself (shouldn't be changed). | |
dwVersion | Version information of this specification. The form for this information is HIWORD for major version number, and LOWORD for minor version number. | |
ConnID | A connection handle uniquely assigned by the server (DO NOT chANGE). | |
dwHttpStatusCode | The status of the current transaction once completed. | |
lpszLogData | Contains a null-terminated string for log information of the size (HSE_LOG_BUFFER_LEN). | |
lpszMethod | Equivalent of the environment variable REQUEST_METHOD. | |
lpszQueryString | Equivalent of the environment variable QUERY_STRING. | |
lpszPathInfo | Equivalent of the environment variable PATH_INFO. | |
lpszPathTranslated | Equivalent of the environment variable PATH_TRANSLATED. | |
cbTotalBytes | Equivalent of the environment variable CONTENT_LENGTH. | |
cbAvailable | Available number of bytes (out of cbTotalBytes) in the lpbData buffer. See the explanation of the Data Buffer that follows this table. | |
lpbData | Pointer to a buffer, of size cbAvailable, which holds the client data. | |
lpszContentType | Equivalent of the environment variable CONTENT_TYPE. |
Note |
For items that are referred to as "Equivalent of the environment variable...," a more detailed explanation of the particular environment variables can be found in this chapter in Table 25.6, "Variable Names and Purposes," and also in Chapter 3, "Crash Course in CGI." |
Sometimes there's a lot of information sent to a program, and sometimes there's not. By default, lpbData will hold a 48K chunk of data from the client. If cbTotalBytes (the number of bytes sent by the client) is equal to cbAvailable, then the program is telling you that all the information that was sent is available within the lpbData buffer. If cbTotalBytes is greater than cbAvailableBytes, lpbData is only holding part of the client's data, and you'll have to ferret out the rest of the data with the ReadClient() callback function.
An additional possibility is that cbTotalBytes has a value of 0xFFFFFFFF. This means that there's at least four gigabytes of client data sitting out there, waiting to be read. What are the chances that you're going to receive that much data? If you do expect it, you'll be happy to know that you can keep calling ReadClient() until you get everything that's there. It's a good thing API functions are fast.
Throughout some of these other functions, there have been references to callback functions. These are the nice little hooks that let you get information that the server supplies you with. The functions are listed in Table 25.3, along with a brief description of each. They are GetServerVariable(), ReadClient(), WriteClient() and ServerSupportFunction().
Function | Purpose |
GetServerVariable | Retrieves connection information or server details |
ReadClient | Reads data from the client's HTTP request |
WriteClient | Sends data back to the client |
ServerSupportFunction | Provides access to general and server-specific functions |
GetServerVariable is used to return information that the server has in regards to a specific HTTP connection, as well as server-specific information. This is done by specifying the name of the server variable that contains the data in lpszVariableName. On the way to the server, the Size indicator (lpdwSize) specifies how much space it has available in its buffer (lpvBuffer). On the way back, the Size indicator (lpdwSize) is set to the amount of bytes now contained in that buffer. If the Size (lpdwSize), going in, is larger than the number of bytes left for reading, the Size indicator (lpdwSize) will be set to the number of bytes that were placed in the buffer (lpvBuffer). Otherwise, the number should be the same going in and coming out. Here are the details on how the GetServerVariable() function is defined:
BOOL WINAPI GetServerVariable (
HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize);
Tables 25.4 and 25.5 show the accepted parameters and possible error returns for GetServerVariable(), respectively.
Parameter | Direction | Purpose |
hConn | IN | Connection handle (if request pertains to a connection), otherwise any non-NULL |
lpszVariableName | IN | Name of the variable being requested (See Table 25.6 for a list) |
lpvBuffer | OUT | Pointer to buffer that will receive the requested information |
lpdwSize | IN/OUT | Indicates the size of the lpvBuffer buffer on execution; on completion is set to the resultant number of bytes transferred into lpvBuffer |
Error Code | Meaning |
ERROR_INVALID_INDEX | Bad or unsupported variable identifier |
ERROR_INVALID_PARAMETER | Bad connection handle |
ERROR_INSUFFICIENT_BUFFER | More data than what is allowed for lpvBuffer; necessary size now set in lpdwSize |
ERROR_MORE_DATA | More data than what is allowed for lpvBuffer, but total size of data is unknown |
ERROR_NO_DATA | The requested data isn't available |
Use of the GetServerVariable() callback function requires knowing exactly what variables are available, and why you might want to get them. A brief description of each of these variables can be found in Table 25.6 "Variable Names and Purposes," but a more detailed explanation can be found in Chapter 3, as they are also commonly encountered as CGI environment variables.
Name | Purpose | |
AUTH_TYPE | Type of authentication in use; normally either none or basic | |
CONTENT_LENGTH | Number of bytes contained in STDIN from a POST request | |
CONTENT_TYPE | Type of Content contained in STDIN | |
GATEWAY_INTERFACE | Revision of CGI spec that the server complies to | |
PATH_INFO | Additional path information, if any, which comes before the QUERY_STRING but after the script name | |
PATH_TRANSLATED | PATH_INFO with any virtual path names expanded | |
QUERY_STRING | Information following the ? in the URL | |
REMOTE_ADDR | IP address of the client (or client gateway/proxy) | |
REMOTE_HOST | Hostname of client (or client gateway/proxy) | |
REMOTE_USER | Username, if any, supplied by the client and authenticated by the server | |
REQUEST_METHOD | The HTTP request method, normally either GET or POST | |
UNMappeD_REMOTE_USER | Username before any ISAPI filter mapped the user to an account name (the mapped name appears in REMOTE_USER) | |
SCRIPT_NAME | Name of the ISAPI DLL being executed | |
SERVER_NAME | Name or IP address of server | |
SERVER_PORT | TCP/IP port that received the request | |
SERVER_PORT_SECURE | If the request is handled by a secure port, the string value will be one. Otherwise it will be zero. | |
SERVER_PROTOCOL | Name and version of information retrieval protocol (usually HTTP/1.0) | |
SERVER_SOFTWARE | Name and version of the Web server running the ISAPI DLL | |
ALL_HTTP | All HTTP headers that are not placed in a previous variable. The format for these is http_<header field name>, and are contained within a null-terminated string, each separated by a line feed. | |
HTTP_AccEPT | Semi-colon (;) concatenated list of all AccEPT statements from the client | |
URL | Provides the base portion of the URL (Version 2.0 only) |
ReadClient does what you'd expect it to-it keeps reading data from the body of the client's HTTP request and placing it into a storage buffer. Just as GetServerVariable() and the other callback functions do, it uses the Buffer Size Indicator (lpdwSize) as an indicator to show how big the buffer (lpvBuffer) initially was, and how big it is after it's finished. ReadClient has only two possible return values, and no specific associated error codes; however, if the return of ReadClient() is True, but lpdwSize is 0, the socket closes prematurely. The following code shows how ReadClient() would be defined:
BOOL ReadClient (
HCONN hConn,
LPVOID lpvBuffer,
LPDWORD lpdwSize);
Table 25.7 details the expected parameters of the ReadClient() function.
Parameter | Purpose | |
hConn | Connection Handle (cannot be NULL) | |
lpvBuffer | Pointer to buffer for receiving client data | |
lpdwSize | Size of available buffer/number of bytes placed in buffer |
WriteClient writes information back to the client from information stored in the buffer. The Buffer Size indicator (lpdwSize), in this case, functions as a record of how many bytes are supposed to be written to the client from the buffer, and how many were. Since this might be used for binary data, it does not assume that the data will be in the form of a null-terminated string like the ServerSupportFunction does. A sample definition of the WriteClient function follows.
BOOL WriteClient(
HCONN hConn,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
DWORD dwReserved);
Table 25.8 shows what parameters are accepted for the WriteClient callback function. (An additional reserved parameter is set aside for changes in the function's behavior that might be implemented in the future.)
Parameter | Purpose | |
hConn | Connection Handle (cannot be NULL) | |
lpvBuffer | Pointer to data being written to client | |
lpdwSize | Number of bytes being sent; number of bytes actually sent | |
dwReserved | Unspecified-reserved for future use |
The final callback function, ServerSupportFunction, is one of the most powerful. It sends a Service Request Code to the server itself, which is a value that the server translates into a request to execute an internal function. An example of such a function would be redirecting the client's browser to a new URL, something that the server knows how to do without any help. This allows some standard operations to be performed, but it also gives server manufacturers a method for allowing developers to have easy access to a specialized internal function. Be it a built-in search routine, an update of user databases, or anything else, this function can call anything the server will allow. The actual list of what each server will allow varies, of course, but the definitions for the individual functions have a fixed order. Any service request code with a value of 1000 or less is a reserved value, used for mandatory ServerSupportFunction codes and defined in the HttpExt.h file. Anything with a Service Request Code of 1001 or greater is a general purpose server function, and should be able to be found in the servers own *Ext.H file. For example, Process Software's Purveyor maintains a Prvr_Ext.h file listing some additional supported functions, which have been included in Table 25.11. The ServerSupportFunction definition follows.
BOOL ServerSupportFunction (
HCONN hConn,
DWORD dwHSERequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType);
Tables 25.9 and 25.10 list the acceptable parameters and the standard defined values for service request codes for the ServerSupportFunction, respectively.
Parameter | Purpose | |
hconn | Connection Handle (cannot be null) | |
dwHSERequest | Service Request code (See Table 25.10 for default values) | |
lpvBuffer | Buffer for optional status string or other information passing | |
lpdwSize | Size of optional status string when sent; bytes of status string sent, including NULL term | |
lpdwDataType | Optional null-terminated string with headers or data to be appended and sent with the header generated by the service request (If NULL, header is terminated by \r\n) |
Service Request | Action |
HSE_REQ_SEND_URL_REDIRECT_RESP | Sends a URL Redirect (302) message to the client. The buffer (lpvBuffer) should contain the null-terminated URL, which does not need to be resident on the server. No further processing is needed after this call. |
HSE_REQ_SEND_URL | Sends the data to the client specified by the null-terminated buffer (lpvBuffer), as if the client had requested that URL. The URL cannot specify any protocol information (for example, it must be /document.htm instead of http://server.com/document.htm). |
HSE_REQ_SEND_RESPONSE_HEADER | Sends a complete HTTP server response header, which includes the status code, server version, message time, and MIME version. The DataType buffer (lpdwDataType) should contain additional headers such as content type and length, along with a CRLF (Carriage Return/Line Feed (\r\n)) combination and any data. It will read text data only, and it will stop at the first \0 termination. |
HSE_REQ_MAP_URL_TO_PATH | The buffer (lpvBuffer) points to the logical path to the URL on entry, and it returns with the physical path. The Size buffer contains the number of bytes being sent in, and is adjusted to the number of bytes sent back on return. |
HSE_REQ_DONE_WITH_SESSION | If the server has previously been sent an HSE_STATUS_PENDING response, this request informs the server that the session is no longer needed, and it can feel free to clean up the previous session and its structures. All parameters are ignored in this case, except for the connection handle (hConn). |
Service Request | Action |
HSE_GET_COUNTER_FOR_GET_METHOD | Accepts the SERVER_NAME system variable in the Buffer (lpvBuffer), the length of SERVER_NAME in the Size buffer (lpdwSize), and returns the total number of GET requests served since initiation of the server, storing them in the DataType buffer (lpdwDataType) |
HSE_GET_COUNTER_FOR_POST_METHOD | Except DataType (lpdwDataType), will hold the number of POST requests since startup of the server |
HSE_GET_COUNTER_FOR_HEAD_METHOD | Except DataType(lpdwDataType), will hold the number of HEAD requests since startup of the server |
HSE_GET_COUNTER_FOR_ALL_METHODS | Except DataType (lpdwDataType), will hold the total number of all requests since server startup |
Note |
Currently, there aren't many extra defined server functions in the public arena. But, given the rate of server expansion, and Microsoft's race to expand it's Internet Information Server, it wouldn't be surprising to see a large number of very useful functions show up in the near future. |
As you've seen, an ISA is much like a traditional program that has a couple of added advantages, such as being able to get at the server program's insides, and taking advantage of things that might otherwise require either lots of file reading, or not be able to be accomplished at all. Next, however, you're going to take a look at something that's one step beyond that-adding onto the server functionality itself, to make it do whatever tricks you want it to.
ISAPI filters are quite different from a traditional CGI program. If ISAPI DLLs make a server more flexible, ISAPI filters turn a server into a true contortionist, able to flex whatever way they need to. They're not just resident with the server, they're part of the server itself, having been loaded into memory and the server's configuration ahead of time. They're direct extensions of the server, allowing them to do tasks that no CGI program could think of doing, such as enhancing the local logging of file transfers, building in pre-defined methods of handling forms or searches, and even doing customized local authentication for requests. You're making the server evolve into something more powerful, instead of adding pieces that the server can call for help.
When you create an ISAPI filter, you're creating a linked DLL that's being examined every time the server processes an HTTP request. The goal is to filter out specific notifications that you're interested in, and remove the duties of handling that particular process from the core functionality of the server. You're essentially saying, "Oh, don't worry about that when you see it; that's what this filter is for." This scalability allows you to take a basic server and customize it to meet whatever needs you might have. You're adding scalability to the server so that it meets your needs, even if the original manufacturer didn't anticipate them.
Like an ISAPI DLL, an ISAPI filter has two entry points that must be present in order to verify that the function meets the current specification, and that it has a place to receive information from the server. Unlike an ISAPI DLL, though, an ISAPI filter isn't something that's spur-of-the-moment in its use-any filters you define have to be entered in the system registry so that they are literally part of the server's configuration. Since they're intercepting things as they happen, the server needs to know about them. In this case, it's convinced that it always had the ability to do these functions, it just never used them before.
The first entry point to define in the ISAPI filter is the one that tells the server that it does in fact correspond to the correct specification version, so it should be able to run without difficulty. This is the GetFilterVersion function, which takes only one argument-a structure that will hold the data that the server uses to find out the version, the description, and the events that the filter wants to process. Here's how GetFilterVersion would normally be defined:
BOOL WINAPI GetFilterVersion(PHTTP_FILTER_VERSION pVer);
This points to a data structure of the HTTP_FILTER_VERSION type, which contains all the little niceties that the server is looking for. Since pointing to a structure that you don't know anything about isn't necessarily a good idea, look at what's inside an HTTP_FILTER_VERSION structure to understand what data the server really needs. A typical definition of an HTTP_FILTER_VERSION structure follows:
typedef struct _HTTP_FILTER_VERSION
{
DWORD dwServerFilterVersion;
DWORD dwFilterVersion;
chAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1];
DWORD dwFlags;
} HTTP_FILTER_VERSION, *PHTTP_FILTER_VERSION;
Table 25.12 points out the details of what those structure components are.
Parameter | Purpose | |
dwServerFilterVersion | Version of specification used by the server (Currently server-defined to HTTP_FILTER_revISION) | |
dwFilterVersion | Version of the specification used by the filter (Currently, server defined to be HTTP_FILTER_revISION) | |
lpszFilterDesc | A string containing a short description of the function | |
dwFlags | Combined list of notification flags (SW_NOTIFY_*) that inform the server what kind of events this filter is interested in knowing about; see Table 25.15 for the complete list of SW_NOTIFY_* flags available |
Note |
dwFlags is a parameter to be careful with-if you don't specify what you need, the filter won't do you any good. If you specify everything, your filter will drag down the server's performance and possibly cause other problems. Be picky with what you place inside. |
Once the GetFilterVersion information has been transferred, it's time to call the main filter process itself-HttpFilterProc(). Just like the main() function in a C program, or the HttpExtensionProc used by an ISAPI DLL, HttpFilterProc() serves as the gateway to all that your filter is designed to do. Here's how the HttpFilterProc() would normally be defined:
DWORD WINAPI HttpFilterProc(
PHTTP_FILTER_CONTEXT pfc,
DWORD notificationType,
LPVOID pvNotification);
Table 25.13 explains the HttpFilterProc() function parameters.
Parameter | Purpose |
pfc | Pointer to a data structure of the HTTP_FILTER_CONTEXT type, which contains context information about the HTTP request. |
notificationType | The type of notification being processed, as defined in the list of notification flags (SW_NOTIFY_*). See Table 25.15 for a complete list of SW_NOTIFY_* flags available. |
pvNotification | The data structure pointed to as a result of the type of notification. See Table 25.15 (SW_NOTIFY_* flags) for the relationship between notification types and specific structures. |
Based on the event, and what was done by the custom processes within the filter, HttpFilterProc() can yield a variety of different return codes, from "All Set," to "Keep Going," to "Whoops." Table 25.14 lists the accepted return codes and their explanations.
Return Code | Meaning |
SF_STATUS_REQ_FINISHED | Successfully handled the HTTP request, and the server can now disconnect |
SF_STATUS_REQ_FINISHED_KEEP_CONN | Successfully handled the HTTP request, but the server shouldn't necessarily disconnect |
SF_STATUS_REQ_NEXT_NOTIFICATION | The next filter in the notification order should be called |
SF_STATUS_REQ_HANDLED_NOTIFICATION | Successfully handled the HTTP request, and no other filters should be called for this notification type |
SF_STATUS_REQ_ERROR | An error happened and should be evaluated |
SF_STATUS_REQ_READ_NEXT | (Used for raw data only) Session parameters are being negotiated by the filter |
One thing you might have noticed from Table 25.14 (Acceptable Returns) is that one of the possible returns is for the server to call the next filter in the notification order. You can have a whole sequence of filters all looking for the same event to occur, and all standing in a line waiting to do something with the data. So, the first function might be an authorization log, while the next might be a document conversion, and the last would be some other custom logging function. As long as one of the earlier functions doesn't return a code saying "OK, shut it all down," then everyone else who's waiting for data will get their turn at it.
Since they have already been mentioned several times, now is probably a good point to examine the Notification flags (SW_NOTIFY_*). These serve a combination of purposes: they let the server know what kind of specific events are being looked for, and they can also specify that the filter should be loaded at a certain priority level. Depending on the type of notification, there are specific data structures that these functions map to that hold the additional information the server might need upon receiving the specific notification. Table 25.15 shows the notification flags and their descriptions, as well as what specific data structures each notification corresponds to, where appropriate.
SF_NOTIFY_ORDER_DEFAULT | (GFV only) Load the filter at the default priority (this is the recommended priority level) |
SF_NOTIFY_ORDER_LOW | (GFV only) Load the filter at a low priority |
SF_NOTIFY_ORDER_MEDIUM | (GFV only) Load the filter at a medium priority |
SF_NOTIFY_ORDER_HIGH | (GFV only) Load the filter at a high priority |
SF_NOTIFY_SECURE_PORT | Looking for sessions over secured ports |
SF_NOTIFY_NONSECURE_PORT | Looking for sessions over nonsecured ports |
SF_NOTIFY_READ_RAW_DATA | Let the filter see the raw data coming in; returned data consists of both data and headers Structure: HTTP_FILTER_RAW_DATA |
SF_NOTIFY_PREPROC_HEADERS | Looking for instances where the server has processed the headers |
Structure: HTTP_FILTER_PREPROC_HEADERS | |
SF_NOTIFY_AUTHENTICATION | Looking for instances where the Client is being authenticated by the server |
Structure: HTTP_FILTER_AUTHENTICATION | |
SF_NOTIFY_URL_MAP | Looking for instances where the server is mapping a logical URL to a physical path |
Structure: HTTP_FILTER_URL_MAP | |
SF_NOTIFY_SEND_RAW_DATA | Let the filter know when the server is sending back raw data to the client |
Structure: HTTP_FILTER_RAW_DATA | |
SF_NOTIFY_LOG | Looking for instances where the server's saving information to its log |
Structure: HTTP_FILTER_LOG | |
SF_NOTIFY_END_OF_NET_SESSION | Looking for the termination of the client session |
SF_NOTIFY_AccESS_DENIED | (New for version 2) Looking for any instance where the server is about to send back an Access Denied (401) status message. Intercepts this instance to allow for special feedback |
Structure: HTTP_FILTER_AccESS_DENIED |
As you can see from Table 25.15, there are a lot of possibilities for the filter to want to do something. There are also lots of pointers to Data Structures.
The two components of the ISAPI standard, one being Applications
and the other Filters, make sense in a number of ways. CGI is
a wide open field, with dozens of tools and languages available
to help people create whatever they need. Since what's normally
needed is more functionality, ISAs take CGI to that next step
by providing the direct link between the server's internal functions
and an external program. Filters allow the programmers to modify
the heart of the server program itself, and make it react differently
for their unique situation. The power
behind these methods is obvious, the pain of implementation tends
to come out later.
The point that tends to detract from all of the wonders of an API are the difficulties in creating an API-based function. You might have to use specific tools, or be limited to specific platforms once you build what you want. You need to know what you're doing because a casual programmer is not going to write a very efficient or safe API function. Why? Because, unlike traditional CGI, where a new instance of the CGI program is run for each user, an API function is a shared resource. This is why it doesn't take up as much space in memory-there's only one of them at a time.
To imagine just one of the possible myriad of problems that could cause an unprepared program, compare it to a small company with one phone. The phone rings occasionally, the receptionist picks it up and does what's necessary, then hangs up. If the phone calls don't come in too fast, there's no problem. As soon as the pace starts picking up, however, there had better be a system in place to deal with multiple callers, or the "one person at a time" syndrome is going to grind the company into the ground.
How does that relate to an API-based function? Well, when you design a traditional CGI program, it follows a very linear path. It accepts data, and then goes down a one-way street. It calls subroutines and other organized functions, but it's only dealing with one user, or one stream of data.
The biggest caveat about an ISAPI function, be it a filter or a DLL, is that it has to be multithread safe. A multithreaded environment is one where lots of things are running at once, requests come in bunches, and every function has to be careful not to step on another function's toes while they use a file or take data from a block of memory. Coding something to be multithreaded can be a challenge, especially for more complex functions. If you're an advanced C programmer, then you've probably already dealt with this issue before, and you've been anticipating it ever since you started reading this chapter. If you're a novice programmer, or even an intermediate programmer, you'll want to delve deeper into the many resources out there that cover multithreaded DLL programming. Microsoft maintains a great deal of these resources on their Web site (www.microsoft.com), and they're also available on their Development Library, and in general programming references.
Caution |
If you write an ISAPI function that's not multithread safe (or safe in general), you're risking a lot more than just some errant data. As mentioned in the beginning of this chapter, ISAPI functions are resident within the same process space as the server itself. That's right-they all live together in harmony. If one crashes and burns, they other is toast as well. The likelihood of a commercial server product like IIS or Purveyor going ker-blooey isn't very high, due to the amount of testing performed on them, so in most cases the weak link can be your ISAPI function. Don't make your server suffer-test early, test often, and get help to do it if necessary. |
There is hope for developers in that there are a number of ways to create an ISAPI function, including one that might not be thought of at first glance-Visual Basic. Microsoft has announced that upcoming releases of Visual Basic will support a number of functions, including the ability to create shared DLLs. Talk about making your life easier
Like any programming endeavor, you're going to hit the debugging phase. ISAPI functions could place your server in danger if not well-tested. Therefore, chances are pretty good you'll be looking at those functions in debug mode for quite a while. The problems that Web servers present in debugging some of these components, however, can cause a bit of frustration.
To cover the standard CGI debugging methods using an ISAPI DLL, you'll want to review Chapter 6, "Testing and Debugging." This covers some of the methods available, but it's important to note that one of the focuses of debugging a regular CGI application is setting the Environment Variables to control the data. Since the ISAPI DLLs use Extension Control Blocks, Environment Variables are no longer in the picture. You can't just set them with a shell script or regular executable. In fact, you really can't set them at all. But wait, there's more.
Web servers on the NT platform normally run as a background service-they start up when the system boots, and go on their merry little way behind the scenes. Unless something goes horribly wrong, they just sit there and process, and things magically work. Unfortunately, to be behind the scenes means that there's no window that they toss messages and other output out of. Therefore, you don't get nice visual recognition of things going on as they process. Before you get nervous about that, though, look at what can be done to help your debugging efforts.
When most servers go ahead and load your ISAPI DLL, they're going
to cache it when they're through. If you're making changes to
your DLL, and wondering why in the world they're not showing up,
this can certainly be a cause of it. By editing the registry,
you can disable caching for the DLLs, making sure the current
copy is loaded each time. This makes the execution of those functions
slower, so make sure you turn it back on once you're done. Here's
how you can disable caching under Microsoft's IIS:
Disabling ISAPI DLL caching with Microsoft's IIS
To re-enable caching, go through the same process, but set CacheExtensions to one. Note that this only applies to ISAPI DLL's with Microsoft's Internet Information Server (IIS). Process Software's Purveyor 1.2 and previous versions do not have a similar option for cache manipulation, and ISAPI Filters can't be controlled in this manner. If you want to replace a filter, you're going to have to shut your server down, replace the filter, and start the server up again. Good thing servers don't (normally) take a long time to come up.
For real-time debugging, you'll probably first turn to the lowly message box. Although not the most elegant method in the world, it's simple. You place a message box within your code to show you data from the ECBs, or as the result of a function call. Then slowly wind your way through your program's execution. Remember, though, that your program is running as a service, so it doesn't have its own window to play with. What you'll want to take advantage of are the MB_SERVICE_NOTIFICATION and MB_TOPMOST flags for the message box, which are defined in the Win32 SDK. These are the flags that let those messages come through when the service has something horrible to report to you and needs its own place on your screen.
Tip |
Don't leave Message boxes in the final version of your program! They're only for debugging purposes. You're not supposed to have any bugs left when you're done, so why keep them around? If one does pop up, it's better to use the built-in logging functions to write it out to a file and continue on, rather than pop up a message box that no one might be around to see. |
Log files are your friends as well. Without a lot of preparation you can insert logging functions in either your ISAPI DLL or ISAPI Filter, and look at a nice printout of what happened. Standard File I/O will create the files so you don't have to rely on building an extended logging function within the ISAPI code set itself. Standard ReadFile and WriteFile will work just fine as long as you've got written permissions for the directory in question (the system root). Logging is great for verifying data coming in and going out as part of your testing process. If someone else is doing the testing for you it might be more suitable to have a written testing record generated this way, rather than relying on message boxes or the next method.
The next method is a slightly more involved method for debugging. You can use the OutputDebugString() function, which sends a string to your specified Debug monitor. If you don't have a Debug monitor, or don't know what one is, this might not be the best choice. If it sounds like fun, though, Microsoft includes a DBMON example with the Win32 SDK to show how this is generally done in code.
Tip |
According to Microsoft, early versions of the Win32 SDK (for instance, prior to the Win32 SDK for NT 4.0 Beta 2) need a code change in DBMON to account for user privileges. If you have one of these earlier versions, you'll want to take a look at the code change they specify in Knowledge Base Article Q152054, page 2, under "OutputDebugString()." This is available from Microsoft's Web site. |
Other methods of debugging ISAPI DLLs and Filters are constantly evolving, as more and more people work on making them. For some further details, check Microsoft's Online Knowledge Base during your development cycle, and check for any updates regarding helpful additions (or new problems) to the ISAPI debugging process. One of the current Knowledge Base articles on this topic is Q152054, which provides the details described above, as well as a few other options that you might like to pursue.
Currently, ISAPI is not extremely cross-platform capable. It will work with all flavors of NT, but only ISAPI DLLs can, at this point, go to any other platform. In this case, that additional platform is OpenVMS, made possible by Process Software's architecture. ISAPI filters are NT-only, and right now they're only to be found with Microsoft's Internet Information Server (IIS) package. If you're thinking about going to UNIX, you might want to talk to the folks at Microsoft-a recent document published by them on their Internet strategy says:
"In addition, Microsoft continues to work closely with its key UNIX partners (Software AG, Bristol, Spyglass, and Mainsoft) to deliver the server and client technologies for the various UNIX platforms."
Of course this doesn't say when, but, with the aggressive efforts Microsoft is making to take a big stake in the Internet market, you can bet that they're not going to sit idly by while other standards, like the Netscape Server API (NSAPI), hold dominance in Internet-intensive markets like UNIX.
Note |
See Chapter 26, "NSAPI," for details on the Netscape Server API (NSAPI) and its cross-platform capabilities. |
You can do almost anything with the ISAPI. The big questions to ask yourself are: Does my Server support ISAPI in either form? Do I have the programming experience to create an API function? Is this a function I could do with standard CGI programming and, if so, what are the real benefits I gain from doing it this way? In many cases you might discover that something limits you from using ISAPI for your needs, but as Web sites continue to evolve, some of the more sophisticated functions within servers will be accessible only to API calls. In addition, building value-added functions for established server packages is an area that has not yet begun to reach saturation of developers. The power and flexibility to expand and exceed what you're doing now with CGI exists. All you have to do is want to take advantage of it.