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 -fPIC

You 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:

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