/*****************************************************************************/
/*
                                  CGIsapi.c

CGIplus-based ISAPI (Internet Server API; Microsoft/Process Software Purveyor
joint specification) extensions (not filters) scripting environment.

The CGIplus environment, due to it's ability to support persistant scripts, and
basic similarities between ISAPI and CGI processing, can quite conveniently
support an ISAPI scripting environment.  This program essentially becomes a
CGIplus wrapper around an ISAPI DLL (dynamic link library, or with VMS known as
a sharable image).

In this environment the ISAPI extension does NOT execute in the server process
space, it executes in a separate subprocess.  Unlike implementations where
ISAPI extensions are loaded into the server space the WASD approach effectively
insulates buggy ISAPI extensions from crashing the server.  The worst they can
do is crash the subprocess.

The down side is the separate process context is more expensive and slower than
the native loadable, but performance measurements indicate persistant CGISAPI
is still approximately four to five times faster than an equivalent CGI script! 
This makes development for the environment worthwhile for those resource-hungry
applications.  Like CGIplus it's persistant.  This means scripts that have high
initialization  overheads (such as opening large databases) have far less
latency on subsequent uses.  A distinct advantage is no multi-threading issues. 
There will only ever be the one thread being executed by CGISAPI!

CGISAPI DLLs must be executed via a CGIplus-enabled path.

CGISAPI successfully loads and executes the Process Software Corporation
Purveyor VMS example extension, SAMPLE_DLL_AXP.DLL and SAMPLE_DLL_VAX.DLL.


CONSIDERATIONS
--------------
This application is designed to be ISAPI 1.0 compliant.  It should also be
vanilla ISAPI 2.0 compliant (not the Microsoft WIN32 variety, so don't think
you'll necessarily be able to grab all those IIS extensions and just recompile
and use ;^)  For experimentation the compliance version check can be set to
allow any version DLL using the /VERSION= qualifier (major digit only).

With CGISAPI multiple instances of any one extension may be active on the one
server (each in an autonomous subprocess, unlike a server-process-space loaded
extension where only one would ever be active at any one time).  Be aware this
could present different concurrency issues than a single multi or single
threaded instance.

When CGIplus subprocesses are idle they can be sys$delprc()ed at any time by
the server at expiry of lifetime or to free up required server resources.  For
this reason ISAPI extensions (scripts) should finalize the processing of
transactions when finished, not leave anything in a state where it's unexpected
demise might corrupt resources or otherwise cause problems (which is fairly
good general advice anyway ;^)  That is, when finished tidy up as much as is
necessary.  Obviously do not exit completely or a complete reactivation will be
necessary (why would you be using something like ISAPI if you didn't know
that?)

CGISAPI is not thread-aware, it doesn't need to be due to the single context of
the subprocess it executes within.  As a consequence, HttpExtensionProc()
cannot be returned from before all processing is complete.  When this function
returns CGISAPI considers the extension is finished and ready to process
another request.  Returning HSE_STATUS_PENDING or using ServerSupportFunction()
with HSE_REQ_DONE_WITH_SESSION are reported as errors and CGISAPI exits.

When CGISAPI invokes the HttpExtensionProc() with the EXTENSION_CONTROL_BLOCK
initialized, any request body is immediately and completely available via the
'cbTotalBytes', 'cbAvailable' and 'lpbData' fields.  No subsequent calls to
ReadClient() are necessary (although can be made).

Any WASD CGI variable can be requested via GetServerVariable(), as well as
ISAPI-specific "ALL_HTTP" and the WASD-specific "*".  Note that some will not
be portable.  If a variable name does not exist '*lpdwSize' is set to zero,
'lpvBuffer' has a null character set at [0] and FALSE is returned.  If the
supplied size overflows 'lpvBuffer' contains a null-terminated string of as
much as would be contained, '*lpdwSize' is set to the required size (including
terminating null) and false is returned.  If found and no overflow '*lpdwSize'
is set to the size of the string (including null) and TRUE is returned.

Output is explicitly buffered for efficiency reasons.  For occasions where
small amounts of output need to be sent to the client periodically (perhaps as
status information during a more extended period of processing) use
WriteClient() with a zero-length 'lpdwBytes' parameter.  This will flush the
output buffer.  The size of this buffer may be explicitly set using the command
line qualifier /WBUFFER= or a WASD extension call of ServerSupportFunction()
with HSE_REQ_DLL_WRITE_BUFFER_SIZE and 'lpdwSize' pointing to a word containing
the required buffer size (although this should seldom, if ever, be necessary). 
Setting the buffer size to zero, *before any output*, disables the buffering,
resulting in immediate writes.

CGISAPI loaded extensions can exit at any time they wish.  The subprocess
context allows this.  Of course, normally a server-process-space loaded
instance would not be able to do so!

Any status code and log message set in EXTENSION_CONTROL_BLOCK at return from
HttpExtensionProc() are ignored.  Normal request logging is performed by the
server.


DEBUGGING
---------
The CGISAPI implementation of ISAPI includes a facility to assist with
debugging DLLs.  Basic information on function parameters is output to the
client whenever a call is made to an ISAPI function.  This debugging can be
toggled on and off whenever desired using a call to ServerSupportFunction()
with 'dwHSERRequest' parameter set to the WASD values of HSE_REQ_DLL_DEBUG_ON
or HSE_REQ_DLL_DEBUG_OFF.  Once enabled DLL debugging remains active through
multiple uses of a CGISAPI instance, or until disabled, or until the particular
CGISAPI subprocess' lifetime expires.


SERVER CONFIGURATION
--------------------
Ensure the following are in the appropriate sections of HTTPD$CONFIG.

  [DclScriptRunTime]
  .DLL  $CGI-BIN:[000000]CGISAPI.EXE

  [AddType]
  .DLL  application/octet-stream  -  ISAPI extension DLL

Ensure this is in the scripting section of HTTPD$MAP.

  exec+ /isapi/* /cgi-bin/*

DLLs may then be accessed with paths such as:

  http://host.name.domain/isapi/isapiexample.dll


ISAPI RESOURCES
---------------
The code in this application, data structures, etc., has been derived in part
from information contained in the QUE book "Using ISAPI", Stephen Genusa,
et.al., 1997.

  http://www.genusa.com/isapi/
  http://www.microsoft.com/win32dev/apitext/isalegal.htm
  http://www.process.com/news/spec.htm
  http://vms.process.com/

There's not a lot obvious anywhere for anything but Microsoft platforms, let
alone specifically for VMS :^(  Still, the efficiencies of having persistant
scripts, along with ISAPI being a well-documented interface (in contrast to
CGIplus, though which is simpler and more efficient), will provide real
benefits for some sites.


PARAMETERS
----------
The full file specification of the DLL must be supplied.


QUALIFIERS
----------
/CHARSET=        "Content-Type: text/html; charset=..."
/DBUG            turns on all "if (Debug)" statements
/VERSION=        set the ISAPI major version digit to allow non-1.0 DLLs
                 (this is for experimentation, it doesn't really change
                 anything, just stops the version check error report)
/WBUFFER=        integer, write buffer size in bytes, zero to disable


LOGICAL NAMES
-------------
CGISAPI$DBUG        turns on all "if (Debug)" statements
CGISAPI$DBUG_DLL    turns on DLL debug mode before an extension is loaded
CGISAPI$PARAM       equivalent to (overrides) the command line
                    parameters/qualifiers (define as a system-wide logical)


BUILD DETAILS
-------------
See BUILD_CGISAPI.COM procedure.


COPYRIGHT
---------
Copyright (C) 1999-2020 Mark G.Daniel
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.
http://www.gnu.org/licenses/gpl.txt


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
25-OCT-2009  MGD  V1.3.2, use CGI response to allow HTTP/1.1 functionalities
                          (e.g. GZIP response encoding, chunking, persistence)
23-DEC-2003  MGD  v1.3.1, minor conditional mods to support IA64
28-OCT-2000  MGD  v1.3.0, use CGILIB object module,
                          modifications for RELAXED_ANSI compilation
16-SEP-2000  MGD  v1.2.2, make DLL debug span whole requests only
12-APR-2000  MGD  v1.2.1, minor changes for CGILIB 1.4
20-NOV-1999  MGD  v1.2.0, write buffer to reduce script I/O,
                          use more of the CGILIB functionality
11-JUN-1999  MGD  v1.1.1, improve (DllDebug) output,
                          bugfix; "#define CGILIB_NONE NULL"
24-APR-1999  MGD  v1.1.0, use CGILIB.C
14-MAR-1999  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.3.2"
#define SOFTWARENM "CGISAPI"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* VMS-related header files */
#include <descrip.h>
#include <libdef.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header files */
#include "cgisapi.h"
#include <cgilib.h>

#define boolean int
#define true 1
#define false 0

#define FI_LI __FILE__, __LINE__

#ifndef __VAX
#   pragma nomember_alignment
#endif

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
#define WRITE_BUFFER_SIZE_DEFAULT  4096
#define WRITE_BUFFER_SIZE_MIN       256
#define WRITE_BUFFER_SIZE_MAX     32768

#define DEFAULT_CHARSET "ISO-8859-1"

#define CGISAPI_MAJOR_VERSION 1
#define CGISAPI_MINOR_VERSION 0

#define HTTP_VARIABLE_PREFIX "HTTP_"
#define HTTP_VARIABLE_PREFIX_LENGTH sizeof(HTTP_VARIABLE_PREFIX)-1

char  Utility [] = "CGISAPI";

boolean  AllowStandardCgi,
         Debug,
         DllDebug,
         DllDebugOn,
         IsCgiPlus;

int  DllActivatedCount,
     IsapiMajorVersion = CGISAPI_MAJOR_VERSION,
     WriteBufferSize = WRITE_BUFFER_SIZE_DEFAULT;

char  *CliCharsetPtr,
      *CliDllFileNamePtr;

char  SoftwareID [48];

/* required function prototypes */

int FindImageSymbolHandler ();

BOOL IsapiServerSupportFunction (HCONN, DWORD, LPVOID, LPDWORD, LPDWORD);
BOOL IsapiGetServerVariable (HCONN, LPSTR, LPVOID, LPDWORD);
BOOL IsapiReadClient (HCONN, LPVOID, LPDWORD);
BOOL IsapiWriteClient (HCONN, LPVOID, LPDWORD, DWORD);

/*****************************************************************************/
/*
*/

main ()

{
   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   if (getenv ("CGISAPI$DBUG") != NULL) Debug = true;
   if (getenv ("CGISAPI$DBUG_DLL") != NULL) DllDebug = true;
   /* only for testing certain behaviours! */
   if (getenv ("CGISAPI$CGI") != NULL) AllowStandardCgi = true;

   GetParameters ();

   if (CliDllFileNamePtr == NULL)
   {
      fprintf (stdout, "%%%s-E-DLL, not specified\n", Utility);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }

   CgiLibEnvironmentInit (0, NULL, false);
   CgiLibEnvironmentSetDebug (Debug);
   /* if a CGILIB CGI variable does not exist then return a NULL pointer */
   CgiLibEnvironmentSetVarNone (NULL);

   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by CGISAPI");

   IsCgiPlus = CgiLibEnvironmentIsCgiPlus ();

   if (!IsCgiPlus && !AllowStandardCgi)
   {
      /* only allows standard CGI environment when debug switched on */
      CgiLibResponseError (FI_LI, 0,
         "ISAPI scripts must be executed in CGIplus environment!");
      exit (SS$_NORMAL);
   }

   /***********/
   /* process */
   /***********/

   if (IsCgiPlus)
   {
      for (;;)
      {
         /* block waiting for next request */
         CgiLibVar ("");
         IsapiMain ();
         CgiLibCgiPlusEOF ();
      }
   }
   else
      IsapiMain ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
This function gets called each time the CGISAPI script gets activated.  The
first call it must load the DLL (shareable image), ensure the two required
entry-points are available and check the ISAPI version of the DLL.  Subsequent
calls it can just invoke the DLL procedure entry-point to execute it's
functionality.
*/

IsapiMain ()

{
   static BOOL (*GetExtensionVersion)(LPHSE_VERSION_INFO);
   static DWORD (*HttpExtensionProc)(LPEXTENSION_CONTROL_BLOCK);
   static $DESCRIPTOR (FileNameDsc, "");
   static $DESCRIPTOR (HttpExtensionProcDsc, "HttpExtensionProc");
   static $DESCRIPTOR (ImageNameDsc, "CGI-BIN:[000000].DLL");
   static $DESCRIPTOR (GetExtensionVersionDsc, "GetExtensionVersion");

   register char  *cptr, *sptr, *zptr;

   boolean  ok;
   int  status,
        MajorVersion,
        MinorVersion;
   char  FileName [256],
         ImageName [256];
   HSE_VERSION_INFO  HseVersion;
   EXTENSION_CONTROL_BLOCK  ExtensionControlBlock;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "IsapiMain() %d\n", DllActivatedCount);

   if (DllActivatedCount++)
   {
      /********************/
      /* subsequent calls */
      /********************/

      IsapiEcbInit (&ExtensionControlBlock);

      /* invoke the DLL main procedure entry-point */
      ok = (*HttpExtensionProc)(&ExtensionControlBlock);
      if (Debug) fprintf (stdout, "(*HttpExtensionProc)() ok:%d\n", ok);

      IsapiEnd (&ExtensionControlBlock, ok);

      return;
   }

   /**************/
   /* first call */
   /**************/

   cptr = CliDllFileNamePtr;

   zptr = (sptr = ImageName) + sizeof(ImageName)-1;
   while (*cptr && *cptr != ']' && sptr < zptr) *sptr++ = *cptr++;
   if (cptr[1] == '[')
   {
      if (sptr < zptr) *sptr++ = *cptr++;
      while (*cptr && *cptr != ']' && sptr < zptr) *sptr++ = *cptr++;
   }
   if (*cptr && sptr < zptr) *sptr++ = *cptr++;
   ImageNameDsc.dsc$a_pointer = sptr;

   zptr = (sptr = FileName) + sizeof(FileName)-1;
   while (*cptr && *cptr != '.' && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';

   FileNameDsc.dsc$a_pointer = FileName;
   FileNameDsc.dsc$w_length = strlen(FileName);
   if (Debug) fprintf (stdout, "|%s|\n", FileName);

   zptr = ImageName + sizeof(ImageName)-1;
   sptr = ImageNameDsc.dsc$a_pointer;
   while (*cptr && *cptr != ';' && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';

   ImageNameDsc.dsc$a_pointer = ImageName;
   ImageNameDsc.dsc$w_length = strlen(ImageName);
   if (Debug) fprintf (stdout, "|%s|\n", ImageName);

   lib$establish (FindImageSymbolHandler);

   status = lib$find_image_symbol (&FileNameDsc, &HttpExtensionProcDsc,
                                   &HttpExtensionProc, &ImageNameDsc);
   if (Debug) fprintf (stdout, "lib$find_image_symbol() %%X%08.08X\n", status);

   lib$revert ();

   if (VMSnok (status))
   {
      /* could not locate the required entry-point in the DLL */
      if (status == LIB$_KEYNOTFOU)
         CgiLibResponseError (FI_LI, 0, "DLL HttpExtensionProc() not found");
      else
         CgiLibResponseError (FI_LI, status, CliDllFileNamePtr);
      exit (SS$_NORMAL);
   }

   status = lib$find_image_symbol (&FileNameDsc, &GetExtensionVersionDsc,
                                   &GetExtensionVersion, &ImageNameDsc);
   if (Debug) fprintf (stdout, "lib$find_image_symbol() %%X%08.08X\n", status);

   if (VMSnok (status))
   {
      /* could not locate the required entry-point in the DLL */
      if (status == LIB$_KEYNOTFOU)
         CgiLibResponseError (FI_LI, 0, "DLL GetExtensionVersion() not found");
      else
         CgiLibResponseError (FI_LI, status, CliDllFileNamePtr);
      exit (SS$_NORMAL);
   }

   /* invoke the DLL version information entry-point */
   ok = (*GetExtensionVersion)(&HseVersion);
   if (Debug) fprintf (stdout, "(*GetExtensionVersion)() ok:%d\n", ok);

   if (ok)
   {
      /* check version compatibility */
      MajorVersion = HseVersion.dwExtensionVersion >> 16;
      MinorVersion = HseVersion.dwExtensionVersion & 0x0ffff;
      if (Debug)
         fprintf (stdout, "HseVersion: %d.%d |%s|\n",
                  MajorVersion, MinorVersion, HseVersion.lpszExtensionDesc);
      if (MajorVersion > IsapiMajorVersion)
      {
         char  String [256];
         sprintf (String, "DLL version is %d.%d, %s script is %d.%d",
                  MajorVersion, MinorVersion, Utility,
                  IsapiMajorVersion, CGISAPI_MINOR_VERSION);
         CgiLibResponseError (FI_LI, 0, String);
         exit (SS$_NORMAL);
      }
   }
   else
   {
      CgiLibResponseError (FI_LI, 0, "DLL GetExtensionVersion() failed");
      exit (SS$_NORMAL);
   }

   /********************************/
   /* call the extension procedure */
   /********************************/

   IsapiEcbInit (&ExtensionControlBlock);

   /* invoke the DLL main procedure entry-point */
   ok = (*HttpExtensionProc)(&ExtensionControlBlock);
   if (Debug) fprintf (stdout, "(*HttpExtensionProc)() ok:%d\n", ok);

   IsapiEnd (&ExtensionControlBlock, ok);
}

/*****************************************************************************/
/*
Just continue, to report an error if the image couldn't be activated or the
required symbol not found.
*/

FindImageSymbolHandler ()

{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "FindImageSymbolHandler()\n");

   return (SS$_CONTINUE);
}

/*****************************************************************************/
/*
Executed after the DLL's main procedure entry-point has returned.
*/

IsapiEnd
(
LPEXTENSION_CONTROL_BLOCK lpEcb,
DWORD HttpExtensionProcReturn
)
{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "IsapiEnd()\n");

   if (Debug)
      fprintf (stdout,
"dwHttpStatusCode: %d\n\
lpszLogData |%s|\n",
               lpEcb->dwHttpStatusCode,
               lpEcb->lpszLogData);

   if (DllDebug)
   {
      fprintf (stdout,
"[HttpExtensionProc() returned: %d\n\
 dwHttpStatusCode: %d\n\
 lpszLogData: |%s|]\n",
         HttpExtensionProcReturn,
         lpEcb->dwHttpStatusCode,
         lpEcb->lpszLogData);
   }

   if (lpEcb->dwHttpStatusCode == HSE_STATUS_PENDING)
   {
      /* we're not threaded here, so anything pending's not supported! */
      CgiLibResponseError (FI_LI, 0, "HSE_STATUS_PENDING not supported!");
      exit (SS$_NORMAL);
   }

   /* flush anything in the write buffer */
   IsapiWriteClient (lpEcb, NULL, 0, 0);
}

/*****************************************************************************/
/*
Initialize the extension control block, filling out the required fields.
*/

IsapiEcbInit (LPEXTENSION_CONTROL_BLOCK lpEcb)

{
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "IsapiEcbInit()\n");

   DllDebug = DllDebugOn;

   memset (lpEcb, 0, sizeof(EXTENSION_CONTROL_BLOCK));

   lpEcb->cbSize = sizeof(EXTENSION_CONTROL_BLOCK);
   lpEcb->dwVersion = CGISAPI_MAJOR_VERSION << 16 | CGISAPI_MINOR_VERSION;
   lpEcb->ConnID = lpEcb;

   if ((lpEcb->lpszMethod = CgiLibVar("REQUEST_METHOD")) == NULL)
      lpEcb->lpszMethod = "";
   if ((lpEcb->lpszQueryString = CgiLibVar("QUERY_STRING")) == NULL)
      lpEcb->lpszQueryString = "";
   if ((lpEcb->lpszPathInfo = CgiLibVar("PATH_INFO")) == NULL)
      lpEcb->lpszPathInfo = "";
   if ((lpEcb->lpszPathTranslated = CgiLibVar("PATH_TRANSLATED")) == NULL)
      lpEcb->lpszPathTranslated = "";
   if ((lpEcb->lpszContentType = CgiLibVar("CONTENT_TYPE")) == NULL)
      lpEcb->lpszContentType = "";

   /* initialize the request body procedure, indicated by a NULL 'lpdBuffer' */
   IsapiReadClient (lpEcb, NULL, NULL);

   lpEcb->GetServerVariable = &IsapiGetServerVariable;
   lpEcb->ReadClient = &IsapiReadClient;
   lpEcb->WriteClient = &IsapiWriteClient;
   lpEcb->ServerSupportFunction = &IsapiServerSupportFunction;

   if (DllDebug)
   {
      CgiLibResponseHeader (200, "text/plain");

      fprintf (stdout, "[%s %s activation: %d]\n",
               SoftwareID, CliDllFileNamePtr, DllActivatedCount);

      if ((cptr = CgiLibVar("*")) != NULL)
      {
         fprintf (stdout, "[%s", cptr);
         while ((cptr = CgiLibVar("*")) != NULL)
            fprintf (stdout, "\n %s", cptr);
         fputs ("]\n", stdout);
      }

      fprintf (stdout,
"[HttpExtensionProc()\n\
 ConnID: %d\n\
 lpszMethod: |%s|\n\
 lpszQueryString: |%s|\n\
 lpszPathInfo: |%s|\n\
 lpszPathTranslated: |%s|\n\
 lpszContentType: |%s|\n\
 cbTotalBytes: %d\n\
 cbAvailable: %d\n\
 lpbData: %d]\n",
         lpEcb->ConnID,
         lpEcb->lpszMethod,
         lpEcb->lpszQueryString,
         lpEcb->lpszPathInfo,
         lpEcb->lpszPathTranslated,
         lpEcb->lpszContentType,
         lpEcb->cbTotalBytes,
         lpEcb->cbAvailable,
         lpEcb->lpbData);
   }
}

/*****************************************************************************/
/*
DLL callback that will return the requested CGI-like variable value.  Returns
TRUE if the variable exists, FALSE if it doesn't (sets 'lpvBuffer' to an empty
string).  Returns FALSE if the buffer overflows (puts as much as possible into
it as a null-terminated stream, then sets 'lpdwSize' to what was actually
required).  Special case is "content-length", sets 'lpvBuffer' to a 4 byte
binary value.
*/

BOOL IsapiGetServerVariable
(
HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize
)
{
   register char  *cptr, *sptr, *zptr;

   int  len;
   char  *CgiLibVarPtr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "IsapiGetServerVariable() |%s|\n", lpszVariableName);

   if (DllDebug)
   {
      fprintf (stdout,
"[GetServerVariable()\n\
 hConn: %d\n\
 lpszVariableName: |%s|\n\
 lpvBuffer: %d\n\
 lpdwSize: %d %d\n",
         hConn, lpszVariableName, lpvBuffer,
         lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
   }

   if (lpszVariableName[0] == '*' ||
       strsame (lpszVariableName, "ALL_HTTP", -1))
   {
      /************************/
      /* all, or all HTTP_... */
      /************************/

      zptr = (sptr = lpvBuffer) + *lpdwSize;
      while ((cptr = CgiLibVar("*")) != NULL)
      {
         if (lpszVariableName[0] == '*' ||
             !memcmp (cptr, HTTP_VARIABLE_PREFIX, HTTP_VARIABLE_PREFIX_LENGTH))
         {
            while (*cptr && *cptr != '=' && sptr < zptr) *sptr++ = *cptr++;
            if (*cptr) cptr++;
            if (sptr < zptr) *sptr++ = ':';
            if (sptr < zptr) *sptr++ = ' ';
            while (*cptr && sptr < zptr) *sptr++ = *cptr++;
            if (sptr < zptr) *sptr++ = '\n';
            if (sptr >= zptr) break;
         }
      }
      if (sptr >= zptr)
      {
         /* too small, suggest to double it's size */
         *(char*)lpvBuffer = '\0';
         *lpdwSize = *lpdwSize < 1;

         if (DllDebug)
         {
            fprintf (stdout,
" BUFFER OVERFLOW\n\
 lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: FALSE]\n",
                (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
         }

         return (FALSE);
      }
      *sptr = '\0';

      *lpdwSize = sptr - (char*)lpvBuffer + 1;

      if (DllDebug)
      {
         fprintf (stdout,
" FOUND\n\
 lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: TRUE]\n",
             (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
      }

      return (TRUE);
   }

   /***********************/
   /* single CGI variable */
   /***********************/

   /* fudge: Purveyor must use "user-agent" because it's example DLL does! */
   if (strsame (lpszVariableName, "user-agent", -1))
      lpszVariableName = "http_user_agent";

   if ((cptr = CgiLibVarPtr = CgiLibVar (lpszVariableName)) == NULL)
   {
      *(char*)lpvBuffer = '\0';
      *lpdwSize = 0;

      if (DllDebug)
      {
         fprintf (stdout,
" NOT FOUND\n\
 lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: FALSE]\n",
             (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
      }

      return (FALSE);
   }
   else
   {
      if (strsame (lpszVariableName, "content_length", -1))
      {
         /* special case, return binary value */

         if (*lpdwSize < sizeof(DWORD))
         {
            *lpdwSize = sizeof(DWORD);

            if (DllDebug)
            {
               fprintf (stdout,
" BUFFER OVERFLOW\n\
 lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: FALSE]\n",
                 (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
            }

            return (FALSE);
         }

         *(DWORD*)lpvBuffer = atoi(cptr);
         *lpdwSize = sizeof(DWORD);

         if (DllDebug)
         {
            fprintf (stdout,
" FOUND\n\
 lpvBuffer: %d %d\n\
 lpdwsize: %d %d\n\
 return: TRUE]\n",
                lpvBuffer, *(DWORD*)lpvBuffer,
                lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
         }

         return (TRUE);
      }

      zptr = (sptr = lpvBuffer) + *lpdwSize - 1;
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';

      if (sptr < zptr)
      {
         *lpdwSize = sptr - (char*)lpvBuffer + 1;

         if (DllDebug)
         {
            fprintf (stdout,
" FOUND\n\
 lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: TRUE]\n",
                (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
         }

         return (TRUE);
      }
      else
      {
         while (*cptr) cptr++;
         *lpdwSize = cptr - CgiLibVarPtr + 1;

         if (DllDebug)
         {
            fprintf (stdout,
" BUFFER OVERFLOW\n\
 lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: FALSE]\n",
                (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
         }

         return (FALSE);
      }
   }
}

/*****************************************************************************/
/*
DLL callback to read (further) request content (body) from the client.  CGISAPI
_always_ supplies all the request content with the initial ECB supplied to the
HttpExtensionProc() entry-point, so this really never will need to be called. 
It it is it just indicates there is no more data available.  IsapiEcbInit()
calls this procedure with 'lpvBuffer' set to NULL to initially set the body
content fields correctly whether there is content or not.
*/

BOOL IsapiReadClient
(
HCONN ConnID,
LPVOID lpvBuffer,
LPDWORD lpdwSize
)
{
   static int  BufferCount;
   static char  *BufferPtr;

   char  *cptr;
   EXTENSION_CONTROL_BLOCK  *lpEcb;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "IsapiReadClient() %d\n", ConnID);

   lpEcb = (EXTENSION_CONTROL_BLOCK*)ConnID;

   if (lpvBuffer == NULL)
   {
      if ((cptr = CgiLibVar("CONTENT_LENGTH")) == NULL)
      {
         lpEcb->cbTotalBytes = lpEcb->cbAvailable = BufferCount = 0;
         lpEcb->lpbData = BufferPtr = "";
         return (FALSE);
      }
      else
      {
         lpEcb->cbTotalBytes = atoi(cptr);
         if (Debug)
            fprintf (stdout, "lpEcb->cbTotalBytes: %d\n", lpEcb->cbTotalBytes);
      }

      if (CgiLibReadRequestBody (&BufferPtr, &BufferCount))
      {
         if (Debug) fprintf (stdout, "BufferCount: %d\n", BufferCount);
         if (lpEcb->cbTotalBytes == BufferCount)
         {
            /* always return the entire body with the initial call! */
            lpEcb->lpbData = BufferPtr;
            lpEcb->cbAvailable = BufferCount;
            return (TRUE);
         }
         else
         {
            lpEcb->cbTotalBytes = lpEcb->cbAvailable = BufferCount = 0;
            lpEcb->lpbData = BufferPtr = "";
            return (FALSE);
         }
      }
      else
      {
         lpEcb->cbTotalBytes = lpEcb->cbAvailable = BufferCount = 0;
         lpEcb->lpbData = BufferPtr = "";
         return (FALSE);
      }
   }

   if (DllDebug)
   {
      fprintf (stdout,
"[ReadClient()\n\
 ConnID: %d\n\
 lpvBuffer: %d\n\
 lpdwSize: %d %d\n",
         ConnID, lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
   }

   /* explicit calls to IsapiReadClient() always have nothing to return! */
   *(char*)lpvBuffer = '\0';
   *lpdwSize = 0;

   if (DllDebug)
   {
      fprintf (stdout,
" lpvBuffer: |%s|\n\
 lpdwsize: %d %d\n\
 return: FALSE]\n",
         (char*)lpvBuffer, lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize);
   }

   return (FALSE);
}

/*****************************************************************************/
/*
DLL callback, write the supplied buffer to the client.  The efficiencies
provided by buffering this data, reducing the number of I/Os between the script
and the server, are worth explicitly buffering it here.  To flush what is
buffered at any time call this function with a zero-length 'lpdwBytes'.

Why not use the C-RTL for this?  Two reasons.  The data given to this function
is specified by pointer and a length, meaning an fwrite() needs to be used
(which does an implicit flush).  Why not then freopen(,,,"ctx=xplct")?  This
worked on Alpha VMS 7.1 but barfed on VAX VMS 6.2 (at least), hence this work
around.  This buffering reduced response time to 12.5% of what it was without
it!! (as measured using the ISAPIEXAMPLE.C program outputing lines of 80
characters).
*/

BOOL IsapiWriteClient
(
HCONN ConnID,
LPVOID lpvBuffer,
LPDWORD lpdwBytes,
DWORD dwReserved
)
{
   static int  WriteBufferCapacity;
   static char  *WriteBuffer = NULL,
                *WriteBufferPtr;

   int  DataBytes,
        DataCount;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "IsapiWriteClient()\n");

   if (DllDebug)
   {
      fprintf (stdout,
"[WriteClient()\n\
 ConnID: %d\n\
 lpvBuffer: %d\n\
 lpdwSize: %d %d\n\
 dwReserved: %d]\n",
         ConnID, lpvBuffer,
         lpdwBytes, lpdwBytes == NULL ? 0 : *lpdwBytes,
         dwReserved);
   }

   if (Debug)
      fprintf (stdout, "%d %d %d\n",
               WriteBuffer, WriteBufferPtr, WriteBufferCapacity);

   if (!WriteBufferSize ||
       (DllDebug &&
        lpdwBytes != NULL &&
        (DataBytes = *lpdwBytes)))
   {
      /* if buffer size set zero, or when debugging the DLL, do not buffer */
      fwrite (lpvBuffer, *lpdwBytes, 1, stdout);
   }
   else
   if (WriteBufferSize &&
       lpdwBytes != NULL &&
       (DataBytes = *lpdwBytes))
   {
      if (WriteBuffer == NULL)
         if ((WriteBufferPtr = WriteBuffer =
             calloc (1, WriteBufferCapacity = WriteBufferSize)) == NULL)
            exit (vaxc$errno);

      if (DataBytes <= WriteBufferCapacity)
      {
         /* initial space in the buffer */
         memcpy (WriteBufferPtr, lpvBuffer, DataBytes);
         WriteBufferPtr += DataBytes;
         WriteBufferCapacity -= DataBytes;
      }
      else
      {
         /* no initial space in the buffer */
         DataCount = WriteBufferCapacity;
         DataBytes -= DataCount;
         for (;;)
         {
            memcpy (WriteBufferPtr, lpvBuffer, DataCount);
            fwrite (WriteBuffer, WriteBufferSize, 1, stdout);

            WriteBufferPtr = WriteBuffer;
            WriteBufferCapacity = WriteBufferSize;
            lpvBuffer = (void*)((int)lpvBuffer + DataCount);
            if (DataBytes <= WriteBufferCapacity)
               DataCount = DataBytes;
            else
               DataCount = WriteBufferCapacity;
            memcpy (WriteBufferPtr, lpvBuffer, DataCount);
            WriteBufferPtr += DataCount;
            WriteBufferCapacity -= DataCount;
            DataBytes -= DataCount;

            if (!DataBytes) break;
         }
      }
   }
   else
   if (WriteBufferSize &&
       !DllDebug &&
       WriteBuffer != NULL &&
       WriteBufferPtr > WriteBuffer)
   {
      /* flush the write buffer */
      fwrite (WriteBuffer, WriteBufferPtr-WriteBuffer, 1, stdout);
      WriteBufferPtr = WriteBuffer;
      WriteBufferCapacity = WriteBufferSize;
   }

   if (DllDebug) fprintf (stdout, "\n[return: TRUE]\n");

   return (TRUE);
}

/*****************************************************************************/
/*
DLL callback, provide support functions.
*/

BOOL IsapiServerSupportFunction
(
HCONN hConn,
DWORD dwHSERRequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType
)
{
   char  *CharsetPtr;
   EXTENSION_CONTROL_BLOCK  *lpEcb;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "IsapiServerSupportFunction() %d %d %d\n",
               dwHSERRequest, lpdwSize == NULL ? 0 : *lpdwSize, lpdwDataType);

   lpEcb = (EXTENSION_CONTROL_BLOCK*)hConn;

   if (DllDebug)
   {
      fprintf (stdout,
"[ServerSupportFunction()\n\
 hConn: %d\n\
 dwHSERRequest: %d\n\
 lpvBuffer: %d\n\
 lpdwSize: %d %d\n\
 lpdwDataType: %d %d\n",
         hConn, dwHSERRequest, lpvBuffer,
         lpdwSize, lpdwSize == NULL ? 0 : *lpdwSize,
         lpdwDataType, lpdwDataType == NULL ? 0 : *lpdwDataType);
   }

   switch (dwHSERRequest)
   {
      case HSE_REQ_SEND_URL_REDIRECT_RESP :

         if (DllDebug || Debug)
            fprintf (stdout, " HSE_REQ_SEND_URL_REDIRECT_RESP]\n");

         CgiLibResponseRedirect (lpvBuffer);
         fflush (stdout);

         if (DllDebug) fprintf (stdout, "[return: TRUE]\n");

         return (TRUE);

      case HSE_REQ_SEND_URL :

         if (DllDebug || Debug) fprintf (stdout, " HSE_REQ_SEND_URL]\n");

         /* this should be in effect a WASD CGI internal redirect */
         CgiLibResponseRedirect (lpvBuffer);
         fflush (stdout);

         if (DllDebug) fprintf (stdout, "[return: TRUE]\n");

         return (TRUE);

      case HSE_REQ_SEND_RESPONSE_HEADER :

         if (DllDebug || Debug)
            fprintf (stdout, " HSE_REQ_SEND_RESPONSE_HEADER]\n");

         if (lpvBuffer == NULL)
            fputs ("Status: 200\r\n", stdout);
         else
            fputs (lpvBuffer, stdout);

         if (lpdwDataType == NULL)
         {
            if ((CharsetPtr = CliCharsetPtr) == NULL)
            {
               /* there is no script-enforced character set */
               CharsetPtr = CgiLibVar ("REQUEST_CHARSET");
               if (!CharsetPtr[0])
                  CharsetPtr = CgiLibVar ("SERVER_CHARSET");
               if (!CharsetPtr[0])
                  CharsetPtr = DEFAULT_CHARSET;
            }

            fprintf (stdout, "Content-Type: text/plain; charset=%s\n\n",
                     CharsetPtr);
         }
         else
            fputs ((char*)lpdwDataType, stdout);

         fflush (stdout);

         if (DllDebug) fprintf (stdout, "[return: TRUE]\n");

         return (TRUE);

      case HSE_REQ_DONE_WITH_SESSION :

         if (DllDebug || Debug)
            fprintf (stdout, " HSE_REQ_DONE_WITH_SESSION]\n");

         /* we're not threaded here, so anything pending's not supported! */
         CgiLibResponseError (FI_LI, 0, "HSE_REQ_DONE_WITH_SESSION not supported!");

         exit (SS$_NORMAL);

      case HSE_REQ_DLL_DEBUG_ON :

         if (DllDebug || Debug)
            fprintf (stdout, " HSE_REQ_DLL_DEBUG_ON\n return: TRUE]\n");

         DllDebugOn = true;
         return (TRUE);

      case HSE_REQ_DLL_DEBUG_OFF :

         if (DllDebug || Debug)
            fprintf (stdout, " HSE_REQ_DLL_DEBUG_OFF\n return: TRUE]\n");

         DllDebugOn = false;
         return (TRUE);

      case HSE_REQ_DLL_WRITE_BUFFER_SIZE :

         if (DllDebug || Debug)
            fprintf (stdout, " HSE_REQ_DLL_WRITE_BUFFER_SIZE\n return: TRUE]\n");

         if (lpdwSize != NULL)
         {
            WriteBufferSize = *lpdwSize;
            /* zero disables output buffering */
            if (WriteBufferSize)
            {
               /* make sure it's a sensible size */
               if (WriteBufferSize < WRITE_BUFFER_SIZE_MIN)
                  WriteBufferSize = WRITE_BUFFER_SIZE_MIN;
               else
               if (WriteBufferSize > WRITE_BUFFER_SIZE_MAX)
                  WriteBufferSize = WRITE_BUFFER_SIZE_MAX;
            }
         }

         return (TRUE);

      default :

         if (DllDebug) fprintf (stdout, " return: FALSE]\n");

         return (FALSE);
   }
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.
*/
                
GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   register char  *aptr, *cptr, *clptr, *sptr;

   int  status;
   unsigned short  Length;
   char  ch;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetParameters()\n");

   if ((clptr = getenv ("CGISAPI$PARAM")) == NULL)
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/VERSION=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         IsapiMajorVersion = atoi(cptr);
         continue;
      }
      if (strsame (aptr, "/WBUFFER=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         WriteBufferSize = atoi(cptr);
         continue;
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (CliDllFileNamePtr == NULL)
      {
         CliDllFileNamePtr = aptr;
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns 
true if two strings are the same, or false if not.  If a maximum number of 
characters are specified only those will be compared, if the entire strings 
should be compared then specify the number of characters as 0.
*/ 

boolean strsame
(
register char *sptr1,
register char *sptr2,
register int  count
)
{
   while (*sptr1 && *sptr2)
   {
      if (toupper (*sptr1++) != toupper (*sptr2++)) return (false);
      if (count)
         if (!--count) return (true);
   }
   if (*sptr1 || *sptr2)
      return (false);
   else
      return (true);
}

/*****************************************************************************/

