Internet Server Application Programming Interface
INTRODUCTION
ISAPI provides a vendor-independent way of extending the functionality of your web server. It offers far more flexibility than the CGI interface and avoids all the performance limitations.The ISAPI specification was designed by Microsoft and Process Software, and is quickly becoming the accepted standard for web server add-ons. Microsoft publishes both an overview and a complete ISAPI reference document at http://msdn.microsoft.com/. Another useful site is The ISAPI developers site. Details on the interface may also be found using the Unix man program with $ZEUSHOME/web/man in your $MANPATH environment variable: see man isapi for the overview and list of further pages.
The Zeus Web Server can run ISAPI extensions and filters either in a separate dedicated process or by loading them directly into the web server. The latter, `in-process ISAPI', is more efficient but does have security and reliability implications; the former, `out-of-process ISAPI', exposes your web server to less risks at a slight cost in performance. Even `out of process', errors in the code of an ISAPI extension or filter do expose all requests handled by the ISAPI runner to any problems experienced by any one request. For this reason, ISAPI should be seen as a platform to build high-performance applications with, and not entirely as a replacement for other alternatives such as CGI.
The aim of this document is to provide a gentle introduction to ISAPI along with information on how to integrate ISAPI applications into the Zeus Web Server. For greater detail, see the man pages and the web sites mentioned above. We'll deal first with the details of compiling an ISAPI application, then move on to describe the two kinds of ISAPI application, extensions and filters: as a rough guide, extensions generate content, filters modify it.
COMPILING
An ISAPI extension or filter is compiled to a binary shared library which is loaded into the ISAPI runner process or the web server itself. For this reason ISAPIs must be compiled to be position-independent shared libraries. Using gcc, this is done using a command line similar to:
gcc isapicode.c -I$ZEUSHOME/web/include/ -o isapicode.so -shared -fPICYou should replace the value of $ZEUSHOME with the filesystem location where you have installed the Zeus Web Server. (Some compilers may have different options, please see your vendor's manual page. We have documented the flags for some common compilers.)
EXTENSIONS
An ISAPI extension provides active content on a web site. We'll work through an example which provides a simple `hit counter' page. The functionality of an extension is defined by two required interface functions, GetExtensionVersion and HttpExtensionProc, optionally suplemented with a `finalisation' function, TerminateExtension. Our example, like most ISAPI extensions, will only provide the first two.
The first interface function, GetExtensionVersion, is called when the module is loaded to check the version numbers and get information about the module.
/* Import the ISAPI constants and type definitions */ #include "httpext.h" BOOL WINAPI GetExtensionVersion( HSE_VERSION_INFO *pVer ) { pVer->dwExtensionVersion = HSE_VERSION; strncpy( pVer->lpszExtensionDesc, "A simple page counter", HSE_MAX_EXT_DLL_NAME_LEN ); return TRUE; }The module also exports a function called HttpExtensionProc which is called to serve each request. Here is a simple HttpExtensionProc function that increments a counter and emits a response containing it.static int hits = 0; DWORD WINAPI HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK ecb ) { char *header = "Content-Type: text/plain"; int headerlen = strlen( header ); char msg[256]; int msglen; /* use a server support function to write out a header with our additional header information */ ecb->ServerSupportFunction( ecb->ConnID, HSE_REQ_SEND_RESPONSE_HEADER, 0, &headerlen, (DWORD *)header ); /* write out the number of accesses */ sprintf( msg, "This page has been accessed %d times", ++hits ); msglen = strlen( msg ); ecb->WriteClient( ecb->ConnID, msg, &msglen, 0 ); /* return, indicating success */ return HSE_STATUS_SUCCESS; }The file then needs to be compiled as a shared library, and placed into a directory - say /isapi/ for this example - that the Zeus Web Server is configured to interpret as an ISAPI alias directory. If the file was installed as /isapi/counter.api then the server will load in the module, and then run the HttpExtensionProc which will send the output to the browser.
This example is quite trivial, but it demonstrates the basic elements of the interface between the web server and an ISAPI extension.
FILTERS
The other type of ISAPI module is the filter. Whereas extensions are responsible for generating content, filters allow you to alter the behavior of the server (more-or-less arbitrarily!). Filters make it possible, for instance, to implement your own logging, encryption, authentication or path-mapping systems.
The process of serving a request is broken into a series of stages: a filter can ask to be `notified' as a request reaches each of these stages, intervening at that point to modify the request and the web server's response. For each notification, the filter receives information pertinent to the relevant stage of processing and is provided with the means to perform actions appropriate to that stage of processing. Some notifications arise repeatedly for typical requests; others only arise for a minority of requests; others happen just once for each request. The ISAPI notification are:
- Read Raw Data
This notification happens whenever data is read from the client. The ISAPI filter can alter the raw data that has been read. This makes it possible to implement your own encryption schemes, or to monitor the amount of data being sent.- Send Raw Data
This is the complement of the Read Raw Data notification: it happens just before the server writes any data to the client. Again the filter can modify the data, for similar purposes.- Preprocessed Headers
This notification happens after the server has processed the client headers into a table. The filter can modify the headers in the table, or log information such as the name of the browser software.- Authentication
This notification should be used for implementing your own authentication schemes.- Access Denied
This notification happens when a client has been refused access to a resource. This can be used to log intruder attempts or to provide help and advice which bona fide clients might find useful when seeking a restricted-access resource.- On URL Map
This notification happens after the server has mapped the requested URL onto the physical path. You can implement your own translation scheme by altering the physical path name at this point.- Logging
This notification happens when the server is about to log the request. It allows you to implement customized logging. An example use of this might be inserting all log information into a database.- End of network session
This notification happens at the end of of a network session before a the TCP connection to the client is closed. This enables a filter to free any resources it allocated for a connection.Example 1 - HTTP Cookies
Most web browsers support cookies. The server gives the browser a cookie, which the browser will send back on each subsequent request. This has many uses, for example, monitoring the number of different people who have accessed your web site or tracking how individual visitors use the site. This works better than schemes based on client host names, as many clients may come via a single proxy or gateway and appear to come from the same host.
Just as an extension provides an initialisation routine, a filter provides a function called GetFilterVersion by which it tells the web server (or separate ISAPI runner process) about itself: the function is called when the filter is loaded, before it handles any notifications. Indeed, the main purpose of GetFilterVersion is to enable the filter to tell the web server which notifications it is to receive.
#include "httpfilt.h" #include <unistd.h> #include <fcntl.h> #include <string.h> BOOL WINAPI GetFilterVersion( HTTP_FILTER_VERSION *pVer ) { pVer->dwFilterVersion = HTTP_FILTER_REVISION; strncpy( pVer->lpszFilterDesc, "A Cookie Filter", SF_MAX_FILTER_DESC_LEN ); /* Ask to be notified when the headers have been processed, regardless of the security status of the request. */ pVer->dwFlags = SF_NOTIFY_SECURE_PORT | SF_NOTIFY_NONSECURE_PORT | SF_NOTIFY_PREPROC_HEADERS; return TRUE; }Again, analogously with extensions, the actual processing a filter performs (on the various notifications it has requested) is provided by a function called HttpFilterProc. In this cookie example, we get the cookie and URL from the processed headers. When a cookie is present, we record the URL and cookie in a log file: otherwise, we generate a random cookie and issue it to the client.
static void RandomBytes( char *buffer, int count ) { /* fill buffer with count random digits and terminate */ int i; for( i=0; i<count; i++ ) buffer[i] = '0' + (rand() % 10); buffer[i] = '\0'; } DWORD WINAPI HttpFilterProc( PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, VOID *pvNotification ) { /* assert: notificationType is SF_NOTIFY_PREPROC_HEADERS */ HTTP_FILTER_PREPROC_HEADERS *headers = (HTTP_FILTER_PREPROC_HEADERS *) pvNotification; char cookie[256], url[256]; int cookielen = 256, urllen=256; /* get header information */ if( headers->GetHeader( pfc, "Cookie:", cookie, &cookielen ) && headers->GetHeader( pfc, "url", url, &urllen ) && cookielen > 1 && urllen > 1 ) { /* we have a cookie, log it */ int fd = open( "/tmp/cookie.log", O_WRONLY|O_CREAT|O_APPEND, 0755 ); if( fd != -1 ) { char outbuff[514]; sprintf( outbuff, "%s %s\n", cookie, url ); write( fd, outbuff, strlen( outbuff ) ); close( fd ); } } else { /* Set a cookie header with a random cookie */ char msg[256]; RandomBytes( cookie, 16 ); sprintf( msg, "Set-Cookie: %s\r\n", cookie ); pfc->AddResponseHeaders( pfc, msg, 0 ); } /* return, instructing the server to notify the next module */ return SF_STATUS_REQ_NEXT_NOTIFICATION; }Example 2 - Authentication
In this example, we will control access to our web site by a password. To keep the example simple, we will hard code a user name of "fred" and a password of "bloggs". It would be reasonably simple to extend the example to query a remote database.
First we need a GetFilterVersion function, now asking for notification on authentication:
#include <string.h> #include "httpfilt.h" BOOL WINAPI GetFilterVersion( HTTP_FILTER_VERSION *pVer ) { pVer->dwFilterVersion = HTTP_FILTER_REVISION; strncpy( pVer->lpszFilterDesc, "Basic auth filter", SF_MAX_FILTER_DESC_LEN ); pVer->dwFlags = SF_NOTIFY_SECURE_PORT | SF_NOTIFY_NONSECURE_PORT | SF_NOTIFY_AUTHENTICATION; return TRUE; }Now we need to write an HttpFilterProc to do the work. A Denied() function is given that actually sends the necessary HTTP headers back to the client on a failure along with an explanation as to why access was denied. On sending back a 401 response, the browser should pop up a dialog box asking the user to log in.
static void Denied( PHTTP_FILTER_CONTEXT pfc, char *msg ) { int l = strlen( msg ); pfc->ServerSupportFunction( pfc, SF_REQ_SEND_RESPONSE_HEADER, (PVOID) "401 Permission Denied", (LPDWORD) "WWW-Authenticate: Basic realm=\"foo\"\r\n", 0 ); pfc->WriteClient( pfc, msg, &l, 0 ); } DWORD WINAPI HttpFilterProc( PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, VOID *pvNotification ) { /* assert: notificationType is SF_NOTIFY_AUTHENTICATION */ HTTP_FILTER_AUTHENT *auth = (HTTP_FILTER_AUTHENT *)pvNotification; if( auth->pszUser[0] == 0) { Denied( pfc, "No user/password given" ); return SF_STATUS_REQ_FINISHED; } if( strcmp( auth->pszUser, "fred" ) ) { Denied( pfc, "Unknown user" ); return SF_STATUS_REQ_FINISHED; } if( strcmp( auth->pszPassword,"bloggs") ) { Denied( pfc, "Wrong Password" ); return SF_STATUS_REQ_FINISHED; } return SF_STATUS_REQ_NEXT_NOTIFICATION; }Example 3 - Multi-Lingual Support
The Internet is international. So wouldn't it be cool if, when a French speaking person looked at your web site, they would automatically see the French Language version of the page (assuming you've already translated it yourself). The HTTP/1.1 specification defined a request header "Accept-Language:" which states the languages that the client (or the human reading the page) understands; multiple languages are allowed, along with a preference associated with each one. A person who speaks English and a bit of French might have their browser configured to send:
Accept Language: en=0.9, fr=0.4, de=0.1 This tells the server: English if possible, if not French, or as a last resort German. Here is a sample ISAPI application that, when asked for (for example) fred.html, will try and load fred.fr.html if the favourite language is French. For simplicity, we will cheat slightly, and only take the first language in the Accept Language referenced header. As always we need to write a GetFilterVersion function. This time we'll request the notification on URL-mapping:
#include "httpfilt.h" #include <string.h> #include <sys/stat.h> BOOL WINAPI GetFilterVersion( HTTP_FILTER_VERSION *pVer ) { pVer->dwFilterVersion = HTTP_FILTER_REVISION; strncpy( pVer->lpszFilterDesc, "Language Negotiation Filter", SF_MAX_FILTER_DESC_LEN ); pVer->dwFlags = SF_NOTIFY_SECURE_PORT | SF_NOTIFY_NONSECURE_PORT | SF_NOTIFY_URL_MAP; return TRUE; }The pvNotification provided as third argument to HttpFilterProc, on the URL-mapping notification, points to an HTTP_FILTER_URL_MAP structure, which provides information about the URL and path to which it was mapped.
static const char AcceptHeader[] = "HTTP_ACCEPT_LANGUAGE"; #define PREAMBLE_LENGTH (sizeof(AcceptHeader) -1) /* length of the header */ DWORD WINAPI HttpFilterProc( PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, VOID *pvNotification ) { HTTP_FILTER_URL_MAP *map = (HTTP_FILTER_URL_MAP *) pvNotification; /* Get "Accept-Language" header using ALL_HTTP */ char *lang; char buffer[4096]; int size = sizeof(buffer); pfc->GetServerVariable(pfc, "ALL_HTTP", buffer, &size); /* rip out the language here */ lang = strstr(buffer, AcceptHeader); if( lang ) { char *p; lang += PREAMBLE_LENGTH; /* skip "Accept Language: " */ for (p = lang; isalpha(*p); p++) /* skip letters */ ; *p = '\0'; /* terminate after first language */ { /* now look for the file with that language in the name */ char *nfile = pfc->AllocMem( pfc, 1 + strlen( lang ) + strlen( map->pszPhysicalPath ), 0 ); p = strchr( map->pszPhysicalPath, '.' ); if( p ) { /* build up the new filename */ int c = p - map->pszPhysicalPath + 1; struct stat st; memcpy( nfile, map->pszPhysicalPath, c); strcpy( nfile + c, lang ); c += strlen( lang ); strcpy( nfile + c, p ); /* if it exists, use it! */ if( !stat( nfile, &st ) ) map->pszPhysicalPath = nfile; } } } return SF_STATUS_REQ_NEXT_NOTIFICATION; }IMPORTANT NOTES
- Multi-threaded environment
When using ISAPI modules with Zeus Server you must remember that the web server or ISAPI runner process may use several threads of execution: typically, Zeus uses one thread per processor provided by your hardware. This means that there could be several threads in your ISAPI application at any one time, so you should be wary of using static variables without some kind of mutex.
- Security considerations
When running in-process, ISAPI applications are part of the server core. This means that if they go wrong, they can take down the entire server. More seriously, due to the way the Zeus Server works, it is possible for a malicious ISAPI module to set the effective user id back to the user that started the server (possibly root). Do not use untrusted code in ISAPI applications.- Overwriting ISAPI modules on disk
When an ISAPI module is "in use" the file is mapped into the address space of the web-server process. If you overwrite or delete the file then you can corrupt the address space of the web server process causing it to crash. Filters are constantly mapped in. Extensions will get mapped in when they are first used, and may be left mapped in, cached in memory after they have been used. Be careful!- Memory allocation
If your ISAPI module needs to allocate memory, you should use the ISAPI AllocMem call. This will allocate memory in a memory pool specific to the current connection that will be free()d automatically when the connection is closed. This call should be used in preference to malloc() or new().