/*****************************************************************************/
/*
                               AuthCache.c


    THE GNU GENERAL PUBLIC LICENSE APPLIES DOUBLY TO ANYTHING TO DO WITH
                    AUTHENTICATION AND AUTHORIZATION!

    This package is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; version 2 of the License, or any later
    version.

>   This package is distributed in the hope that it will be useful,
>   but WITHOUT ANY WARRANTY; without even the implied warranty of
>   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
>   GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.


INSTANCE SUPPORT
----------------
Note that within the global section no self-relative addresses can be used
(i.e. a linked list cannot be used).  All references into the section must be
made relative to the starting address.  This is due to the starting address not
being fixed in per-process virtual memory.

With the advent of multiple integrated "instances" executing on a single system
and sharing incoming service requests it became highly desirable to provide a
single global instance of the especially significant authentication cache
resource.

This has been done by making it work within the confines of globally shared
memory (i.e. a global section and collection of global pages).  This
necessarily puts some constraints on the design and operation.  Most notably
normal memory allocation routines are impossible and the quantity of memory
available for the required structures becomes constrained.  Hence a fixed (but
configurable) sized authentication cache, with records being reused as
required.

A typical message when there are too few cache entries for the concurrent
number of requests requiring authorization is SS$_ITEMNOTFOUND (or
"%SYSTEM-W-ITEMNOTFOUND, requested item cannot be returned").  This indicates
the authorization cache is "thrashing" for space (i.e. too many concurrent
requests attempting to use it).  Increase the value of [AuthCacheEntriesMax],
shut down all servers (to cause the global section to be deleted) and then
restart.

Typical message for insufficient storage space for an authorization cache
entry id SS$_RESULTOVF or "%SYSTEM-F-RESULTOVF, resultant string overflow". 
This might occur if /PROFILE was in use and an account generated a particularly
large one!  Increase the value of [AuthCacheEntrySize], shut down all servers
(to cause the global section to be deleted) and then restart.

See AUTH.C for overall detail on the WASD authorization environment.


LOGIN COOKIE
------------
Consecutive browser authentication dialogs can occur if a cache entry exists
for the realm/username with a user revalidation timer expired but in the
interim the user has closed the browser and so submits the initial request with
no request header authorization field at all.  This is bounced straight back
and when valid credentials are entered and accessed the cached entry indicates
there need to be revalidation resulting in an immediate browser reprompt for
the same resource ... mighty irritating!

The "login" cookie approach attempts to work around this by returning a cookie
containing a unique, "one-shot" token (in this case a non-deterministically
generated 32 bit number) with the response to all requests that contain no
authorization.  This token is cached on the server and when the next request
(presumably containing the required authorization information) is returned the
cookie is with it, the token is looked up in the cache and if found user
revalidation suppressed.  The cached token is cancelled during this lookup and
so can "never" be used again.

Note that the cookie contains no authorization information itself!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A request must always also contain the required HTTP authorization header
fields.  The method of generating the token makes spoofing the cookie
improbable, and it is also only valid for the next, single request.  Even if
spoofing could occur it wouldn't authorize anything of itself - the token just
suppresses revalidation of already supplied authentication.  If authentication
presented at the next request is incorrect and refused no new token is issued
and the user would then experience the revalidation-timeout-induced failure
once correct credentials are supplied.


VERSION HISTORY
---------------
08-SEP-2003  MGD  allow for config-directory in cache
06-AUG-2003  MGD  sanity checking for locked cache
                  bugfix; AuthCacheAddRecord()
04-FEB-2003  MGD  AuthCacheReset() minor refinements
06-AUG-2002  MGD  enhance global section creation,
                  bugfix; set AuthCacheRecordSize from HTTPD$CONFIG value
02-MAR-2002  MGD  bugfix; read group cache offset assignment
20-OCT-2001  MGD  significant cache rework (for instance sharing)
04-AUG-2001  MGD  support module WATCHing
07-MAY-2000  MGD  login cookie
08-APR-2000  MGD  bugfix; AuthCacheFree()
04-MAR-2000  MGD  use NetWriteFaol(), et.al.
24-DEC-1999  MGD  break-in evasion period in report
28-AUG-1999  MGD  unbundled from AUTH.C for v6.1
*/
/*****************************************************************************/

#ifdef WASD_VMS_V6
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#define __VMS_VER 60000000
#undef __CRTL_VER
#define __CRTL_VER 60000000
#endif

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

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

/* application related header files */
#include "wasd.h"

#define WASD_MODULE "AUTHCACHE"

#if WATCH_MOD
#define FI_NOLI WASD_MODULE, __LINE__
#else
/* in production let's keep the exact line to ourselves! */
#define FI_NOLI WASD_MODULE, 0
#endif

/******************/
/* global storage */
/******************/

int  AuthCacheRecordMax,
     AuthCacheRecordSize;

char  AuthCacheLoginCookieName [] = AUTH_LOGIN_COOKIE_NAME;

int AuthCacheLoginCookieNameLength = sizeof(AuthCacheLoginCookieName)-1;

AUTH_GBLSEC  *AuthGblSecPtr;
int  AuthGblSecPages,
     AuthGblSecSize;

/********************/
/* external storage */
/********************/

#ifdef DBUG
extern BOOL Debug;
#else
#define Debug 0 
#endif

extern BOOL  AuthVmsUserProfileEnabled,
             InstanceNodeSupervisor;

extern BOOL  InstanceMutexHeld[];

extern int  AuthGblSecVersion,
            GblPageCount,
            GblSectionCount,
            HttpdTickSecond,
            InstanceNodeConfig,
            InstanceGroupNumber;

extern unsigned long  GblSecPrvMask [];

extern char  ErrorSanityCheck[];

extern ACCOUNTING_STRUCT  *AccountingPtr;
extern CONFIG_STRUCT  Config;
extern HTTPD_GBLSEC  *HttpdGblSecPtr;
extern MSG_STRUCT  Msgs;
extern SYS_INFO  SysInfo;
extern WATCH_STRUCT  Watch;

/*****************************************************************************/
/*
Initialize the authentication cache.
*/ 

AuthCacheInit (META_CONFIG *mcptr)

{
   static char  ProblemRecordSize [] =
"Cache record size could be too small (adjusted upwards)";

   int  cnt, status;
   AUTH_CREC  *acrptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "AuthCacheInit()");

   if (AuthGblSecPtr)
   {
      /* cache has previously been initialized, just reset it */
      AuthCachePurge (true);
      return;
   }

   AuthCacheRecordMax = Config.cfAuth.CacheEntriesMax;
   AuthCacheRecordSize = Config.cfAuth.CacheEntrySize;

   if (AuthCacheRecordMax < AUTH_DEFAULT_CACHE_RECORD_MAX)
      AuthCacheRecordMax = AUTH_DEFAULT_CACHE_RECORD_MAX;

   if (!AuthCacheRecordSize)
      if (AuthVmsUserProfileEnabled)
         AuthCacheRecordSize = AUTH_DEFAULT_CACHE_RECORD_PROFILE_SIZE;
      else
         AuthCacheRecordSize = AUTH_DEFAULT_CACHE_RECORD_SIZE;
   /* let's round it to a 64 byte chunk */
   if (AuthCacheRecordSize % 64)
      AuthCacheRecordSize = ((AuthCacheRecordSize / 64) + 1) * 64;

   if (AuthVmsUserProfileEnabled &&
       AuthCacheRecordSize < AUTH_DEFAULT_CACHE_RECORD_PROFILE_SIZE)
   {
      MetaConReport (mcptr, METACON_REPORT_ERROR, ProblemRecordSize);
      AuthCacheRecordSize = AUTH_DEFAULT_CACHE_RECORD_PROFILE_SIZE;
   }
   else
   if (AuthCacheRecordSize < AUTH_DEFAULT_CACHE_RECORD_SIZE)
   {
      MetaConReport (mcptr, METACON_REPORT_ERROR, ProblemRecordSize);
      AuthCacheRecordSize = AUTH_DEFAULT_CACHE_RECORD_PROFILE_SIZE;
   }

   AuthCacheGblSecInit (mcptr);
}

/*****************************************************************************/
/*
If only one instance can execute (from configuration) then allocate a block of
process-local dynamic memory and point to that as the cache.  If multiple
instances create and map a global section and point to that.
*/ 

AuthCacheGblSecInit (META_CONFIG *mcptr)

{
   static char  ReportCacheRecords [] =
"Cache for !UL records of !UL bytes in !AZ of !UL page(let)s";

   /* global, allocate space, system, in page file, writable */
   static int CreFlags = SEC$M_GBL | SEC$M_EXPREG | SEC$M_SYSGBL |
                         SEC$M_PAGFIL | SEC$M_WRT;
   static int DelFlags = SEC$M_SYSGBL;
   /* system & owner full access, group and world no access */
   static unsigned long  ProtectionMask = 0xff00;
   /* it is recommended to map into any virtual address in the region (P0) */
   static unsigned long  InAddr [2] = { 0x200, 0x200 };

   int  attempt, status,
        BaseGblSecPages,
        CacheRecordPoolSize,
        PageCount,
        SetPrvStatus;
   short  ShortLength;
   unsigned long  RetAddr [2];
   char  GblSecName [32];
   $DESCRIPTOR (GblSecNameDsc, GblSecName);
   AUTH_GBLSEC  *gsptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "AuthCacheGblSecInit()");

   for (;;)
   {
      CacheRecordPoolSize = AuthCacheRecordSize * AuthCacheRecordMax;
      AuthGblSecSize = sizeof(AUTH_GBLSEC) + CacheRecordPoolSize;
      AuthGblSecPages = AuthGblSecSize / 512;
#ifndef __VAX
      /* may as well fully utilize the full GBLPAGE(s) */
      if (AuthGblSecPages % SysInfo.PageFactor &&
          AuthGblSecPages % SysInfo.PageFactor < SysInfo.PageFactor - 1)
      {
         AuthCacheRecordMax++;
         continue;
      }
#endif
      if (AuthGblSecPages & 0x1ff) AuthGblSecPages++;
      if (Debug) fprintf (stdout, "page(lets)s: %d\n", AuthGblSecPages);
      break;
   }

   if (InstanceNodeConfig <= 1)
   {
      /* no need for a global section, just use process-local storage */
      AuthGblSecPtr = (AUTH_GBLSEC*)VmGet (AuthGblSecPages * 512);
      sys$gettim (&AuthGblSecPtr->SinceBinTime);
      MetaConReport (mcptr, METACON_REPORT_INFORM, ReportCacheRecords,
                     AuthCacheRecordMax, AuthCacheRecordSize,
                     "local storage", AuthGblSecPages);
      return (SS$_CREATED);
   }

   WriteFao (GblSecName, sizeof(GblSecName), &ShortLength,
             GBLSEC_NAME_FAO, HTTPD_NAME, AUTH_GBLSEC_VERSION,
             InstanceGroupNumber, "AUTH");
   GblSecNameDsc.dsc$w_length = ShortLength;

   if VMSnok ((SetPrvStatus = sys$setprv (1, &GblSecPrvMask, 0, 0)))
      ErrorExitVmsStatus (SetPrvStatus, "sys$setprv()", FI_LI);

   for (attempt = 1; attempt <= 2; attempt++)
   {
      /* create and/or map the specified global section */
      sys$setprv (1, &GblSecPrvMask, 0, 0);
      status = sys$crmpsc (&InAddr, &RetAddr, 0, CreFlags,
                           &GblSecNameDsc, 0, 0, 0, AuthGblSecPages, 0,
                           ProtectionMask, AuthGblSecPages);
      sys$setprv (0, &GblSecPrvMask, 0, 0);

      if (WATCH_MODULE(WATCH_MOD__OTHER))
         WatchThis (NULL, FI_LI, WATCH_MOD__OTHER,
                    "sys$crmpsc() !&S begin:!UL end:!UL",
                    status, RetAddr[0], RetAddr[1]);

      PageCount = (RetAddr[1]+1) - RetAddr[0] >> 9;
      AuthGblSecPtr = gsptr = (AUTH_GBLSEC*)RetAddr[0];
      AuthGblSecPages = PageCount;
      if (VMSnok (status) || status == SS$_CREATED) break;

      /* section already exists, break if 'same size' and version! */
      if (gsptr->GblSecVersion &&
          (gsptr->GblSecVersion == AuthGblSecVersion ||
           gsptr->GblSecLength == AuthGblSecSize))
         break;

      /* delete the current global section, have one more attempt */
      sys$setprv (1, &GblSecPrvMask, 0, 0);
      status = sys$dgblsc (DelFlags, &GblSecNameDsc, 0);
      sys$setprv (0, &GblSecPrvMask, 0, 0);
      status = SS$_IDMISMATCH;
   }

   if (VMSnok (status))
   {
      /* must have this global section! */
      char  String [256];
      WriteFao (String, sizeof(String), NULL,
                "1 global section, !UL global pages", AuthGblSecPages);
      ErrorExitVmsStatus (status, String, FI_LI);
   }

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH,
         "GBLSEC \"!AZ\" page(let)s:!UL !&S %!-!&M",
         GblSecName, PageCount, status);

   MetaConReport (mcptr, METACON_REPORT_INFORM, ReportCacheRecords,
                  AuthCacheRecordMax, AuthCacheRecordSize,
                  status == SS$_CREATED ? "a new global section" :
                                          "an existing global section",
                  AuthGblSecPages);

   if (status == SS$_CREATED)
   {
      /* first time it's been mapped */
      memset (gsptr, 0, PageCount * 512);
      gsptr->GblSecVersion = AuthGblSecVersion;
      gsptr->GblSecLength =  AuthGblSecSize;
      sys$gettim (&AuthGblSecPtr->SinceBinTime);
   }

   GblSectionCount++;
   GblPageCount += PageCount;

   return (status);
}

/*****************************************************************************/
/*
Create a request-local record of authorization information.
*/ 

AuthCacheRequestRecord
(
REQUEST_STRUCT *rqptr,
AUTH_CREC **RecordPtrPtr
)
{
   char  *cptr, *sptr;
   AUTH_CREC  *acrptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "AuthCacheRequestRecord()");

   acrptr = (AUTH_CREC*)VmGetHeap (rqptr, AuthCacheRecordSize);
   *RecordPtrPtr = acrptr;

   acrptr->LastAccessBinTime[0] = rqptr->rqTime.Vms64bit[0];
   acrptr->LastAccessBinTime[1] = rqptr->rqTime.Vms64bit[1];

   acrptr->SourceRealm = rqptr->rqAuth.SourceRealm;
   acrptr->SourceGroupRead = rqptr->rqAuth.SourceGroupRead;
   acrptr->SourceGroupWrite = rqptr->rqAuth.SourceGroupWrite;

   /* "fixed" record fields */

   strcpy (acrptr->Realm, rqptr->rqAuth.RealmPtr);
   acrptr->RealmLength = rqptr->rqAuth.RealmLength;

   strcpy (acrptr->UserName, rqptr->RemoteUser);
   acrptr->UserNameLength = rqptr->RemoteUserLength;
   strcpy (acrptr->Password, rqptr->RemoteUserPassword);

   /* "dynamic" record fields */
   cptr = sptr = (char*)acrptr->Storage;

   memcpy (acrptr->ConfigDirectory = cptr,
           rqptr->ConfigDirectory,
           rqptr->ConfigDirectoryLength + 1);
   acrptr->ConfigDirectoryLength = rqptr->ConfigDirectoryLength;
   acrptr->ConfigDirectoryOffset = cptr - sptr;
   cptr += acrptr->ConfigDirectoryLength + 1;

   memcpy (acrptr->PathParameter = cptr,
           rqptr->rqAuth.PathParameterPtr,
           rqptr->rqAuth.PathParameterLength + 1);
   acrptr->PathParameterLength = rqptr->rqAuth.PathParameterLength;
   acrptr->PathParameterOffset = cptr - sptr;
   cptr += acrptr->PathParameterLength + 1;

   acrptr->SourceGroupWrite = rqptr->rqAuth.SourceGroupWrite;
   memcpy (acrptr->GroupWrite = cptr,
           rqptr->rqAuth.GroupWritePtr,
           rqptr->rqAuth.GroupWriteLength + 1);
   acrptr->GroupWriteLength = rqptr->rqAuth.GroupWriteLength;
   acrptr->GroupWriteOffset = cptr - sptr;
   cptr += acrptr->GroupWriteLength + 1;

   acrptr->SourceGroupRead = rqptr->rqAuth.SourceGroupRead;
   memcpy (acrptr->GroupRead = cptr,
           rqptr->rqAuth.GroupReadPtr,
           rqptr->rqAuth.GroupReadLength + 1);
   acrptr->GroupReadLength = rqptr->rqAuth.GroupReadLength;
   acrptr->GroupReadOffset = cptr - sptr;
   cptr += acrptr->GroupReadLength + 1;
                     
   /* note where we can start storing user details, VMS profile, etc. */
   acrptr->StorageNextOffset = cptr - sptr;

   /* small sanity check */
   if (AuthCacheRecordSize - (cptr - (char*)acrptr) <= 0)
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);
}

/*****************************************************************************/
/*
Add the supplied authorization information to the (global section) cache.
*/ 

int AuthCacheAddRecord
(
AUTH_CREC *aptr,
AUTH_CREC **RecordPtrPtr
)
{
   int  cnt,
        OldestSecond,
        RecordCount;
   char  *cptr;
   unsigned char  *RecordPoolPtr;
   AUTH_CREC  *acrptr,
              *acr2ptr;
   
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH,
                 "AuthCacheAddRecord() !UL", AuthGblSecPtr->CacheRecordCount);

   if (InstanceNodeConfig > 1 && !InstanceMutexHeld[INSTANCE_MUTEX_AUTH_CACHE])
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   RecordCount = AuthGblSecPtr->CacheRecordCount;
   RecordPoolPtr = AuthGblSecPtr->CacheRecordPool;

   if (RecordCount < AuthCacheRecordMax)
   {
      /* fresh record */
      acrptr = RecordPoolPtr + (AuthCacheRecordSize * RecordCount);
      AuthGblSecPtr->CacheRecordCount++;
   }
   else
   {
      /* all entries in list in use, reuse the least recently accessed */
      OldestSecond = HttpdTickSecond + 999999;
      acrptr = acr2ptr = RecordPoolPtr;
      for (cnt = 0; cnt < RecordCount; cnt++)
      {
         acrptr = RecordPoolPtr + (AuthCacheRecordSize * cnt);
         if (acrptr->LastAccessTickSecond < OldestSecond)
         {                              
            acr2ptr = acrptr;
            OldestSecond = acrptr->LastAccessTickSecond;
         }
         if (acrptr->SourceRealm) continue;
         break;
      }
      AuthGblSecPtr->CacheReuseCount++;
      acrptr = acr2ptr;
      memset (acrptr, 0, AuthCacheRecordSize);
   }

   /* copy contents of source record */
   memcpy (acrptr, aptr, AuthCacheRecordSize);

   /* adjust variable length field pointers using the field offsets */
   cptr = (char*)acrptr->Storage;
   acrptr->ConfigDirectory = cptr + acrptr->ConfigDirectoryOffset;
   acrptr->PathParameter = cptr + acrptr->PathParameterOffset;
   acrptr->GroupWrite = cptr + acrptr->GroupWriteOffset;
   acrptr->GroupRead = cptr + acrptr->GroupReadOffset;
 
   sys$gettim (&acrptr->LastAccessBinTime);
   acrptr->LastAccessTickSecond = HttpdTickSecond;
   acrptr->LastAccessMinutesAgo = acrptr->LastAccessSecondsAgo = 0;

   *RecordPtrPtr = acrptr;

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Add additional details to the cached record.
*/ 

int AuthCacheAddRecordDetails
(
REQUEST_STRUCT *rqptr,
AUTH_CREC *acrptr
)
{
   int  StorageRemaining;
   char  *cptr, *sptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "AuthCacheAddRecordDetails()");

   if (InstanceNodeConfig > 1 && !InstanceMutexHeld[INSTANCE_MUTEX_AUTH_CACHE])
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   acrptr->DetailsUpdated = true;

   sptr = (char*)acrptr->Storage;
   cptr = sptr + acrptr->StorageNextOffset;
   StorageRemaining = AuthCacheRecordSize - (cptr - (char*)acrptr);

   StorageRemaining -= rqptr->rqAuth.UserDetailsLength + 1;
   if (StorageRemaining <= 0)
   {
      ErrorNoticed (SS$_RESULTOVF, "AuthCacheAddRecordDetails()", FI_LI);
      return (SS$_RESULTOVF);
   }

   memcpy (acrptr->UserDetails = cptr,
           rqptr->rqAuth.UserDetailsPtr ? rqptr->rqAuth.UserDetailsPtr : "",
           rqptr->rqAuth.UserDetailsLength + 1);
   acrptr->UserDetailsLength = rqptr->rqAuth.UserDetailsLength;
   acrptr->UserDetailsOffset = acrptr->UserDetails - sptr;
   cptr += acrptr->UserDetailsLength + 1;
   
   StorageRemaining -= rqptr->rqAuth.VmsUserProfileLength;
   if (StorageRemaining <= 0)
   {
      ErrorNoticed (SS$_RESULTOVF, "AuthCacheAddRecordDetails()", FI_LI);
      return (SS$_RESULTOVF);
   }

   if (acrptr->VmsUserProfileLength = rqptr->rqAuth.VmsUserProfileLength)
   {
      memcpy (acrptr->VmsUserProfilePtr = cptr,
              rqptr->rqAuth.VmsUserProfilePtr,
              rqptr->rqAuth.VmsUserProfileLength);
      acrptr->VmsUserProfileLength = rqptr->rqAuth.VmsUserProfileLength;
      acrptr->VmsUserProfileOffset = acrptr->VmsUserProfilePtr - sptr;
      cptr += acrptr->VmsUserProfileLength;
   }

   /* note where we could if we needed to start storing more detail */
   acrptr->StorageNextOffset = cptr - sptr;

   return (SS$_NORMAL);
}

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

int AuthCacheFindRecord
(
AUTH_CREC *acr1ptr,
AUTH_CREC **RecordPtrPtr
)
{
   int  cnt,
        MinutesAgo,
        RecordCount;
   unsigned char  *RecordPoolPtr;
   AUTH_CREC  *acr2ptr;
   
   /*********/
   /* begin */
   /*********/

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH,
                 "AuthCacheFindRecord() !UL !UL",
                 AuthGblSecPtr->CacheRecordCount, acr1ptr->FindRecordCount);

   if (InstanceNodeConfig > 1 && !InstanceMutexHeld[INSTANCE_MUTEX_AUTH_CACHE])
      ErrorExitVmsStatus (SS$_BUGCHECK, ErrorSanityCheck, FI_LI);

   RecordCount = AuthGblSecPtr->CacheRecordCount;
   RecordPoolPtr = AuthGblSecPtr->CacheRecordPool;

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      acr2ptr = RecordPoolPtr + (AuthCacheRecordSize * cnt);
      if (!acr2ptr->SourceRealm) continue;

      if (WATCH_MODULE(WATCH_MOD_AUTH) && WATCH_MODULE(WATCH_MOD__DETAIL))
         WatchDataFormatted (
"!UL !UL\n!UL !UL\n!UL !UL\n!&Z !&Z\n!&Z !&Z\n\
!&Z !&Z\n!&Z !&Z\n!&Z !&Z\n!&Z !&Z\n!UL !UL !&B\n",
            acr1ptr->SourceRealm, acr2ptr->SourceRealm,
            acr1ptr->SourceGroupWrite, acr2ptr->SourceGroupWrite,
            acr1ptr->SourceGroupRead, acr2ptr->SourceGroupRead,
            acr1ptr->Realm, acr2ptr->Realm,
            acr1ptr->ConfigDirectory, acr2ptr->ConfigDirectory,
            acr1ptr->PathParameter, acr2ptr->PathParameter,
            acr1ptr->GroupWrite, acr2ptr->GroupWrite,
            acr1ptr->GroupRead, acr2ptr->GroupRead,
            acr1ptr->UserName, acr2ptr->UserName,
            strlen(acr1ptr->Password), strlen(acr2ptr->Password),
            !strcmp(acr1ptr->Password, acr2ptr->Password));

      /* compare the records */
      if (acr1ptr->SourceRealm != acr2ptr->SourceRealm) continue;
      if (acr1ptr->UserNameLength != acr2ptr->UserNameLength) continue;
      if (acr1ptr->RealmLength != acr2ptr->RealmLength) continue;
      if (acr1ptr->ConfigDirectoryLength != acr2ptr->ConfigDirectoryLength)
         continue;
      if (acr1ptr->PathParameterLength != acr2ptr->PathParameterLength)
         continue;
      if (acr1ptr->SourceGroupWrite != acr2ptr->SourceGroupWrite) continue;
      if (acr1ptr->GroupWriteLength != acr2ptr->GroupWriteLength) continue;
      if (acr1ptr->SourceGroupRead != acr2ptr->SourceGroupRead) continue;
      if (acr1ptr->GroupReadLength != acr2ptr->GroupReadLength) continue;
      if (strcmp (acr1ptr->UserName, acr2ptr->UserName)) continue;
      if (strcmp (acr1ptr->Realm, acr2ptr->Realm)) continue;
      if (acr1ptr->ConfigDirectoryLength &&
          strcmp (acr1ptr->ConfigDirectory, acr2ptr->ConfigDirectory)) continue;
      if (acr1ptr->PathParameterLength &&
          strcmp (acr1ptr->PathParameter, acr2ptr->PathParameter)) continue;
      if (acr1ptr->GroupWriteLength &&
          strcmp (acr1ptr->GroupWrite, acr2ptr->GroupWrite)) continue;
      if (acr1ptr->GroupReadLength &&
          strcmp (acr1ptr->GroupRead, acr2ptr->GroupRead)) continue;

      /* got this far, must have matched */
      *RecordPtrPtr = acr2ptr;
      if (!acr1ptr->FindRecordCount++)
      {
         /* first time this request that the record has been searched for */
         acr2ptr->LastAccessSecondsAgo = HttpdTickSecond -
                                         acr2ptr->LastAccessTickSecond;
         acr2ptr->LastAccessMinutesAgo = acr2ptr->LastAccessSecondsAgo / 60;
         if (acr2ptr->LastAccessMinutesAgo < Config.cfAuth.CacheMinutes)
            AuthGblSecPtr->CacheHitCount++;
         else
            AuthGblSecPtr->CacheTimeoutCount++;
         sys$gettim (&acr2ptr->LastAccessBinTime);
         acr2ptr->LastAccessTickSecond = HttpdTickSecond;
      }

      if (WATCH_MODULE(WATCH_MOD_AUTH))
         WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "FOUND");

      return (SS$_NORMAL);
   }

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "NOT-FOUND");

   AuthGblSecPtr->CacheMissCount++;
   return (SS$_ITEMNOTFOUND);
}

/*****************************************************************************/
/*
Clear all of the authentication records.  Will result in all subsequent
requests being re-authenticated from their respective on-disk databases. 
Called from the Admin.c and Control.c modules.  'ControlFlush' indicates its
was initiated through a /DO= command and has therefore been seen by all of
multiple instances.
*/

int AuthCachePurge (BOOL ControlFlush)

{
   int  cnt, status,
        RecordCount;
   unsigned char  *RecordPoolPtr;
   AUTH_CREC  *acrptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH, "AuthCachePurge() !&B !UL !&B",
                 ControlFlush, InstanceNodeConfig, InstanceNodeSupervisor);

   /* in a multi-instance config, shared data only by the supervisor */
   if (ControlFlush && InstanceNodeConfig > 1 && !InstanceNodeSupervisor)
      return (SS$_NORMAL);

   InstanceMutexLock (INSTANCE_MUTEX_AUTH_CACHE);

   RecordCount = AuthGblSecPtr->CacheRecordCount;
   RecordPoolPtr = AuthGblSecPtr->CacheRecordPool;

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      acrptr = RecordPoolPtr + (AuthCacheRecordSize * cnt);
      memset (acrptr, 0, AuthCacheRecordSize);
   }

   AuthGblSecPtr->CacheRecordCount = 0;
   sys$gettim (&AuthGblSecPtr->SinceBinTime);

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_CACHE);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Reset the entry for all records with a matching realm and user name.  This
effectively causes the username to be revalidated next authorized path access.
*/

AuthCacheReset
(
REQUEST_STRUCT *rqptr,
char *Realm,
char *UserName
)
{
   int  cnt, status,
        RecordCount;
   unsigned char  *RecordPoolPtr;
   AUTH_CREC  *acrptr;

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

   if (WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (NULL, FI_LI, WATCH_MOD_AUTH,
                 "AuthCacheReset() !&Z !&Z", Realm, UserName);

   InstanceMutexLock (INSTANCE_MUTEX_AUTH_CACHE);

   RecordCount = AuthGblSecPtr->CacheRecordCount;
   RecordPoolPtr = AuthGblSecPtr->CacheRecordPool;

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      acrptr = RecordPoolPtr + (AuthCacheRecordSize * cnt);
      if (!acrptr->SourceRealm) continue;
      if (Realm && Realm[0] && strcmp (Realm, acrptr->Realm)) continue;
      if (UserName && UserName[0] && strcmp (UserName, acrptr->UserName)) continue;
      if (WATCH_MODULE(WATCH_MOD_AUTH))
         WatchThis (NULL, FI_LI, WATCH_MOD_AUTH,
                    "!&Z !&Z", acrptr->Realm, acrptr->UserName);
      memset (acrptr, 0, AuthCacheRecordSize);
   }

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_CACHE);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Display all records in the authentication cache.
*/

AuthCacheReport
(
REQUEST_STRUCT *rqptr,
REQUEST_AST NextTaskFunction
)
{
   static char  BeginPageFao [] =
"<P><TABLE CELLPADDING=3 CELLSPACING=0 BORDER=1>\n\
<TR><TH>Entries</TH></TR>\n\
<TR><TD>\n\
<TABLE CELLPADDING=0 CELLSPACING=5 BORDER=0>\n\
<TR><TH ALIGN=right>Since:&nbsp;</TH><TD>!20&W</TD></TR>\n\
<TR><TH ALIGN=right>Maximum:&nbsp;</TH><TD>!UL x !UL bytes</TD></TR>\n\
<TR><TH ALIGN=right>Current:&nbsp;</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right>Reused:&nbsp;</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right>Hits:&nbsp;</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right>Misses:&nbsp;</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right>Timeouts:&nbsp;</TH><TD>!UL</TD></TR>\n\
<TR><TH ALIGN=right></NOBR>Skeleton-Key:&nbsp;</NOBR></TH>\
<TD>!UL&nbsp;!AZ</TD></TR>\n\
</TABLE>\n\
</TD></TR>\n\
</TABLE>\n\
\
<P><TABLE CELLPADDING=1 CELLSPACING=0 BORDER=0>\n\\
<TR>\
<TH></TH>\
<TH COLSPAN=2 ALIGN=left><U>Realm</U>&nbsp;&nbsp;</TH>\
<TH COLSPAN=2 ALIGN=left><NOBR><U>Group-R+W</U>&nbsp;&nbsp;</NOBR></TH>\
<TH ALIGN=left><NOBR><U>Group-R</U>&nbsp;&nbsp;</NOBR></TH>\
<TH COLSPAN=4 ALIGN=left><NOBR><U>Config-Dir&nbsp;/&nbsp;Path-Param</U></NOBR></TH>\
</TR>\n<TR>\
<TH></TH>\
<TH ALIGN=left><U>User</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=left><U>Access</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U>Basic</U>&nbsp;&nbsp;</TH>\
<TH><U>Digest</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=left><NOBR><U>Most Recent</U>&nbsp;&nbsp;</NOBR></TH>\
<TH ALIGN=right><U>Cache</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U>Source</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><U>Fail</U>&nbsp;&nbsp;</TH>\
<TH ALIGN=right><NOBR><U>Break-in</U></NOBR></TH>\
<TH>&nbsp;&nbsp;</TH>\
</TR>\n\
<TR HEIGHT=5></TR>\n";

   /* the empty 99% column just forces the rest left with long request URIs */
   static char  RecordFao [] =
"<TR>\
<TH>!3ZL&nbsp;&nbsp;</TH>\
<TD COLSPAN=2 ALIGN=left><NOBR>!AZ!AZ</NOBR></TD>\
<TD COLSPAN=2 ALIGN=left><NOBR>!&@&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=left><NOBR>!&@&nbsp;&nbsp;</NOBR></TD>\
<TD COLSPAN=4 ALIGN=left><NOBR>!AZ !AZ</NOBR></TD>\
</TR>\n<TR>\
<TH></TH>\
<TD ALIGN=left BGCOLOR=\"#eeeeee\"><NOBR>!AZ&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=left BGCOLOR=\"#eeeeee\"><NOBR>!AZ!AZ!AZ&nbsp;&nbsp;</NOBR></TD>\
<TD ALIGN=right BGCOLOR=\"#eeeeee\">!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right BGCOLOR=\"#eeeeee\">!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=left BGCOLOR=\"#eeeeee\"><NOBR>!20%D&nbsp;&nbsp;</NOBR></FONT></TD>\
<TD ALIGN=right BGCOLOR=\"#eeeeee\">!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right BGCOLOR=\"#eeeeee\">!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right BGCOLOR=\"#eeeeee\">!UL&nbsp;&nbsp;</TD>\
<TD ALIGN=right BGCOLOR=\"#eeeeee\">!AZ!UL!AZ</TD>\
!AZ\
</TR>\n";

   static char EmptyCacheFao [] =
"<TR><TH><B>000</B>&nbsp;&nbsp;</TH>\
<TD COLSPAN=9 BGCOLOR=\"#eeeeee\"><I>empty</I></TD><TR>\n";

   static char  EndPageFao [] =
"</TABLE>\n\
<P><HR SIZE=1 NOSHADE WIDTH=60% ALIGN=LEFT>\n\
</BODY>\n\
</HTML>\n";

   int  cnt, status,
        RecordCount;
   unsigned long  FaoVector [32];
   unsigned long  *vecptr;
   unsigned char  *RecordPoolPtr;
   char  *cptr;
   AUTH_CREC  *acrptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH,
                 "AuthCacheReport() !&X", NextTaskFunction);

   rqptr->rqResponse.PreExpired = PRE_EXPIRE_ADMIN;
   RESPONSE_HEADER_200_HTML (rqptr);
   AdminPageTitle (rqptr, "User Authentication");

   InstanceMutexLock (INSTANCE_MUTEX_AUTH_CACHE);

   vecptr = FaoVector;
   *vecptr++ = &AuthGblSecPtr->SinceBinTime;
   *vecptr++ = AuthCacheRecordMax;
   *vecptr++ = AuthCacheRecordSize;
   *vecptr++ = AuthGblSecPtr->CacheRecordCount;
   *vecptr++ = AuthGblSecPtr->CacheReuseCount;
   *vecptr++ = AuthGblSecPtr->CacheHitCount;
   *vecptr++ = AuthGblSecPtr->CacheMissCount;
   *vecptr++ = AuthGblSecPtr->CacheTimeoutCount;
   *vecptr++ = AccountingPtr->AuthSkelKeyCount;
   if (HttpdGblSecPtr->AuthSkelKeyHttpdTickSecond)
      *vecptr++ = "(<B>Active</B>)";
   else
      *vecptr++ = "(Inactive)";
   status = NetWriteFaol (rqptr, BeginPageFao, &FaoVector);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   RecordCount = AuthGblSecPtr->CacheRecordCount;
   RecordPoolPtr = AuthGblSecPtr->CacheRecordPool;

   for (cnt = 0; cnt < RecordCount; cnt++)
   {
      acrptr = RecordPoolPtr + (AuthCacheRecordSize * cnt);
      if (!acrptr->SourceRealm) continue;

      cptr = AuthCanString (acrptr->AuthUserCan, AUTH_CAN_FORMAT_HTML);
      if (!cptr) cptr = "*ERROR*";

      vecptr = FaoVector;

      *vecptr++ = cnt+1;

      *vecptr++ = acrptr->Realm;
      *vecptr++ = AuthSourceString (acrptr->Realm, acrptr->SourceRealm);
      if (acrptr->GroupWrite[0])
      {
         *vecptr++ = "!AZ!AZ";
         *vecptr++ = acrptr->GroupWrite;
         *vecptr++ = AuthSourceString (acrptr->GroupWrite,
                                       acrptr->SourceGroupWrite);
      }
      else
         *vecptr++ = "<I>none</I>";
      if (acrptr->GroupRead[0])
      {
         *vecptr++ = "!AZ!AZ";
         *vecptr++ = acrptr->GroupRead;
         *vecptr++ = AuthSourceString (acrptr->GroupRead,
                                       acrptr->SourceGroupRead);
      }
      else
         *vecptr++ = "<I>none</I>";
      if (acrptr->ConfigDirectory[0])
         *vecptr++ = acrptr->ConfigDirectory;
      else
      if (!acrptr->PathParameter[0])
         *vecptr++ = "<I>neither</I>";
      else
         *vecptr++ = "";
      if (acrptr->PathParameter[0])
         *vecptr++ = acrptr->PathParameter;
      else
         *vecptr++ = "";

      *vecptr++ = acrptr->UserName;
      *vecptr++ = cptr;
      if (acrptr->VmsUserProfileLength)
         *vecptr++ = " (profile)";
      else
         *vecptr++ = "";
      if (acrptr->HttpsOnly)
         *vecptr++ = " (&quot;https:&quot;&nbsp;only)";
      else
         *vecptr++ = "";

      *vecptr++ = acrptr->BasicCount;
      *vecptr++ = acrptr->DigestCount;
      *vecptr++ = &acrptr->LastAccessBinTime;
      *vecptr++ = acrptr->AccessCount;
      *vecptr++ = acrptr->DataBaseCount;
      if (acrptr->FailureCount < Config.cfAuth.FailureLimit)
      {
         *vecptr++ = acrptr->FailureCount;
         *vecptr++ = "";
         *vecptr++ = 0;
         *vecptr++ = "";
      }
      else
      {
         *vecptr++ = 0;
         *vecptr++ = "<FONT COLOR=\"#ff0000\">";
         *vecptr++ = acrptr->FailureCount;
         *vecptr++ = "</FONT>";
      }

      if (rqptr->rqHeader.AdminNetscapeGold)
         *vecptr++ = "<TD></TD>";
      else
         *vecptr++ = "<TD WIDTH=99%></TD>";

      status = NetWriteFaol (rqptr, RecordFao, &FaoVector);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);
   }
   if (!RecordCount)
   {
      status = NetWriteFaol (rqptr, EmptyCacheFao, NULL);
      if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);
   }

   status = NetWriteFaol (rqptr, EndPageFao, NULL);
   if (VMSnok (status)) ErrorNoticed (status, "NetWriteFaol()", FI_LI);

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_CACHE);

   SysDclAst (NextTaskFunction, rqptr);
}

/*****************************************************************************/
/*
Set the "login" cookie.  See description at the beginning of the module.
*/

int AuthCacheSetLoginCookie (REQUEST_STRUCT *rqptr)

{
   BOOL  AlreadyLocked;
   int  idx, status,
        TokenNumber;
   unsigned short  Length;
   char  *cptr;
   char  Buffer [256];

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH, "AuthCacheSetLoginCookie()");

   /* three bytes of the least-significant time longword plus a count byte */
   TokenNumber = ((int)rqptr->rqTime.Vms64bit[0] & 0xffffff00) |
                    (AuthGblSecPtr->LoginCookieCount++ & 0x000000ff);

   if (!(AlreadyLocked = InstanceMutexHeld[INSTANCE_MUTEX_AUTH_CACHE]))
      InstanceMutexLock (INSTANCE_MUTEX_AUTH_CACHE);

   for (idx = 0; idx <= AuthGblSecPtr->LoginCookieCacheMax; idx++)
      if (!AuthGblSecPtr->LoginCookieCache[idx]) break;
   if (idx <= AuthGblSecPtr->LoginCookieCacheMax)
   {
      /* found a previously used but now empty cache entry */
      AuthGblSecPtr->LoginCookieCache[idx] = TokenNumber;
   }
   else
   if (AuthGblSecPtr->LoginCookieCacheMax < AUTH_MAX_COOKIE_CACHE)
   {
      /* the maximum number of entries in the cache have not yet been used */
      AuthGblSecPtr->LoginCookieCache[AuthGblSecPtr->LoginCookieCacheMax++] =
         TokenNumber;
   }
   else
   {
     /* complete cache in use, begin to cycle through using used entries */
     if (AuthGblSecPtr->LoginCookieCacheNext >
         AuthGblSecPtr->LoginCookieCacheMax)
        AuthGblSecPtr->LoginCookieCacheNext = 0;
     AuthGblSecPtr->LoginCookieCache[AuthGblSecPtr->LoginCookieCacheNext++] =
        TokenNumber;
   }

   WriteFao (Buffer, sizeof(Buffer), &Length, "!AZ=!UL; path=/;",
             AuthCacheLoginCookieName, TokenNumber);

   cptr = VmGetHeap (rqptr, Length+1);
   for (idx = 0; idx < RESPONSE_COOKIE_MAX; idx++)
   {
      if (!rqptr->rqResponse.CookiePtr[idx])
      {
         memcpy (rqptr->rqResponse.CookiePtr[idx] = cptr, Buffer, Length+1);
         break;
      }
   }

   if (AlreadyLocked) return (TokenNumber);

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_CACHE);

   return (TokenNumber);
}

/*****************************************************************************/
/*
Reset the "login" cookie on the browser.  See description at the beginning of
the module.
*/

AuthCacheResetLoginCookie (REQUEST_STRUCT *rqptr)

{
   BOOL  AlreadyLocked;
   int  idx, status;
   unsigned short  Length;
   char  *cptr;
   char  Buffer [256];

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH, "AuthCacheResetLoginCookie()");

   WriteFao (Buffer, sizeof(Buffer), &Length,
             "!AZ=; path=/; expires=Fri, 13 Jan 1978 14:00:00 GMT",
             AuthCacheLoginCookieName);

   cptr = VmGetHeap (rqptr, Length+1);

   if (!(AlreadyLocked = InstanceMutexHeld[INSTANCE_MUTEX_AUTH_CACHE]))
      InstanceMutexLock (INSTANCE_MUTEX_AUTH_CACHE);

   for (idx = 0; idx < RESPONSE_COOKIE_MAX; idx++)
   {
      if (!rqptr->rqResponse.CookiePtr[idx])
      {
         memcpy (rqptr->rqResponse.CookiePtr[idx] = cptr, Buffer, Length+1);
         break;
      }
   }

   if (AlreadyLocked) return;

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_CACHE);
}

/*****************************************************************************/
/*
Check for the "login" cookie in the request cookie.  If one is found look
through the revalidation cookie cache for the same number.  If found then empty
the entry and return true, otherwise always return false.  See description at
the beginning of the module.
*/

BOOL AuthCacheCheckLoginCookie (REQUEST_STRUCT *rqptr)

{
   BOOL  AlreadyLocked,
         TokenFound;
   int  idx, status,
        TokenNumber;
   char  *cptr;

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

   if (WATCHING(rqptr) && WATCH_MODULE(WATCH_MOD_AUTH))
      WatchThis (rqptr, FI_LI, WATCH_MOD_AUTH, "AuthCacheCheckLoginCookie()");

   if (!(cptr = rqptr->rqHeader.CookiePtr)) return (false);
   if (Debug) fprintf (stdout, "cptr |%s|\n", cptr);

   if (!(AlreadyLocked = InstanceMutexHeld[INSTANCE_MUTEX_AUTH_CACHE]))
      InstanceMutexLock (INSTANCE_MUTEX_AUTH_CACHE);

   TokenFound = false;
   while (*cptr)
   {
      while (*cptr && *cptr != AuthCacheLoginCookieName[0]) cptr++;
      if (!*cptr) break;
      if (!memcmp (cptr, AuthCacheLoginCookieName,
                         AuthCacheLoginCookieNameLength))
      {
         cptr += AuthCacheLoginCookieNameLength;
         if (*cptr == '=')
         {
            TokenNumber = atoi(cptr+1);
            if (Debug) fprintf (stdout, "TokenNumber: %u\n", TokenNumber);
            if (!TokenNumber) return (false);
            for (idx = 0; idx <= AuthGblSecPtr->LoginCookieCacheMax; idx++)
               if (AuthGblSecPtr->LoginCookieCache[idx] == TokenNumber) break;
            if (idx <= AuthGblSecPtr->LoginCookieCacheMax)
            {
               AuthGblSecPtr->LoginCookieCache[idx] = 0;
               {
                  TokenFound = true;
                  break;
               }
            }
            TokenFound = false;
            break;
         }
      }
      if (*cptr) cptr++;
   }

   if (AlreadyLocked) return (TokenFound);

   InstanceMutexUnLock (INSTANCE_MUTEX_AUTH_CACHE);

   return (TokenFound);
}

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

