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.

However, you should note that ISAPI modules are loaded directly into the server and run as part of the server process, this does have security and reliability implications. 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 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.

The aim of this document is to supplement that information with a gentle introduction with ISAPI along with information on how to integrate ISAPI applications into the Zeus Server.

COMPILING

ISAPI extensions and filters are compiled to a binary shared library which are loaded directly into the web server process. For this reason ISAPIs must be compiled to be position independent shared libraries. Using gcc, this be done using a command line similar to: gcc faff.c -o faff.api -shared -fPIC For other compilers, see your man page.

EXTENSIONS

There are two types of ISAPI modules. Extensions provide a mechanism for producing active page content. Here is how to create a simple extension that acts as a simple page counter. An ISAPI module is a dynamically loaded library, that exports two functions. The first of these 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_MAJOR;
   strncpy(pVer->lpszExtensionDesc, "A simple page counter", 
        HSE_MAX_EXT_DLL_NAME_LEN);
   return TRUE;
}
The module also exports a function called HttpExtensionProc that is called when the extension is requested by the web server. Here is a simple HttpExtensionProc function that increments a counter and returns it.

int hits=0;

DWORD WINAPI
HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK ecb )
{
   char *header   =  "Content-Type: text/html";
   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 0;	
}
The file then needs to be compiled as a shared library, and places into a directory that is marked 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 ISAPI module and the application.

FILTERS

The other type of ISAPI module is filters. Where as extensions are responsible for generating content, filters allow you to alter the behaviour of the server making it possible to implement your own logging, encryption, authentication or path mapping systems.

The server contains several hooks at different stages of processing a request. An ISAPI filter, can request to be notified at each of these points, where it will be passed information about the current request, and it can alter connection settings or perform operations through callback functions. The following ISAPI hooks exist:

Example 1 - HTTP Cookies

Most web browsers support the notion of cookies. The server gives the browser a cookie, which the browser will send back on each subsequent request. This is useful in monitoring the number of different people who have accessed your web site. This is a better figure than the number of unique hostnames as all the people in a particular organisation might all go through one proxy or gateway and appear to come from the same host.

As with extensions, there is a GetFilterVersion function that is called when the module is loaded.

#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 */
   pVer->dwFlags = SF_NOTIFY_PREPROC_HEADERS;
   return TRUE;
}

The actual function that gets notified on the ISAPI hook should be called HttpFilterProc. In this cookie example, we get the url and cookie from the processed headers. If there is a cookie set we log it, along with the url to a log file. If no cookie is set, we generate a random cookie, and issue it to the client.
void
RandomBytes( char *buffer, int count )
{
   /* fill buffer with count random digits and terminate */
   int i=0;
   for( i=0; i<count; i++ ) buffer[i] = '0' + (rand() % 10);
   buffer[i]=0;
}


DWORD WINAPI
HttpFilterProc( PHTTP_FILTER_CONTEXT pfc,
                DWORD notificationType,
	        PHTTP_FILTER_PREPROC_HEADERS headers )
{
   char cookie[256],     url[256];
   int  cookielen = 256, urllen=256;

   /* get header information */
   headers->GetHeader( pfc, "Cookie:", cookie, &cookielen );
   headers->GetHeader( pfc, "url:", url, &urllen );
	  
   if( cookielen > 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. This time asking for notification on the authentication hook.

#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_AUTHENTICATION;
   return 1;
}
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.

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,
                PHTTP_FILTER_AUTHENT auth )
{
   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 specified a request header "Accept-Language:" which specifies the languages that the client (or the human reading the page) understands. The specification allows multiple languages along with a preference for 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
Which 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 use the SF_NOTIFY_URL_MAP hook.

#include "httpfilt.h"
#include 
#include 

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_URL_MAP;
   return 1;
}

Now the HttpFilterProc will receive a PHTTP_FILTER_URL_MAP structure as its third argument.
DWORD WINAPI HttpFilterProc( PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, PHTTP_FILTER_URL_MAP map ) { /* 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, "HTTP_ACCEPT_LANGUAGE"); if( lang ) { char *p; lang = lang+22; /* skip "Accept Language: " */ for(p=lang; isalpha(*p); p++); *p=0; /* terminate after first language */ { /* now look for the file with that language in the name */ struct stat st; char *nfile = pfc->AllocMem(pfc, 256, 0); p = strchr( map->pszPhysicalPath, '.' ); if( p ) { /* build up the new filename */ int c = p - map->pszPhysicalPath + 1; memcpy( nfile, map->pszPhysicalPath, c); memcpy( nfile + c, lang, strlen( lang ) ); c += strlen( lang ); memcpy( nfile + c, p, strlen( p ) + 1 ); /* if it exists, use it! */ if( !stat( nfile, &st ) ) map->pszPhysicalPath=nfile; } } } return SF_STATUS_REQ_NEXT_NOTIFICATION; }

IMPORTANT NOTES